tweak BT + first AirPlay commit

This commit is contained in:
philippe44
2019-08-17 23:39:05 -07:00
parent 2a770483a1
commit e9731ee232
22 changed files with 4642 additions and 101 deletions

1
.gitignore vendored
View File

@@ -66,3 +66,4 @@ libs/
/cdump.cmd
/_*
sdkconfig
*_history/

View File

@@ -1,65 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include "mdns.h"
#include "nvs.h"
#include "tcpip_adapter.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_pthread.h"
#include "esp_system.h"
#include "freertos/timers.h"
#include "airplay_sink.h"
#include "trace.h"
static const char * TAG = "platform";
extern char current_namespace[];
void airplay_sink_init(void) {
const char *hostname;
char *airplay_name, sink_name[32] = CONFIG_AIRPLAY_NAME;
nvs_handle nvs;
tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &hostname);
//initialize mDNS
ESP_ERROR_CHECK( mdns_init() );
ESP_ERROR_CHECK( mdns_hostname_set(hostname) );
//structure with TXT records
mdns_txt_item_t serviceTxtData[] = {
{"am", "esp32"},
{"tp", "UDP"},
{"sm","false"},
{"sv","false"},
{"ek","1"},
{"et","0,1"},
{"md","0,1,2"},
{"cn","0,1"},
{"ch","2"},
{"ss","16"},
{"sr","44100"},
{"vn","3"},
{"txtvers","1"},
};
if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = 31;
nvs_get_str(nvs, "airplay_sink_name", sink_name, &len);
nvs_close(nvs);
}
// AirPlay wants mDNS name to be MAC@name
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
asprintf(&airplay_name, "%02X%02X%02X%02X%02X%02X@%s", mac[3], mac[4], mac[5], mac[3], mac[4], mac[5], sink_name);
ESP_LOGI(TAG, "mdns hostname set to: [%s] with servicename %s", hostname, sink_name);
//initialize service
ESP_ERROR_CHECK( mdns_service_add(airplay_name, "_raop", "_tcp", 6000, serviceTxtData, sizeof(serviceTxtData) / sizeof(mdns_txt_item_t)) );
free(airplay_name);
}

View File

@@ -1,22 +0,0 @@
/*
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#ifndef __AIRPLAY_SINK_H__
#define __AIRPLAY_SINK_H__
#include <stdint.h>
//typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE,
//BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t;
/**
* @brief init sink mode (need to be provided)
*/
void airplay_sink_init(void);
#endif /* __AIRPLAY_SINK_H__*/

View File

@@ -355,7 +355,7 @@ static void bt_av_hdl_avrc_tg_evt(uint16_t event, void *p_param)
}
}
void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len))
void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb)
{
esp_err_t err;

View File

@@ -13,10 +13,18 @@
typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE,
BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t;
typedef void (*bt_cmd_cb_t)(bt_sink_cmd_t cmd, ...);
typedef void (*bt_data_cb_t)(const uint8_t *data, uint32_t len);
/**
* @brief init sink mode (need to be provided)
*/
void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len));
void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb);
/**
* @brief local command mode (stop, play, volume ...)
*/
void bt_sink_cmd(bt_sink_cmd_t event, ...);
#endif /* __BT_APP_SINK_H__*/

1135
components/raop/alac.c Normal file

File diff suppressed because it is too large Load Diff

55
components/raop/alac.h Normal file
View File

@@ -0,0 +1,55 @@
#ifndef __ALAC__DECOMP_H
#define __ALAC__DECOMP_H
typedef struct alac_file alac_file;
alac_file *create_alac(int samplesize, int numchannels);
void delete_alac(alac_file *alac);
void decode_frame(alac_file *alac,
unsigned char *inbuffer,
void *outbuffer, int *outputsize);
void alac_set_info(alac_file *alac, char *inputbuffer);
void allocate_buffers(alac_file *alac);
struct alac_file
{
unsigned char *input_buffer;
int input_buffer_bitaccumulator; /* used so we can do arbitary
bit reads */
int samplesize;
int numchannels;
int bytespersample;
/* buffers */
int32_t *predicterror_buffer_a;
int32_t *predicterror_buffer_b;
int32_t *outputsamples_buffer_a;
int32_t *outputsamples_buffer_b;
int32_t *uncompressed_bytes_buffer_a;
int32_t *uncompressed_bytes_buffer_b;
/* stuff from setinfo */
uint32_t setinfo_max_samples_per_frame; /* 0x1000 = 4096 */ /* max samples per frame? */
uint8_t setinfo_7a; /* 0x00 */
uint8_t setinfo_sample_size; /* 0x10 */
uint8_t setinfo_rice_historymult; /* 0x28 */
uint8_t setinfo_rice_initialhistory; /* 0x0a */
uint8_t setinfo_rice_kmodifier; /* 0x0e */
uint8_t setinfo_7f; /* 0x02 */
uint16_t setinfo_80; /* 0x00ff */
uint32_t setinfo_82; /* 0x000020e7 */ /* max sample size?? */
uint32_t setinfo_86; /* 0x00069fe4 */ /* bit rate (avarge)?? */
uint32_t setinfo_8a_rate; /* 0x0000ac44 */
/* end setinfo stuff */
};
#endif /* __ALAC__DECOMP_H */

View File

@@ -7,4 +7,7 @@
# please read the SDK documents if you need to do this.
#
CFLAGS += -I$(COMPONENT_PATH)/../tools
CFLAGS += -fstack-usage \
-I$(COMPONENT_PATH)/../tools \
-I$(COMPONENT_PATH)/../codecs/inc/alac

View File

@@ -0,0 +1,545 @@
#include "dmap_parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define DMAP_STRINGIFY_(x) #x
#define DMAP_STRINGIFY(x) DMAP_STRINGIFY_(x)
typedef enum {
DMAP_UNKNOWN,
DMAP_UINT,
DMAP_INT,
DMAP_STR,
DMAP_DATA,
DMAP_DATE,
DMAP_VERS,
DMAP_DICT,
DMAP_ITEM
} DMAP_TYPE;
typedef struct {
/**
* The four-character code used in the encoded message.
*/
const char *code;
/**
* The type of data associated with the content code.
*/
DMAP_TYPE type;
/**
* For listings, the type of their listing item children.
*
* Listing items (mlit) can be of any type, and as with other content codes
* their type information is not encoded in the message. Parsers must
* determine the type of the listing items based on their parent context.
*/
DMAP_TYPE list_item_type;
/**
* A human-readable name for the content code.
*/
const char *name;
} dmap_field;
static const dmap_field dmap_fields[] = {
{ "abal", DMAP_DICT, DMAP_STR, "daap.browsealbumlisting" },
{ "abar", DMAP_DICT, DMAP_STR, "daap.browseartistlisting" },
{ "abcp", DMAP_DICT, DMAP_STR, "daap.browsecomposerlisting" },
{ "abgn", DMAP_DICT, DMAP_STR, "daap.browsegenrelisting" },
{ "abpl", DMAP_UINT, 0, "daap.baseplaylist" },
{ "abro", DMAP_DICT, 0, "daap.databasebrowse" },
{ "adbs", DMAP_DICT, 0, "daap.databasesongs" },
{ "aeAD", DMAP_DICT, 0, "com.apple.itunes.adam-ids-array" },
{ "aeAI", DMAP_UINT, 0, "com.apple.itunes.itms-artistid" },
{ "aeCD", DMAP_DATA, 0, "com.apple.itunes.flat-chapter-data" },
{ "aeCF", DMAP_UINT, 0, "com.apple.itunes.cloud-flavor-id" },
{ "aeCI", DMAP_UINT, 0, "com.apple.itunes.itms-composerid" },
{ "aeCK", DMAP_UINT, 0, "com.apple.itunes.cloud-library-kind" },
{ "aeCM", DMAP_UINT, 0, "com.apple.itunes.cloud-match-type" },
{ "aeCR", DMAP_STR, 0, "com.apple.itunes.content-rating" } ,
{ "aeCS", DMAP_UINT, 0, "com.apple.itunes.artworkchecksum" },
{ "aeCU", DMAP_UINT, 0, "com.apple.itunes.cloud-user-id" },
{ "aeCd", DMAP_UINT, 0, "com.apple.itunes.cloud-id" },
{ "aeDE", DMAP_STR, 0, "com.apple.itunes.longest-content-description" },
{ "aeDL", DMAP_UINT, 0, "com.apple.itunes.drm-downloader-user-id" },
{ "aeDP", DMAP_UINT, 0, "com.apple.itunes.drm-platform-id" },
{ "aeDR", DMAP_UINT, 0, "com.apple.itunes.drm-user-id" },
{ "aeDV", DMAP_UINT, 0, "com.apple.itunes.drm-versions" },
{ "aeEN", DMAP_STR, 0, "com.apple.itunes.episode-num-str" },
{ "aeES", DMAP_UINT, 0, "com.apple.itunes.episode-sort" },
{ "aeFA", DMAP_UINT, 0, "com.apple.itunes.drm-family-id" },
{ "aeGD", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-dr" } ,
{ "aeGE", DMAP_UINT, 0, "com.apple.itunes.gapless-enc-del" },
{ "aeGH", DMAP_UINT, 0, "com.apple.itunes.gapless-heur" },
{ "aeGI", DMAP_UINT, 0, "com.apple.itunes.itms-genreid" },
{ "aeGR", DMAP_UINT, 0, "com.apple.itunes.gapless-resy" },
{ "aeGU", DMAP_UINT, 0, "com.apple.itunes.gapless-dur" },
{ "aeGs", DMAP_UINT, 0, "com.apple.itunes.can-be-genius-seed" },
{ "aeHC", DMAP_UINT, 0, "com.apple.itunes.has-chapter-data" },
{ "aeHD", DMAP_UINT, 0, "com.apple.itunes.is-hd-video" },
{ "aeHV", DMAP_UINT, 0, "com.apple.itunes.has-video" },
{ "aeK1", DMAP_UINT, 0, "com.apple.itunes.drm-key1-id" },
{ "aeK2", DMAP_UINT, 0, "com.apple.itunes.drm-key2-id" },
{ "aeMC", DMAP_UINT, 0, "com.apple.itunes.playlist-contains-media-type-count" },
{ "aeMK", DMAP_UINT, 0, "com.apple.itunes.mediakind" },
{ "aeMX", DMAP_STR, 0, "com.apple.itunes.movie-info-xml" },
{ "aeMk", DMAP_UINT, 0, "com.apple.itunes.extended-media-kind" },
{ "aeND", DMAP_UINT, 0, "com.apple.itunes.non-drm-user-id" },
{ "aeNN", DMAP_STR, 0, "com.apple.itunes.network-name" },
{ "aeNV", DMAP_UINT, 0, "com.apple.itunes.norm-volume" },
{ "aePC", DMAP_UINT, 0, "com.apple.itunes.is-podcast" },
{ "aePI", DMAP_UINT, 0, "com.apple.itunes.itms-playlistid" },
{ "aePP", DMAP_UINT, 0, "com.apple.itunes.is-podcast-playlist" },
{ "aePS", DMAP_UINT, 0, "com.apple.itunes.special-playlist" },
{ "aeRD", DMAP_UINT, 0, "com.apple.itunes.rental-duration" },
{ "aeRP", DMAP_UINT, 0, "com.apple.itunes.rental-pb-start" },
{ "aeRS", DMAP_UINT, 0, "com.apple.itunes.rental-start" },
{ "aeRU", DMAP_UINT, 0, "com.apple.itunes.rental-pb-duration" },
{ "aeRf", DMAP_UINT, 0, "com.apple.itunes.is-featured" },
{ "aeSE", DMAP_UINT, 0, "com.apple.itunes.store-pers-id" },
{ "aeSF", DMAP_UINT, 0, "com.apple.itunes.itms-storefrontid" },
{ "aeSG", DMAP_UINT, 0, "com.apple.itunes.saved-genius" },
{ "aeSI", DMAP_UINT, 0, "com.apple.itunes.itms-songid" },
{ "aeSN", DMAP_STR, 0, "com.apple.itunes.series-name" },
{ "aeSP", DMAP_UINT, 0, "com.apple.itunes.smart-playlist" },
{ "aeSU", DMAP_UINT, 0, "com.apple.itunes.season-num" },
{ "aeSV", DMAP_VERS, 0, "com.apple.itunes.music-sharing-version" },
{ "aeXD", DMAP_STR, 0, "com.apple.itunes.xid" },
{ "aecp", DMAP_STR, 0, "com.apple.itunes.collection-description" },
{ "aels", DMAP_UINT, 0, "com.apple.itunes.liked-state" },
{ "aemi", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing-item" },
{ "aeml", DMAP_DICT, 0, "com.apple.itunes.media-kind-listing" },
{ "agac", DMAP_UINT, 0, "daap.groupalbumcount" },
{ "agma", DMAP_UINT, 0, "daap.groupmatchedqueryalbumcount" },
{ "agmi", DMAP_UINT, 0, "daap.groupmatchedqueryitemcount" },
{ "agrp", DMAP_STR, 0, "daap.songgrouping" },
{ "ajAE", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-type" },
{ "ajAS", DMAP_UINT, 0, "com.apple.itunes.store.ams-episode-sort-order" },
{ "ajAT", DMAP_UINT, 0, "com.apple.itunes.store.ams-show-type" },
{ "ajAV", DMAP_UINT, 0, "com.apple.itunes.store.is-ams-video" },
{ "ajal", DMAP_UINT, 0, "com.apple.itunes.store.album-liked-state" },
{ "ajcA", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" },
{ "ajca", DMAP_UINT, 0, "com.apple.itunes.store.show-composer-as-artist" },
{ "ajuw", DMAP_UINT, 0, "com.apple.itunes.store.use-work-name-as-display-name" },
{ "amvc", DMAP_UINT, 0, "daap.songmovementcount" },
{ "amvm", DMAP_STR, 0, "daap.songmovementname" },
{ "amvn", DMAP_UINT, 0, "daap.songmovementnumber" },
{ "aply", DMAP_DICT, 0, "daap.databaseplaylists" },
{ "aprm", DMAP_UINT, 0, "daap.playlistrepeatmode" },
{ "apro", DMAP_VERS, 0, "daap.protocolversion" },
{ "apsm", DMAP_UINT, 0, "daap.playlistshufflemode" },
{ "apso", DMAP_DICT, 0, "daap.playlistsongs" },
{ "arif", DMAP_DICT, 0, "daap.resolveinfo" },
{ "arsv", DMAP_DICT, 0, "daap.resolve" },
{ "asaa", DMAP_STR, 0, "daap.songalbumartist" },
{ "asac", DMAP_UINT, 0, "daap.songartworkcount" },
{ "asai", DMAP_UINT, 0, "daap.songalbumid" },
{ "asal", DMAP_STR, 0, "daap.songalbum" },
{ "asar", DMAP_STR, 0, "daap.songartist" },
{ "asas", DMAP_UINT, 0, "daap.songalbumuserratingstatus" },
{ "asbk", DMAP_UINT, 0, "daap.bookmarkable" },
{ "asbo", DMAP_UINT, 0, "daap.songbookmark" },
{ "asbr", DMAP_UINT, 0, "daap.songbitrate" },
{ "asbt", DMAP_UINT, 0, "daap.songbeatsperminute" },
{ "ascd", DMAP_UINT, 0, "daap.songcodectype" },
{ "ascm", DMAP_STR, 0, "daap.songcomment" },
{ "ascn", DMAP_STR, 0, "daap.songcontentdescription" },
{ "asco", DMAP_UINT, 0, "daap.songcompilation" },
{ "ascp", DMAP_STR, 0, "daap.songcomposer" },
{ "ascr", DMAP_UINT, 0, "daap.songcontentrating" },
{ "ascs", DMAP_UINT, 0, "daap.songcodecsubtype" },
{ "asct", DMAP_STR, 0, "daap.songcategory" },
{ "asda", DMAP_DATE, 0, "daap.songdateadded" },
{ "asdb", DMAP_UINT, 0, "daap.songdisabled" },
{ "asdc", DMAP_UINT, 0, "daap.songdisccount" },
{ "asdk", DMAP_UINT, 0, "daap.songdatakind" },
{ "asdm", DMAP_DATE, 0, "daap.songdatemodified" },
{ "asdn", DMAP_UINT, 0, "daap.songdiscnumber" },
{ "asdp", DMAP_DATE, 0, "daap.songdatepurchased" },
{ "asdr", DMAP_DATE, 0, "daap.songdatereleased" },
{ "asdt", DMAP_STR, 0, "daap.songdescription" },
{ "ased", DMAP_UINT, 0, "daap.songextradata" },
{ "aseq", DMAP_STR, 0, "daap.songeqpreset" },
{ "ases", DMAP_UINT, 0, "daap.songexcludefromshuffle" },
{ "asfm", DMAP_STR, 0, "daap.songformat" },
{ "asgn", DMAP_STR, 0, "daap.songgenre" },
{ "asgp", DMAP_UINT, 0, "daap.songgapless" },
{ "asgr", DMAP_UINT, 0, "daap.supportsgroups" },
{ "ashp", DMAP_UINT, 0, "daap.songhasbeenplayed" },
{ "askd", DMAP_DATE, 0, "daap.songlastskipdate" },
{ "askp", DMAP_UINT, 0, "daap.songuserskipcount" },
{ "asky", DMAP_STR, 0, "daap.songkeywords" },
{ "aslc", DMAP_STR, 0, "daap.songlongcontentdescription" },
{ "aslr", DMAP_UINT, 0, "daap.songalbumuserrating" },
{ "asls", DMAP_UINT, 0, "daap.songlongsize" },
{ "aspc", DMAP_UINT, 0, "daap.songuserplaycount" },
{ "aspl", DMAP_DATE, 0, "daap.songdateplayed" },
{ "aspu", DMAP_STR, 0, "daap.songpodcasturl" },
{ "asri", DMAP_UINT, 0, "daap.songartistid" },
{ "asrs", DMAP_UINT, 0, "daap.songuserratingstatus" },
{ "asrv", DMAP_INT, 0, "daap.songrelativevolume" },
{ "assa", DMAP_STR, 0, "daap.sortartist" },
{ "assc", DMAP_STR, 0, "daap.sortcomposer" },
{ "assl", DMAP_STR, 0, "daap.sortalbumartist" },
{ "assn", DMAP_STR, 0, "daap.sortname" },
{ "assp", DMAP_UINT, 0, "daap.songstoptime" },
{ "assr", DMAP_UINT, 0, "daap.songsamplerate" },
{ "asss", DMAP_STR, 0, "daap.sortseriesname" },
{ "asst", DMAP_UINT, 0, "daap.songstarttime" },
{ "assu", DMAP_STR, 0, "daap.sortalbum" },
{ "assz", DMAP_UINT, 0, "daap.songsize" },
{ "astc", DMAP_UINT, 0, "daap.songtrackcount" },
{ "astm", DMAP_UINT, 0, "daap.songtime" },
{ "astn", DMAP_UINT, 0, "daap.songtracknumber" },
{ "asul", DMAP_STR, 0, "daap.songdataurl" },
{ "asur", DMAP_UINT, 0, "daap.songuserrating" },
{ "asvc", DMAP_UINT, 0, "daap.songprimaryvideocodec" },
{ "asyr", DMAP_UINT, 0, "daap.songyear" },
{ "ated", DMAP_UINT, 0, "daap.supportsextradata" },
{ "avdb", DMAP_DICT, 0, "daap.serverdatabases" },
{ "awrk", DMAP_STR, 0, "daap.songwork" },
{ "caar", DMAP_UINT, 0, "dacp.availablerepeatstates" },
{ "caas", DMAP_UINT, 0, "dacp.availableshufflestates" },
{ "caci", DMAP_DICT, 0, "caci" },
{ "cafe", DMAP_UINT, 0, "dacp.fullscreenenabled" },
{ "cafs", DMAP_UINT, 0, "dacp.fullscreen" },
{ "caia", DMAP_UINT, 0, "dacp.isactive" },
{ "cana", DMAP_STR, 0, "dacp.nowplayingartist" },
{ "cang", DMAP_STR, 0, "dacp.nowplayinggenre" },
{ "canl", DMAP_STR, 0, "dacp.nowplayingalbum" },
{ "cann", DMAP_STR, 0, "dacp.nowplayingname" },
{ "canp", DMAP_UINT, 0, "dacp.nowplayingids" },
{ "cant", DMAP_UINT, 0, "dacp.nowplayingtime" },
{ "capr", DMAP_VERS, 0, "dacp.protocolversion" },
{ "caps", DMAP_UINT, 0, "dacp.playerstate" },
{ "carp", DMAP_UINT, 0, "dacp.repeatstate" },
{ "cash", DMAP_UINT, 0, "dacp.shufflestate" },
{ "casp", DMAP_DICT, 0, "dacp.speakers" },
{ "cast", DMAP_UINT, 0, "dacp.songtime" },
{ "cavc", DMAP_UINT, 0, "dacp.volumecontrollable" },
{ "cave", DMAP_UINT, 0, "dacp.visualizerenabled" },
{ "cavs", DMAP_UINT, 0, "dacp.visualizer" },
{ "ceJC", DMAP_UINT, 0, "com.apple.itunes.jukebox-client-vote" },
{ "ceJI", DMAP_UINT, 0, "com.apple.itunes.jukebox-current" },
{ "ceJS", DMAP_UINT, 0, "com.apple.itunes.jukebox-score" },
{ "ceJV", DMAP_UINT, 0, "com.apple.itunes.jukebox-vote" },
{ "ceQR", DMAP_DICT, 0, "com.apple.itunes.playqueue-contents-response" },
{ "ceQa", DMAP_STR, 0, "com.apple.itunes.playqueue-album" },
{ "ceQg", DMAP_STR, 0, "com.apple.itunes.playqueue-genre" },
{ "ceQn", DMAP_STR, 0, "com.apple.itunes.playqueue-name" },
{ "ceQr", DMAP_STR, 0, "com.apple.itunes.playqueue-artist" },
{ "cmgt", DMAP_DICT, 0, "dmcp.getpropertyresponse" },
{ "cmmk", DMAP_UINT, 0, "dmcp.mediakind" },
{ "cmpr", DMAP_VERS, 0, "dmcp.protocolversion" },
{ "cmsr", DMAP_UINT, 0, "dmcp.serverrevision" },
{ "cmst", DMAP_DICT, 0, "dmcp.playstatus" },
{ "cmvo", DMAP_UINT, 0, "dmcp.volume" },
{ "f\215ch", DMAP_UINT, 0, "dmap.haschildcontainers" },
{ "ipsa", DMAP_DICT, 0, "dpap.iphotoslideshowadvancedoptions" },
{ "ipsl", DMAP_DICT, 0, "dpap.iphotoslideshowoptions" },
{ "mbcl", DMAP_DICT, 0, "dmap.bag" },
{ "mccr", DMAP_DICT, 0, "dmap.contentcodesresponse" },
{ "mcna", DMAP_STR, 0, "dmap.contentcodesname" },
{ "mcnm", DMAP_UINT, 0, "dmap.contentcodesnumber" },
{ "mcon", DMAP_DICT, 0, "dmap.container" },
{ "mctc", DMAP_UINT, 0, "dmap.containercount" },
{ "mcti", DMAP_UINT, 0, "dmap.containeritemid" },
{ "mcty", DMAP_UINT, 0, "dmap.contentcodestype" },
{ "mdbk", DMAP_UINT, 0, "dmap.databasekind" },
{ "mdcl", DMAP_DICT, 0, "dmap.dictionary" },
{ "mdst", DMAP_UINT, 0, "dmap.downloadstatus" },
{ "meds", DMAP_UINT, 0, "dmap.editcommandssupported" },
{ "meia", DMAP_UINT, 0, "dmap.itemdateadded" },
{ "meip", DMAP_UINT, 0, "dmap.itemdateplayed" },
{ "mext", DMAP_UINT, 0, "dmap.objectextradata" },
{ "miid", DMAP_UINT, 0, "dmap.itemid" },
{ "mikd", DMAP_UINT, 0, "dmap.itemkind" },
{ "mimc", DMAP_UINT, 0, "dmap.itemcount" },
{ "minm", DMAP_STR, 0, "dmap.itemname" },
{ "mlcl", DMAP_DICT, DMAP_DICT, "dmap.listing" },
{ "mlid", DMAP_UINT, 0, "dmap.sessionid" },
{ "mlit", DMAP_ITEM, 0, "dmap.listingitem" },
{ "mlog", DMAP_DICT, 0, "dmap.loginresponse" },
{ "mpco", DMAP_UINT, 0, "dmap.parentcontainerid" },
{ "mper", DMAP_UINT, 0, "dmap.persistentid" },
{ "mpro", DMAP_VERS, 0, "dmap.protocolversion" },
{ "mrco", DMAP_UINT, 0, "dmap.returnedcount" },
{ "mrpr", DMAP_UINT, 0, "dmap.remotepersistentid" },
{ "msal", DMAP_UINT, 0, "dmap.supportsautologout" },
{ "msas", DMAP_UINT, 0, "dmap.authenticationschemes" },
{ "msau", DMAP_UINT, 0, "dmap.authenticationmethod" },
{ "msbr", DMAP_UINT, 0, "dmap.supportsbrowse" },
{ "msdc", DMAP_UINT, 0, "dmap.databasescount" },
{ "msex", DMAP_UINT, 0, "dmap.supportsextensions" },
{ "msix", DMAP_UINT, 0, "dmap.supportsindex" },
{ "mslr", DMAP_UINT, 0, "dmap.loginrequired" },
{ "msma", DMAP_UINT, 0, "dmap.machineaddress" },
{ "msml", DMAP_DICT, 0, "msml" },
{ "mspi", DMAP_UINT, 0, "dmap.supportspersistentids" },
{ "msqy", DMAP_UINT, 0, "dmap.supportsquery" },
{ "msrs", DMAP_UINT, 0, "dmap.supportsresolve" },
{ "msrv", DMAP_DICT, 0, "dmap.serverinforesponse" },
{ "mstc", DMAP_DATE, 0, "dmap.utctime" },
{ "mstm", DMAP_UINT, 0, "dmap.timeoutinterval" },
{ "msto", DMAP_INT, 0, "dmap.utcoffset" },
{ "msts", DMAP_STR, 0, "dmap.statusstring" },
{ "mstt", DMAP_UINT, 0, "dmap.status" },
{ "msup", DMAP_UINT, 0, "dmap.supportsupdate" },
{ "mtco", DMAP_UINT, 0, "dmap.specifiedtotalcount" },
{ "mudl", DMAP_DICT, 0, "dmap.deletedidlisting" },
{ "mupd", DMAP_DICT, 0, "dmap.updateresponse" },
{ "musr", DMAP_UINT, 0, "dmap.serverrevision" },
{ "muty", DMAP_UINT, 0, "dmap.updatetype" },
{ "pasp", DMAP_STR, 0, "dpap.aspectratio" },
{ "pcmt", DMAP_STR, 0, "dpap.imagecomments" },
{ "peak", DMAP_UINT, 0, "com.apple.itunes.photos.album-kind" },
{ "peed", DMAP_DATE, 0, "com.apple.itunes.photos.exposure-date" },
{ "pefc", DMAP_DICT, 0, "com.apple.itunes.photos.faces" },
{ "peki", DMAP_UINT, 0, "com.apple.itunes.photos.key-image-id" },
{ "pekm", DMAP_DICT, 0, "com.apple.itunes.photos.key-image" },
{ "pemd", DMAP_DATE, 0, "com.apple.itunes.photos.modification-date" },
{ "pfai", DMAP_DICT, 0, "dpap.failureids" },
{ "pfdt", DMAP_DICT, 0, "dpap.filedata" },
{ "pfmt", DMAP_STR, 0, "dpap.imageformat" },
{ "phgt", DMAP_UINT, 0, "dpap.imagepixelheight" },
{ "picd", DMAP_DATE, 0, "dpap.creationdate" },
{ "pifs", DMAP_UINT, 0, "dpap.imagefilesize" },
{ "pimf", DMAP_STR, 0, "dpap.imagefilename" },
{ "plsz", DMAP_UINT, 0, "dpap.imagelargefilesize" },
{ "ppro", DMAP_VERS, 0, "dpap.protocolversion" },
{ "prat", DMAP_UINT, 0, "dpap.imagerating" },
{ "pret", DMAP_DICT, 0, "dpap.retryids" },
{ "pwth", DMAP_UINT, 0, "dpap.imagepixelwidth" }
};
static const size_t dmap_field_count = sizeof(dmap_fields) / sizeof(dmap_field);
typedef int (*sort_func) (const void *, const void *);
int dmap_version(void) {
return DMAP_VERSION;
}
const char *dmap_version_string(void) {
return DMAP_STRINGIFY(DMAP_VERSION_MAJOR) "."
DMAP_STRINGIFY(DMAP_VERSION_MINOR) "."
DMAP_STRINGIFY(DMAP_VERSION_PATCH);
}
static int dmap_field_sort(const dmap_field *a, const dmap_field *b) {
return memcmp(a->code, b->code, 4);
}
static const dmap_field *dmap_field_from_code(const char *code) {
dmap_field key;
key.code = code;
return bsearch(&key, dmap_fields, dmap_field_count, sizeof(dmap_field), (sort_func)dmap_field_sort);
}
const char *dmap_name_from_code(const char *code) {
const dmap_field *field;
if (!code)
return NULL;
field = dmap_field_from_code(code);
return field ? field->name : NULL;
}
static uint16_t dmap_read_u16(const char *buf) {
return (uint16_t)(((buf[0] & 0xff) << 8) | (buf[1] & 0xff));
}
static int16_t dmap_read_i16(const char *buf) {
return (int16_t)dmap_read_u16(buf);
}
static uint32_t dmap_read_u32(const char *buf) {
return ((uint32_t)(buf[0] & 0xff) << 24) |
((uint32_t)(buf[1] & 0xff) << 16) |
((uint32_t)(buf[2] & 0xff) << 8) |
((uint32_t)(buf[3] & 0xff));
}
static int32_t dmap_read_i32(const char *buf) {
return (int32_t)dmap_read_u32(buf);
}
static uint64_t dmap_read_u64(const char *buf) {
return ((uint64_t)(buf[0] & 0xff) << 56) |
((uint64_t)(buf[1] & 0xff) << 48) |
((uint64_t)(buf[2] & 0xff) << 40) |
((uint64_t)(buf[3] & 0xff) << 32) |
((uint64_t)(buf[4] & 0xff) << 24) |
((uint64_t)(buf[5] & 0xff) << 16) |
((uint64_t)(buf[6] & 0xff) << 8) |
((uint64_t)(buf[7] & 0xff));
}
static int64_t dmap_read_i64(const char *buf) {
return (int64_t)dmap_read_u64(buf);
}
static int dmap_parse_internal(const dmap_settings *settings, const char *buf, size_t len, const dmap_field *parent) {
const dmap_field *field;
DMAP_TYPE field_type;
size_t field_len;
const char *field_name;
const char *p = buf;
const char *end = buf + len;
char code[5] = {0};
if (!settings || !buf)
return -1;
while (end - p >= 8) {
memcpy(code, p, 4);
field = dmap_field_from_code(code);
p += 4;
field_len = dmap_read_u32(p);
p += 4;
if (p + field_len > end)
return -1;
if (field) {
field_type = field->type;
field_name = field->name;
if (field_type == DMAP_ITEM) {
if (parent != NULL && parent->list_item_type) {
field_type = parent->list_item_type;
} else {
field_type = DMAP_DICT;
}
}
} else {
/* Make a best guess of the type */
field_type = DMAP_UNKNOWN;
field_name = code;
if (field_len >= 8) {
/* Look for a four char code followed by a length within the current field */
if (isalpha(p[0] & 0xff) &&
isalpha(p[1] & 0xff) &&
isalpha(p[2] & 0xff) &&
isalpha(p[3] & 0xff)) {
if (dmap_read_u32(p + 4) < field_len)
field_type = DMAP_DICT;
}
}
if (field_type == DMAP_UNKNOWN) {
size_t i;
int is_string = 1;
for (i=0; i < field_len; i++) {
if (!isprint(p[i] & 0xff)) {
is_string = 0;
break;
}
}
field_type = is_string ? DMAP_STR : DMAP_UINT;
}
}
switch (field_type) {
case DMAP_UINT:
/* Determine the integer's type based on its size */
switch (field_len) {
case 1:
if (settings->on_uint32)
settings->on_uint32(settings->ctx, code, field_name, (unsigned char)*p);
break;
case 2:
if (settings->on_uint32)
settings->on_uint32(settings->ctx, code, field_name, dmap_read_u16(p));
break;
case 4:
if (settings->on_uint32)
settings->on_uint32(settings->ctx, code, field_name, dmap_read_u32(p));
break;
case 8:
if (settings->on_uint64)
settings->on_uint64(settings->ctx, code, field_name, dmap_read_u64(p));
break;
default:
if (settings->on_data)
settings->on_data(settings->ctx, code, field_name, p, field_len);
break;
}
break;
case DMAP_INT:
switch (field_len) {
case 1:
if (settings->on_int32)
settings->on_int32(settings->ctx, code, field_name, *p);
break;
case 2:
if (settings->on_int32)
settings->on_int32(settings->ctx, code, field_name, dmap_read_i16(p));
break;
case 4:
if (settings->on_int32)
settings->on_int32(settings->ctx, code, field_name, dmap_read_i32(p));
break;
case 8:
if (settings->on_int64)
settings->on_int64(settings->ctx, code, field_name, dmap_read_i64(p));
break;
default:
if (settings->on_data)
settings->on_data(settings->ctx, code, field_name, p, field_len);
break;
}
break;
case DMAP_STR:
if (settings->on_string)
settings->on_string(settings->ctx, code, field_name, p, field_len);
break;
case DMAP_DATA:
if (settings->on_data)
settings->on_data(settings->ctx, code, field_name, p, field_len);
break;
case DMAP_DATE:
/* Seconds since epoch */
if (settings->on_date)
settings->on_date(settings->ctx, code, field_name, dmap_read_u32(p));
break;
case DMAP_VERS:
if (settings->on_string && field_len >= 4) {
char version[20];
sprintf(version, "%u.%u", dmap_read_u16(p), dmap_read_u16(p+2));
settings->on_string(settings->ctx, code, field_name, version, strlen(version));
}
break;
case DMAP_DICT:
if (settings->on_dict_start)
settings->on_dict_start(settings->ctx, code, field_name);
if (dmap_parse_internal(settings, p, field_len, field) != 0)
return -1;
if (settings->on_dict_end)
settings->on_dict_end(settings->ctx, code, field_name);
break;
case DMAP_ITEM:
/* Unreachable: listing item types are always mapped to another type */
abort();
case DMAP_UNKNOWN:
break;
}
p += field_len;
}
if (p != end)
return -1;
return 0;
}
int dmap_parse(const dmap_settings *settings, const char *buf, size_t len) {
return dmap_parse_internal(settings, buf, len, NULL);
}

View File

@@ -0,0 +1,90 @@
#ifndef dmap_parser_h
#define dmap_parser_h
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#define DMAP_VERSION_MAJOR 1
#define DMAP_VERSION_MINOR 2
#define DMAP_VERSION_PATCH 1
#define DMAP_VERSION (DMAP_VERSION_MAJOR * 1000000 + \
DMAP_VERSION_MINOR * 1000 + \
DMAP_VERSION_PATCH)
/*
* Callbacks invoked during parsing.
*
* @param ctx The context pointer specified in the dmap_settings structure.
* @param code The content code from the message.
* @param name The name associated with the content code, if known. If there is
* no known name this parameter contains the same value as the code
* parameter.
*/
typedef void (*dmap_dict_cb) (void *ctx, const char *code, const char *name);
typedef void (*dmap_int32_cb) (void *ctx, const char *code, const char *name, int32_t value);
typedef void (*dmap_int64_cb) (void *ctx, const char *code, const char *name, int64_t value);
typedef void (*dmap_uint32_cb) (void *ctx, const char *code, const char *name, uint32_t value);
typedef void (*dmap_uint64_cb) (void *ctx, const char *code, const char *name, uint64_t value);
typedef void (*dmap_data_cb) (void *ctx, const char *code, const char *name, const char *buf, size_t len);
typedef struct {
/* Callbacks to indicate the start and end of dictionary fields. */
dmap_dict_cb on_dict_start;
dmap_dict_cb on_dict_end;
/* Callbacks for field data. */
dmap_int32_cb on_int32;
dmap_int64_cb on_int64;
dmap_uint32_cb on_uint32;
dmap_uint64_cb on_uint64;
dmap_uint32_cb on_date;
dmap_data_cb on_string;
dmap_data_cb on_data;
/** A context pointer passed to each callback function. */
void *ctx;
} dmap_settings;
/**
* Returns the library version number.
*
* The version number format is (major * 1000000) + (minor * 1000) + patch.
* For example, the value for version 1.2.3 is 1002003.
*/
int dmap_version(void);
/**
* Returns the library version as a string.
*/
const char *dmap_version_string(void);
/**
* Returns the name associated with the provided content code, or NULL if there
* is no known name.
*
* For example, if given the code "minm" this function returns "dmap.itemname".
*/
const char *dmap_name_from_code(const char *code);
/**
* Parses a DMAP message buffer using the provided settings.
*
* @param settings A dmap_settings structure populated with the callbacks to
* invoke during parsing.
* @param buf Pointer to a DMAP message buffer. The buffer must contain a
* complete message.
* @param len The length of the DMAP message buffer.
*
* @return 0 if parsing was successful, or -1 if an error occurred.
*/
int dmap_parse(const dmap_settings *settings, const char *buf, size_t len);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,40 @@
/*
* logging utility
*
* (c) Adrian Smith 2012-2015, triode1@btinternet.com
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef __LOG_UTIL_H
#define __LOG_UTIL_H
#include "platform.h"
typedef enum { lERROR = 0, lWARN, lINFO, lDEBUG, lSDEBUG } log_level;
const char *logtime(void);
void logprint(const char *fmt, ...);
log_level debug2level(char *level);
char *level2debug(log_level level);
#define LOG_ERROR(fmt, ...) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) if (*loglevel >= lWARN) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) if (*loglevel >= lINFO) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) if (*loglevel >= lDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define LOG_SDEBUG(fmt, ...) if (*loglevel >= lSDEBUG) logprint("%s %s:%d " fmt "\n", logtime(), __FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif

127
components/raop/platform.h Normal file
View File

@@ -0,0 +1,127 @@
/*
* platform setting definition
*
* (c) Philippe, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef __PLATFORM_H
#define __PLATFORM_H
#ifdef WIN32
#define LINUX 0
#define WIN 1
#else
#define LINUX 1
#define WIN 0
#endif
#include <stdbool.h>
#include <signal.h>
#include <sys/stat.h>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <io.h>
#include <iphlpapi.h>
#include <sys/timeb.h>
typedef unsigned __int8 u8_t;
typedef unsigned __int16 u16_t;
typedef unsigned __int32 u32_t;
typedef unsigned __int64 u64_t;
typedef __int16 s16_t;
typedef __int32 s32_t;
typedef __int64 s64_t;
#define inline __inline
int gettimeofday(struct timeval *tv, struct timezone *tz);
char *strcasestr(const char *haystack, const char *needle);
#define usleep(x) Sleep((x)/1000)
#define sleep(x) Sleep((x)*1000)
#define last_error() WSAGetLastError()
#define ERROR_WOULDBLOCK WSAEWOULDBLOCK
#define open _open
#define read _read
#define poll WSAPoll
#define snprintf _snprintf
#define strcasecmp stricmp
#define _random(x) random(x)
#define VALGRIND_MAKE_MEM_DEFINED(x,y)
#define S_ADDR(X) X.S_un.S_addr
#define in_addr_t u32_t
#define socklen_t int
#define ssize_t int
#define RTLD_NOW 0
#else
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
/*
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/time.h>
#include <netdb.h>
*/
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/poll.h>
#include <lwip/inet.h>
#include <pthread.h>
#include <errno.h>
#define min(a,b) (((a) < (b)) ? (a) : (b))
#define max(a,b) (((a) > (b)) ? (a) : (b))
typedef int16_t s16_t;
typedef int32_t s32_t;
typedef int64_t s64_t;
typedef uint8_t u8_t;
typedef uint16_t u16_t;
typedef uint32_t u32_t;
typedef unsigned long long u64_t;
#define last_error() errno
#define ERROR_WOULDBLOCK EWOULDBLOCK
char *strlwr(char *str);
#define _random(x) random()
#define closesocket(s) close(s)
#define S_ADDR(X) X.s_addr
#endif
typedef struct ntp_s {
u32_t seconds;
u32_t fraction;
} ntp_t;
u64_t timeval_to_ntp(struct timeval tv, struct ntp_s *ntp);
u64_t get_ntp(struct ntp_s *ntp);
// we expect somebody to provide the ms clock, system-wide
u32_t _gettime_ms_(void);
#define gettime_ms _gettime_ms_

823
components/raop/raop.c Normal file
View File

@@ -0,0 +1,823 @@
/*
*
* (c) Philippe 2019, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <stdio.h>
#include "platform.h"
#ifdef WIN32
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/engine.h>
#include "mdns.h"
#include "mdnsd.h"
#include "mdnssd-itf.h"
#else
#include "esp_pthread.h"
#include "mdns.h"
#include "mbedtls/version.h"
#include <mbedtls/x509.h>
#endif
#include "util.h"
#include "raop.h"
#include "rtp.h"
#include "dmap_parser.h"
#include "log_util.h"
typedef struct raop_ctx_s {
#ifdef WIN32
struct mdns_service *svc;
struct mdnsd *svr;
#endif
struct in_addr host; // IP of bridge
short unsigned port; // RTSP port for AirPlay
int sock; // socket of the above
struct in_addr peer; // IP of the iDevice (airplay sender)
bool running;
bool drift;
#ifdef WIN32
pthread_t thread, search_thread;
#else
TaskHandle_t thread, search_thread, joiner;
#endif
unsigned char mac[6];
int latency;
struct {
char *aesiv, *aeskey;
char *fmtp;
} rtsp;
struct rtp_s *rtp;
raop_cmd_cb_t cmd_cb;
raop_data_cb_t data_cb;
/*
struct {
char DACPid[32], id[32];
struct in_addr host;
u16_t port;
struct mDNShandle_s *handle;
} active_remote;
*/
void *owner;
} raop_ctx_t;
extern struct mdnsd* glmDNSServer;
extern log_level raop_loglevel;
static log_level *loglevel = &raop_loglevel;
static void* rtsp_thread(void *arg);
static bool handle_rtsp(raop_ctx_t *ctx, int sock);
static char* rsa_apply(unsigned char *input, int inlen, int *outlen, int mode);
static int base64_pad(char *src, char **padded);
static int base64_encode(const void *data, int size, char **str);
static int base64_decode(const char *str, void *data);
static void* search_remote(void *args);
extern char private_key[];
enum { RSA_MODE_KEY, RSA_MODE_AUTH };
static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len);
/*----------------------------------------------------------------------------*/
struct raop_ctx_s *raop_create(struct in_addr host, char *name,
unsigned char mac[6], int latency,
raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) {
struct raop_ctx_s *ctx = malloc(sizeof(struct raop_ctx_s));
struct sockaddr_in addr;
char id[64];
#ifdef WIN32
socklen_t nlen = sizeof(struct sockaddr);
char *txt[] = { "am=esp32", "tp=UDP", "sm=false", "sv=false", "ek=1",
"et=0,1", "md=0,1,2", "cn=0,1", "ch=2",
"ss=16", "sr=44100", "vn=3", "txtvers=1",
NULL };
#else
mdns_txt_item_t txt[] = {
{"am", "esp32"},
{"tp", "UDP"},
{"sm","false"},
{"sv","false"},
{"ek","1"},
{"et","0,1"},
{"md","0,1,2"},
{"cn","0,1"},
{"ch","2"},
{"ss","16"},
{"sr","44100"},
{"vn","3"},
{"txtvers","1"},
};
#endif
if (!ctx) return NULL;
// make sure we have a clean context
memset(ctx, 0, sizeof(raop_ctx_t));
#ifdef WIN32
ctx->svr = glmDNSServer;
#endif
ctx->host = host;
ctx->sock = socket(AF_INET, SOCK_STREAM, 0);
ctx->cmd_cb = cmd_cb;
ctx->data_cb = data_cb;
ctx->drift = false;
ctx->latency = min(latency, 44100);
if (ctx->sock == -1) {
LOG_ERROR("Cannot create listening socket", NULL);
free(ctx);
return NULL;
}
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = host.s_addr;
addr.sin_family = AF_INET;
#ifdef WIN32
addr.sin_port = 0;
#else
ctx->port = 5000;
addr.sin_port = htons(ctx->port);
#endif
if (bind(ctx->sock, (struct sockaddr *) &addr, sizeof(addr)) < 0 || listen(ctx->sock, 1)) {
LOG_ERROR("Cannot bind or listen RTSP listener: %s", strerror(errno));
free(ctx);
closesocket(ctx->sock);
return NULL;
}
#ifdef WIN32
getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen);
ctx->port = ntohs(addr.sin_port);
#endif
ctx->running = true;
memcpy(ctx->mac, mac, 6);
snprintf(id, 64, "%02X%02X%02X%02X%02X%02X@%s", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], name);
#ifdef WIN32
// seems that Windows snprintf does not add NULL char if actual size > max
id[63] = '\0';
ctx->svc = mdnsd_register_svc(ctx->svr, id, "_raop._tcp.local", ctx->port, NULL, (const char**) txt);
pthread_create(&ctx->thread, NULL, &rtsp_thread, ctx);
#else
LOG_INFO("starting mDNS with %s", id);
ESP_ERROR_CHECK( mdns_service_add(id, "_raop", "_tcp", ctx->port, txt, sizeof(txt) / sizeof(mdns_txt_item_t)) );
xTaskCreate((TaskFunction_t) rtsp_thread, "RTSP_thread", 8*1024, ctx, ESP_TASK_PRIO_MIN + 1, &ctx->thread);
#endif
return ctx;
}
/*----------------------------------------------------------------------------*/
void raop_delete(struct raop_ctx_s *ctx) {
int sock;
struct sockaddr addr;
socklen_t nlen = sizeof(struct sockaddr);
if (!ctx) return;
ctx->running = false;
// wake-up thread by connecting socket, needed for freeBSD
sock = socket(AF_INET, SOCK_STREAM, 0);
getsockname(ctx->sock, (struct sockaddr *) &addr, &nlen);
connect(sock, (struct sockaddr*) &addr, sizeof(addr));
closesocket(sock);
#ifdef WIN32
pthread_join(ctx->thread, NULL);
#else
ctx->joiner = xTaskGetCurrentTaskHandle();
xTaskNotifyWait(0, 0, NULL, portMAX_DELAY);
#endif
rtp_end(ctx->rtp);
#ifdef WIN32
shutdown(ctx->sock, SD_BOTH);
#else
shutdown(ctx->sock, SHUT_RDWR);
#endif
closesocket(ctx->sock);
/*
// terminate search, but do not reclaim memory of pthread if never launched
if (ctx->active_remote.handle) {
close_mDNS(ctx->active_remote.handle);
pthread_join(ctx->search_thread, NULL);
}
*/
NFREE(ctx->rtsp.aeskey);
NFREE(ctx->rtsp.aesiv);
NFREE(ctx->rtsp.fmtp);
// stop broadcasting devices
#ifdef WIN32
mdns_service_remove(ctx->svr, ctx->svc);
mdnsd_stop(ctx->svr);
#endif
free(ctx);
}
/*----------------------------------------------------------------------------*/
void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param) {
/*
struct sockaddr_in addr;
int sock;
char *command = NULL;
// first notify the remote controller (if any)
switch(event) {
case RAOP_PAUSE:
command = strdup("pause");
break;
case RAOP_PLAY:
command = strdup("play");
break;
case RAOP_STOP:
command = strdup("stop");
break;
case RAOP_VOLUME: {
float Volume = *((float*) param);
Volume = Volume ? (Volume - 1) * 30 : -144;
asprintf(&command,"setproperty?dmcp.device-volume=%0.4lf", Volume);
break;
}
default:
break;
}
// no command to send to remote or no remote found yet
if (!command || !ctx->active_remote.port) {
NFREE(command);
return;
}
sock = socket(AF_INET, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = S_ADDR(ctx->active_remote.host);
addr.sin_port = htons(ctx->active_remote.port);
if (!connect(sock, (struct sockaddr*) &addr, sizeof(addr))) {
char *method, *buf, resp[512] = "";
int len;
key_data_t headers[4] = { {NULL, NULL} };
asprintf(&method, "GET /ctrl-int/1/%s HTTP/1.0", command);
kd_add(headers, "Active-Remote", ctx->active_remote.id);
kd_add(headers, "Connection", "close");
buf = http_send(sock, method, headers);
len = recv(sock, resp, 512, 0);
if (len > 0) resp[len-1] = '\0';
LOG_INFO("[%p]: sending airplay remote\n%s<== received ==>\n%s", ctx, buf, resp);
NFREE(method);
NFREE(buf);
kd_free(headers);
}
free(command);
closesocket(sock);
*/
// then notify local system
ctx->cmd_cb(event, param);
}
/*----------------------------------------------------------------------------*/
static void *rtsp_thread(void *arg) {
raop_ctx_t *ctx = (raop_ctx_t*) arg;
int sock = -1;
while (ctx->running) {
fd_set rfds;
struct timeval timeout = {0, 100*1000};
int n;
bool res = false;
if (sock == -1) {
struct sockaddr_in peer;
socklen_t addrlen = sizeof(struct sockaddr_in);
sock = accept(ctx->sock, (struct sockaddr*) &peer, &addrlen);
ctx->peer.s_addr = peer.sin_addr.s_addr;
if (sock != -1 && ctx->running) {
LOG_INFO("got RTSP connection %u", sock);
} else continue;
}
FD_ZERO(&rfds);
FD_SET(sock, &rfds);
n = select(sock + 1, &rfds, NULL, NULL, &timeout);
if (!n) continue;
if (n > 0) res = handle_rtsp(ctx, sock);
if (n < 0 || !res) {
closesocket(sock);
LOG_INFO("RTSP close %u", sock);
sock = -1;
}
}
if (sock != -1) closesocket(sock);
#ifndef WIN32
xTaskNotify(ctx->joiner, 0, eNoAction);
vTaskDelete(NULL);
#endif
return NULL;
}
/*----------------------------------------------------------------------------*/
static bool handle_rtsp(raop_ctx_t *ctx, int sock)
{
char *buf = NULL, *body = NULL, method[16] = "";
key_data_t headers[16], resp[8] = { {NULL, NULL} };
int len;
bool success = true;
if (!http_parse(sock, method, headers, &body, &len)) {
NFREE(body);
kd_free(headers);
return false;
}
if (strcmp(method, "OPTIONS")) {
LOG_INFO("[%p]: received %s", ctx, method);
}
if ((buf = kd_lookup(headers, "Apple-Challenge")) != NULL) {
int n;
char *buf_pad, *p, *data_b64 = NULL, data[32];
LOG_INFO("[%p]: challenge %s", ctx, buf);
// need to pad the base64 string as apple device don't
base64_pad(buf, &buf_pad);
p = data + min(base64_decode(buf_pad, data), 32-10);
p = (char*) memcpy(p, &S_ADDR(ctx->host), 4) + 4;
p = (char*) memcpy(p, ctx->mac, 6) + 6;
memset(p, 0, 32 - (p - data));
p = rsa_apply((unsigned char*) data, 32, &n, RSA_MODE_AUTH);
n = base64_encode(p, n, &data_b64);
// remove padding as well (seems to be optional now)
for (n = strlen(data_b64) - 1; n > 0 && data_b64[n] == '='; data_b64[n--] = '\0');
kd_add(resp, "Apple-Response", data_b64);
NFREE(p);
NFREE(buf_pad);
NFREE(data_b64);
}
if (!strcmp(method, "OPTIONS")) {
kd_add(resp, "Public", "ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER");
} else if (!strcmp(method, "ANNOUNCE")) {
char *padded, *p;
NFREE(ctx->rtsp.aeskey);
NFREE(ctx->rtsp.aesiv);
NFREE(ctx->rtsp.fmtp);
if ((p = strcasestr(body, "rsaaeskey")) != NULL) {
unsigned char *aeskey;
int len, outlen;
p = strextract(p, ":", "\r\n");
base64_pad(p, &padded);
aeskey = malloc(strlen(padded));
len = base64_decode(padded, aeskey);
ctx->rtsp.aeskey = rsa_apply(aeskey, len, &outlen, RSA_MODE_KEY);
NFREE(p);
NFREE(aeskey);
NFREE(padded);
}
if ((p = strcasestr(body, "aesiv")) != NULL) {
p = strextract(p, ":", "\r\n");
base64_pad(p, &padded);
ctx->rtsp.aesiv = malloc(strlen(padded));
base64_decode(padded, ctx->rtsp.aesiv);
NFREE(p);
NFREE(padded);
}
if ((p = strcasestr(body, "fmtp")) != NULL) {
p = strextract(p, ":", "\r\n");
ctx->rtsp.fmtp = strdup(p);
NFREE(p);
}
// on announce, search remote
/*
if ((buf = kd_lookup(headers, "DACP-ID")) != NULL) strcpy(ctx->active_remote.DACPid, buf);
if ((buf = kd_lookup(headers, "Active-Remote")) != NULL) strcpy(ctx->active_remote.id, buf);
ctx->active_remote.handle = init_mDNS(false, ctx->host);
pthread_create(&ctx->search_thread, NULL, &search_remote, ctx);
*/
} else if (!strcmp(method, "SETUP") && ((buf = kd_lookup(headers, "Transport")) != NULL)) {
char *p;
rtp_resp_t rtp = { 0 };
short unsigned tport = 0, cport = 0;
if ((p = strcasestr(buf, "timing_port")) != NULL) sscanf(p, "%*[^=]=%hu", &tport);
if ((p = strcasestr(buf, "control_port")) != NULL) sscanf(p, "%*[^=]=%hu", &cport);
rtp = rtp_init(ctx->peer, false, ctx->drift, true, ctx->latency,
ctx->rtsp.aeskey, ctx->rtsp.aesiv, ctx->rtsp.fmtp,
cport, tport, ctx->data_cb);
ctx->rtp = rtp.ctx;
if (cport * tport * rtp.cport * rtp.tport * rtp.aport && rtp.ctx) {
char *transport;
asprintf(&transport, "RTP/AVP/UDP;unicast;mode=record;control_port=%u;timing_port=%u;server_port=%u", rtp.cport, rtp.tport, rtp.aport);
LOG_DEBUG("[%p]: audio=(%hu:%hu), timing=(%hu:%hu), control=(%hu:%hu)", ctx, 0, rtp.aport, tport, rtp.tport, cport, rtp.cport);
kd_add(resp, "Transport", transport);
kd_add(resp, "Session", "DEADBEEF");
free(transport);
} else {
success = false;
LOG_INFO("[%p]: cannot start session, missing ports", ctx);
}
} else if (!strcmp(method, "RECORD")) {
unsigned short seqno = 0;
unsigned rtptime = 0;
char *p;
if (ctx->latency) {
char latency[6];
snprintf(latency, 6, "%u", ctx->latency);
kd_add(resp, "Audio-Latency", latency);
}
buf = kd_lookup(headers, "RTP-Info");
if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno);
if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime);
if (ctx->rtp) rtp_record(ctx->rtp, seqno, rtptime);
ctx->cmd_cb(RAOP_STREAM, NULL);
} else if (!strcmp(method, "FLUSH")) {
unsigned short seqno = 0;
unsigned rtptime = 0;
char *p;
buf = kd_lookup(headers, "RTP-Info");
if ((p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno);
if ((p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime);
// only send FLUSH if useful (discards frames above buffer head and top)
if (ctx->rtp && rtp_flush(ctx->rtp, seqno, rtptime))
ctx->cmd_cb(RAOP_FLUSH, NULL);
} else if (!strcmp(method, "TEARDOWN")) {
rtp_end(ctx->rtp);
ctx->rtp = NULL;
/*
// need to make sure no search is on-going and reclaim pthread memory
if (ctx->active_remote.handle) close_mDNS(ctx->active_remote.handle);
pthread_join(ctx->search_thread, NULL);
memset(&ctx->active_remote, 0, sizeof(ctx->active_remote));
*/
NFREE(ctx->rtsp.aeskey);
NFREE(ctx->rtsp.aesiv);
NFREE(ctx->rtsp.fmtp);
ctx->cmd_cb(RAOP_STOP, NULL);
} if (!strcmp(method, "SET_PARAMETER")) {
char *p;
if ((p = strcasestr(body, "volume")) != NULL) {
float volume;
sscanf(p, "%*[^:]:%f", &volume);
LOG_INFO("[%p]: SET PARAMETER volume %f", ctx, volume);
volume = (volume == -144.0) ? 0 : (1 + volume / 30);
ctx->cmd_cb(RAOP_VOLUME, &volume);
}
/*
if (((p = kd_lookup(headers, "Content-Type")) != NULL) && !strcasecmp(p, "application/x-dmap-tagged")) {
struct metadata_s metadata;
dmap_settings settings = {
NULL, NULL, NULL, NULL, NULL, NULL, NULL, on_dmap_string, NULL,
NULL
};
settings.ctx = &metadata;
memset(&metadata, 0, sizeof(struct metadata_s));
if (!dmap_parse(&settings, body, len)) {
LOG_INFO("[%p]: received metadata\n\tartist: %s\n\talbum: %s\n\ttitle: %s",
ctx, metadata.artist, metadata.album, metadata.title);
free_metadata(&metadata);
}
}
*/
}
// don't need to free "buf" because kd_lookup return a pointer, not a strdup
kd_add(resp, "Audio-Jack-Status", "connected; type=analog");
kd_add(resp, "CSeq", kd_lookup(headers, "CSeq"));
if (success) buf = http_send(sock, "RTSP/1.0 200 OK", resp);
else buf = http_send(sock, "RTSP/1.0 500 ERROR", NULL);
if (strcmp(method, "OPTIONS")) {
LOG_INFO("[%p]: responding:\n%s", ctx, buf ? buf : "<void>");
}
NFREE(body);
NFREE(buf);
kd_free(resp);
kd_free(headers);
return true;
}
/*----------------------------------------------------------------------------*/
/*
bool search_remote_cb(mDNSservice_t *slist, void *cookie, bool *stop) {
mDNSservice_t *s;
raop_ctx_t *ctx = (raop_ctx_t*) cookie;
// see if we have found an active remote for our ID
for (s = slist; s; s = s->next) {
if (strcasestr(s->name, ctx->active_remote.DACPid)) {
ctx->active_remote.host = s->addr;
ctx->active_remote.port = s->port;
LOG_INFO("[%p]: found ActiveRemote for %s at %s:%u", ctx, ctx->active_remote.DACPid,
inet_ntoa(ctx->active_remote.host), ctx->active_remote.port);
*stop = true;
break;
}
}
// let caller clear list
return false;
}
*/
/*----------------------------------------------------------------------------*/
/*
static void* search_remote(void *args) {
raop_ctx_t *ctx = (raop_ctx_t*) args;
query_mDNS(ctx->active_remote.handle, "_dacp._tcp.local", 0, 0, &search_remote_cb, (void*) ctx);
return NULL;
}
*/
/*----------------------------------------------------------------------------*/
static char *rsa_apply(unsigned char *input, int inlen, int *outlen, int mode)
{
static char super_secret_key[] =
"-----BEGIN RSA PRIVATE KEY-----\n"
"MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n"
"wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n"
"wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n"
"/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n"
"UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n"
"BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n"
"LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n"
"NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n"
"lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n"
"aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n"
"a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n"
"oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n"
"oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n"
"k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n"
"AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n"
"cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n"
"54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n"
"17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n"
"1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n"
"LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n"
"2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n"
"-----END RSA PRIVATE KEY-----";
#ifdef WIN32
unsigned char *out;
RSA *rsa;
BIO *bmem = BIO_new_mem_buf(super_secret_key, -1);
rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL);
BIO_free(bmem);
out = malloc(RSA_size(rsa));
switch (mode) {
case RSA_MODE_AUTH:
*outlen = RSA_private_encrypt(inlen, input, out, rsa,
RSA_PKCS1_PADDING);
break;
case RSA_MODE_KEY:
*outlen = RSA_private_decrypt(inlen, input, out, rsa,
RSA_PKCS1_OAEP_PADDING);
break;
}
RSA_free(rsa);
return (char*) out;
#else
mbedtls_pk_context pkctx;
mbedtls_rsa_context *trsa;
size_t olen;
/*
we should do entropy initialization & pass a rng function but this
consumes a ton of stack and there is no security concern here. Anyway,
mbedtls takes a lot of stack, unfortunately ...
*/
mbedtls_pk_init(&pkctx);
mbedtls_pk_parse_key(&pkctx, (unsigned char *)super_secret_key,
sizeof(super_secret_key), NULL, 0);
uint8_t *outbuf = NULL;
trsa = mbedtls_pk_rsa(pkctx);
switch (mode) {
case RSA_MODE_AUTH:
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_NONE);
outbuf = malloc(trsa->len);
mbedtls_rsa_pkcs1_encrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, inlen, input, outbuf);
*outlen = trsa->len;
break;
case RSA_MODE_KEY:
mbedtls_rsa_set_padding(trsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA1);
outbuf = malloc(trsa->len);
mbedtls_rsa_pkcs1_decrypt(trsa, NULL, NULL, MBEDTLS_RSA_PRIVATE, &olen, input, outbuf, trsa->len);
*outlen = olen;
break;
}
mbedtls_pk_free(&pkctx);
return (char*) outbuf;
#endif
}
#define DECODE_ERROR 0xffffffff
static char base64_chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/*----------------------------------------------------------------------------*/
static int base64_pad(char *src, char **padded)
{
int n;
n = strlen(src) + strlen(src) % 4;
*padded = malloc(n + 1);
memset(*padded, '=', n);
memcpy(*padded, src, strlen(src));
(*padded)[n] = '\0';
return strlen(*padded);
}
/*----------------------------------------------------------------------------*/
static int pos(char c)
{
char *p;
for (p = base64_chars; *p; p++)
if (*p == c)
return p - base64_chars;
return -1;
}
/*----------------------------------------------------------------------------*/
static int base64_encode(const void *data, int size, char **str)
{
char *s, *p;
int i;
int c;
const unsigned char *q;
p = s = (char *) malloc(size * 4 / 3 + 4);
if (p == NULL) return -1;
q = (const unsigned char *) data;
i = 0;
for (i = 0; i < size;) {
c = q[i++];
c *= 256;
if (i < size) c += q[i];
i++;
c *= 256;
if (i < size) c += q[i];
i++;
p[0] = base64_chars[(c & 0x00fc0000) >> 18];
p[1] = base64_chars[(c & 0x0003f000) >> 12];
p[2] = base64_chars[(c & 0x00000fc0) >> 6];
p[3] = base64_chars[(c & 0x0000003f) >> 0];
if (i > size) p[3] = '=';
if (i > size + 1) p[2] = '=';
p += 4;
}
*p = 0;
*str = s;
return strlen(s);
}
/*----------------------------------------------------------------------------*/
static unsigned int token_decode(const char *token)
{
int i;
unsigned int val = 0;
int marker = 0;
if (strlen(token) < 4)
return DECODE_ERROR;
for (i = 0; i < 4; i++) {
val *= 64;
if (token[i] == '=')
marker++;
else if (marker > 0)
return DECODE_ERROR;
else
val += pos(token[i]);
}
if (marker > 2)
return DECODE_ERROR;
return (marker << 24) | val;
}
/*----------------------------------------------------------------------------*/
static int base64_decode(const char *str, void *data)
{
const char *p;
unsigned char *q;

32
components/raop/raop.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* AirCast: Chromecast to AirPlay
*
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef __RAOP_H
#define __RAOP_H
#include "platform.h"
#include "raop_sink.h"
struct raop_ctx_s* raop_create(struct in_addr host, char *name, unsigned char mac[6], int latency,
raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb);
void raop_delete(struct raop_ctx_s *ctx);
void raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param);
#endif

View File

@@ -0,0 +1,68 @@
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include "mdns.h"
#include "nvs.h"
#include "tcpip_adapter.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_pthread.h"
#include "esp_system.h"
#include "freertos/timers.h"
#include "raop.h"
#include "log_util.h"
#include "trace.h"
static const char * TAG = "platform";
extern char current_namespace[];
log_level raop_loglevel = lINFO;
log_level util_loglevel;
static log_level *loglevel = &raop_loglevel;
static struct raop_ctx_s *raop;
/****************************************************************************************
* Airplay sink initialization
*/
void raop_sink_init(raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb) {
const char *hostname;
char sink_name[64-6] = CONFIG_AIRPLAY_NAME;
nvs_handle nvs;
tcpip_adapter_ip_info_t ipInfo;
struct in_addr host;
// get various IP info
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);
tcpip_adapter_get_hostname(TCPIP_ADAPTER_IF_STA, &hostname);
host.s_addr = ipInfo.ip.addr;
//initialize mDNS
ESP_ERROR_CHECK( mdns_init() );
ESP_ERROR_CHECK( mdns_hostname_set(hostname) );
if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(sink_name) - 1;
nvs_get_str(nvs, "airplay_sink_name", sink_name, &len);
nvs_close(nvs);
}
ESP_LOGI(TAG, "mdns hostname set to: [%s] with servicename %s", hostname, sink_name);
//initialize service
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
raop = raop_create(host, sink_name, mac, 44100, cmd_cb, data_cb);
}
/****************************************************************************************
* Airplay local command (stop, start, volume ...)
*/
void raop_sink_cmd(raop_event_t event, void *param) {
raop_cmd(raop, event, param);
}

View File

@@ -0,0 +1,31 @@
/*
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#ifndef RAOP_SINK_H
#define RAOP_SINK_H
#include <stdint.h>
typedef enum { RAOP_STREAM, RAOP_PLAY, RAOP_FLUSH, RAOP_PAUSE, RAOP_STOP, RAOP_VOLUME } raop_event_t ;
typedef void (*raop_cmd_cb_t)(raop_event_t event, void *param);
typedef void (*raop_data_cb_t)(const u8_t *data, size_t len);
/**
* @brief init sink mode (need to be provided)
*/
void raop_sink_init(raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb);
/**
* @brief init sink mode (need to be provided)
*/
void raop_sink_cmd(raop_event_t event, void *param);
#endif /* RAOP_SINK_H*/

893
components/raop/rtp.c Normal file
View File

@@ -0,0 +1,893 @@
/*
* HairTunes - RAOP packet handler and slave-clocked replay engine
* Copyright (c) James Laird 2011
* All rights reserved.
*
* Modularisation: philippe_44@outlook.com, 2019
*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/types.h>
#include <pthread.h>
#include <math.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdint.h>
#include <fcntl.h>
#include <assert.h>
#include "platform.h"
#include "rtp.h"
#include "log_util.h"
#include "util.h"
#ifdef WIN32
#include <openssl/aes.h>
#include "alac.h"
#else
#include "esp_pthread.h"
#include "esp_system.h"
#include <mbedtls/version.h>
#include <mbedtls/aes.h>
//#include "alac_wrapper.h"
#include "alac.h"
#endif
#define NTP2MS(ntp) ((((ntp) >> 10) * 1000L) >> 22)
#define MS2NTP(ms) (((((u64_t) (ms)) << 22) / 1000) << 10)
#define NTP2TS(ntp, rate) ((((ntp) >> 16) * (rate)) >> 16)
#define TS2NTP(ts, rate) (((((u64_t) (ts)) << 16) / (rate)) << 16)
#define MS2TS(ms, rate) ((((u64_t) (ms)) * (rate)) / 1000)
#define TS2MS(ts, rate) NTP2MS(TS2NTP(ts,rate))
#define GAP_THRES 8
#define GAP_COUNT 20
extern log_level raop_loglevel;
static log_level *loglevel = &raop_loglevel;
//#define __RTP_STORE
// default buffer size
#define BUFFER_FRAMES (44100 / 352 + 1)
#define MAX_PACKET 1408
#define RTP_SYNC (0x01)
#define NTP_SYNC (0x02)
#define RESEND_TO 200
enum { DATA, CONTROL, TIMING };
static const u8_t silence_frame[MAX_PACKET] = { 0 };
typedef u16_t seq_t;
typedef struct audio_buffer_entry { // decoded audio packets
int ready;
u32_t rtptime, last_resend;
s16_t *data;
int len;
} abuf_t;
typedef struct rtp_s {
#ifdef __RTP_STORE
FILE *rtpIN, *rtpOUT;
#endif
bool running;
unsigned char aesiv[16];
#ifdef WIN32
AES_KEY aes;
#else
mbedtls_aes_context aes;
#endif
bool decrypt, range;
int frame_size, frame_duration;
int in_frames, out_frames;
struct in_addr host;
struct sockaddr_in rtp_host;
struct {
unsigned short rport, lport;
int sock;
} rtp_sockets[3]; // data, control, timing
struct timing_s {
bool drift;
u64_t local, remote;
u32_t count, gap_count;
s64_t gap_sum, gap_adjust;
} timing;
struct {
u32_t rtp, time;
u8_t status;
bool first, required;
} synchro;
struct {
u32_t time;
seq_t seqno;
u32_t rtptime;
} record;
int latency; // rtp hold depth in samples
u32_t resent_frames; // total recovered frames
u32_t silent_frames; // total silence frames
u32_t filled_frames; // silence frames in current silence episode
int skip; // number of frames to skip to keep sync alignement
abuf_t audio_buffer[BUFFER_FRAMES];
seq_t ab_read, ab_write;
pthread_mutex_t ab_mutex;
#ifdef WIN32
pthread_t rtp_thread;
#else
TaskHandle_t rtp_thread, joiner;
#endif
alac_file *alac_codec;
int flush_seqno;
bool playing;
rtp_data_cb_t callback;
} rtp_t;
#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES)
static void buffer_alloc(abuf_t *audio_buffer, int size);
static void buffer_release(abuf_t *audio_buffer);
static void buffer_reset(abuf_t *audio_buffer);
static void buffer_push_packet(rtp_t *ctx);
static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last);
static bool rtp_request_timing(rtp_t *ctx);
static void* rtp_thread_func(void *arg);
static int seq_order(seq_t a, seq_t b);
/*---------------------------------------------------------------------------*/
static alac_file* alac_init(int fmtp[32]) {
alac_file *alac;
int sample_size = fmtp[3];
if (sample_size != 16) {
LOG_ERROR("sample size must be 16 %d", sample_size);
return false;
}
alac = create_alac(sample_size, 2);
if (!alac) {
LOG_ERROR("cannot create alac codec", NULL);
return NULL;
}
alac->setinfo_max_samples_per_frame = fmtp[1];
alac->setinfo_7a = fmtp[2];
alac->setinfo_sample_size = sample_size;
alac->setinfo_rice_historymult = fmtp[4];
alac->setinfo_rice_initialhistory = fmtp[5];
alac->setinfo_rice_kmodifier = fmtp[6];
alac->setinfo_7f = fmtp[7];
alac->setinfo_80 = fmtp[8];
alac->setinfo_82 = fmtp[9];
alac->setinfo_86 = fmtp[10];
alac->setinfo_8a_rate = fmtp[11];
allocate_buffers(alac);
return alac;
}
/*---------------------------------------------------------------------------*/
rtp_resp_t rtp_init(struct in_addr host, bool sync, bool drift, bool range,
int latency, char *aeskey, char *aesiv, char *fmtpstr,
short unsigned pCtrlPort, short unsigned pTimingPort,
rtp_data_cb_t callback)
{
int i = 0;
char *arg;
int fmtp[12];
bool rc = true;
rtp_t *ctx = calloc(1, sizeof(rtp_t));
rtp_resp_t resp = { 0, 0, 0, NULL };
if (!ctx) return resp;
ctx->host = host;
ctx->decrypt = false;
ctx->callback = callback;
ctx->rtp_host.sin_family = AF_INET;
ctx->rtp_host.sin_addr.s_addr = INADDR_ANY;
pthread_mutex_init(&ctx->ab_mutex, 0);
ctx->flush_seqno = -1;
ctx->latency = latency;
ctx->synchro.required = sync;
ctx->timing.drift = drift;
ctx->range = range;
// write pointer = last written, read pointer = next to read so fill = w-r+1
ctx->ab_read = ctx->ab_write + 1;
#ifdef __RTP_STORE
ctx->rtpIN = fopen("airplay.rtpin", "wb");
ctx->rtpOUT = fopen("airplay.rtpout", "wb");
#endif
ctx->rtp_sockets[CONTROL].rport = pCtrlPort;
ctx->rtp_sockets[TIMING].rport = pTimingPort;
if (aesiv && aeskey) {
memcpy(ctx->aesiv, aesiv, 16);
#ifdef WIN32
AES_set_decrypt_key((unsigned char*) aeskey, 128, &ctx->aes);
#else
memset(&ctx->aes, 0, sizeof(mbedtls_aes_context));
mbedtls_aes_setkey_dec(&ctx->aes, (unsigned char*) aeskey, 128);
#endif
ctx->decrypt = true;
}
memset(fmtp, 0, sizeof(fmtp));
while ((arg = strsep(&fmtpstr, " \t")) != NULL) fmtp[i++] = atoi(arg);
ctx->frame_size = fmtp[1];
ctx->frame_duration = (ctx->frame_size * 1000) / 44100;
// alac decoder
ctx->alac_codec = alac_init(fmtp);
rc &= ctx->alac_codec != NULL;
buffer_alloc(ctx->audio_buffer, ctx->frame_size*4);
// create rtp ports
for (i = 0; i < 3; i++) {
ctx->rtp_sockets[i].sock = bind_socket(&ctx->rtp_sockets[i].lport, SOCK_DGRAM);
rc &= ctx->rtp_sockets[i].sock > 0;
}
// create http port and start listening
resp.cport = ctx->rtp_sockets[CONTROL].lport;
resp.tport = ctx->rtp_sockets[TIMING].lport;
resp.aport = ctx->rtp_sockets[DATA].lport;
if (rc) {
ctx->running = true;
#ifdef WIN32
pthread_create(&ctx->rtp_thread, NULL, rtp_thread_func, (void *) ctx);
#else
xTaskCreate((TaskFunction_t) rtp_thread_func, "RTP_thread", 4096, ctx, configMAX_PRIORITIES - 3, &ctx->rtp_thread);
#endif
} else {
rtp_end(ctx);
ctx = NULL;
}
resp.ctx = ctx;
return resp;
}
/*---------------------------------------------------------------------------*/
void rtp_end(rtp_t *ctx)
{
int i;
if (!ctx) return;
if (ctx->running) {
ctx->running = false;
#ifdef WIN32
pthread_join(ctx->rtp_thread, NULL);
#else
ctx->joiner = xTaskGetCurrentTaskHandle();
xTaskNotifyWait(0, 0, NULL, portMAX_DELAY);
#endif
}
for (i = 0; i < 3; i++) shutdown_socket(ctx->rtp_sockets[i].sock);
delete_alac(ctx->alac_codec);
buffer_release(ctx->audio_buffer);
free(ctx);
#ifdef __RTP_STORE
fclose(ctx->rtpIN);
fclose(ctx->rtpOUT);
#endif
}
/*---------------------------------------------------------------------------*/
bool rtp_flush(rtp_t *ctx, unsigned short seqno, unsigned int rtptime)
{
bool rc = true;
u32_t now = gettime_ms();
if (now < ctx->record.time + 250 || (ctx->record.seqno == seqno && ctx->record.rtptime == rtptime)) {
rc = false;
LOG_ERROR("[%p]: FLUSH ignored as same as RECORD (%hu - %u)", ctx, seqno, rtptime);
} else {
pthread_mutex_lock(&ctx->ab_mutex);
buffer_reset(ctx->audio_buffer);
ctx->playing = false;
ctx->flush_seqno = seqno;
ctx->synchro.first = false;
pthread_mutex_unlock(&ctx->ab_mutex);
}
LOG_INFO("[%p]: flush %hu %u", ctx, seqno, rtptime);
return rc;
}
/*---------------------------------------------------------------------------*/
void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime)
{
ctx->record.seqno = seqno;
ctx->record.rtptime = rtptime;
ctx->record.time = gettime_ms();
LOG_INFO("[%p]: record %hu %u", ctx, seqno, rtptime);
}
/*---------------------------------------------------------------------------*/
static void buffer_alloc(abuf_t *audio_buffer, int size) {
int i;
for (i = 0; i < BUFFER_FRAMES; i++) {
audio_buffer[i].data = malloc(size);
audio_buffer[i].ready = 0;
}
}
/*---------------------------------------------------------------------------*/
static void buffer_release(abuf_t *audio_buffer) {
int i;
for (i = 0; i < BUFFER_FRAMES; i++) {
free(audio_buffer[i].data);
}
}
/*---------------------------------------------------------------------------*/
static void buffer_reset(abuf_t *audio_buffer) {
int i;
for (i = 0; i < BUFFER_FRAMES; i++) audio_buffer[i].ready = 0;
}
/*---------------------------------------------------------------------------*/
// the sequence numbers will wrap pretty often.
// this returns true if the second arg is after the first
static int seq_order(seq_t a, seq_t b) {
s16_t d = b - a;
return d > 0;
}
/*---------------------------------------------------------------------------*/
static void alac_decode(rtp_t *ctx, s16_t *dest, char *buf, int len, int *outsize) {
unsigned char packet[MAX_PACKET];
unsigned char iv[16];
int aeslen;
assert(len<=MAX_PACKET);
if (ctx->decrypt) {
aeslen = len & ~0xf;
memcpy(iv, ctx->aesiv, sizeof(iv));
#ifdef WIN32
AES_cbc_encrypt((unsigned char*)buf, packet, aeslen, &ctx->aes, iv, AES_DECRYPT);
#else
mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, packet);
#endif
memcpy(packet+aeslen, buf+aeslen, len-aeslen);
decode_frame(ctx->alac_codec, packet, dest, outsize);
} else decode_frame(ctx->alac_codec, (unsigned char*) buf, dest, outsize);
}
/*---------------------------------------------------------------------------*/
static void buffer_put_packet(rtp_t *ctx, seq_t seqno, unsigned rtptime, bool first, char *data, int len) {
abuf_t *abuf = NULL;
pthread_mutex_lock(&ctx->ab_mutex);
if (!ctx->playing) {
if ((ctx->flush_seqno == -1 || seq_order(ctx->flush_seqno, seqno)) &&
((ctx->synchro.required && ctx->synchro.first) || !ctx->synchro.required)) {
ctx->ab_write = seqno-1;
ctx->ab_read = seqno;
ctx->skip = 0;
ctx->flush_seqno = -1;
ctx->playing = true;
ctx->synchro.first = false;
ctx->resent_frames = ctx->silent_frames = 0;
} else {
pthread_mutex_unlock(&ctx->ab_mutex);
return;
}
}
if (seqno == ctx->ab_write+1) {
// expected packet
abuf = ctx->audio_buffer + BUFIDX(seqno);
ctx->ab_write = seqno;
LOG_SDEBUG("packet expected seqno:%hu rtptime:%u (W:%hu R:%hu)", seqno, rtptime, ctx->ab_write, ctx->ab_read);
} else if (seq_order(ctx->ab_write, seqno)) {
// newer than expected
if (seqno - ctx->ab_write - 1 > ctx->latency / ctx->frame_size) {
// only get rtp latency-1 frames back (last one is seqno)
LOG_WARN("[%p] too many missing frames %hu", ctx, seqno - ctx->ab_write - 1);
ctx->ab_write = seqno - ctx->latency / ctx->frame_size;
}
if (seqno - ctx->ab_read + 1 > ctx->latency / ctx->frame_size) {
// if ab_read is lagging more than http latency, advance it
LOG_WARN("[%p] on hold for too long %hu", ctx, seqno - ctx->ab_read + 1);
ctx->ab_read = seqno - ctx->latency / ctx->frame_size + 1;
}
if (rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1)) {
seq_t i;
u32_t now = gettime_ms();
for (i = ctx->ab_write + 1; i <= seqno-1; i++) {
ctx->audio_buffer[BUFIDX(i)].rtptime = rtptime - (seqno-i)*ctx->frame_size;
ctx->audio_buffer[BUFIDX(i)].last_resend = now;
}
}
LOG_DEBUG("[%p]: packet newer seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read);
abuf = ctx->audio_buffer + BUFIDX(seqno);
ctx->ab_write = seqno;
} else if (seq_order(ctx->ab_read, seqno + 1)) {
// recovered packet, not yet sent
abuf = ctx->audio_buffer + BUFIDX(seqno);
LOG_DEBUG("[%p]: packet recovered seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read);
} else {
// too late
LOG_DEBUG("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read);
}
if (ctx->in_frames++ > 1000) {
LOG_INFO("[%p]: fill [level:%hd] [W:%hu R:%hu]", ctx, (seq_t) (ctx->ab_write - ctx->ab_read + 1), ctx->ab_write, ctx->ab_read);
ctx->in_frames = 0;
}
if (abuf) {
alac_decode(ctx, abuf->data, data, len, &abuf->len);
abuf->ready = 1;
// this is the local rtptime when this frame is expected to play
abuf->rtptime = rtptime;
#ifdef __RTP_STORE
fwrite(data, len, 1, ctx->rtpIN);
fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT);
#endif
}
buffer_push_packet(ctx);
pthread_mutex_unlock(&ctx->ab_mutex);
}
/*---------------------------------------------------------------------------*/
// push as many frames as possible through callback
static void buffer_push_packet(rtp_t *ctx) {
abuf_t *curframe = NULL;
u32_t now, playtime;
int i;
// not ready to play yet
if (!ctx->playing || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return;
// maybe re-evaluate time in loop in case data callback blocks ...
now = gettime_ms();
// there is always at least one frame in the buffer
do {
curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read);
playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp)) * 1000) / 44100;
/*
if (now > playtime + ctx->frame_duration) {
//LOG_INFO("[%p]: discarded frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read);
} else if (curframe->ready) {
ctx->callback((const u8_t*) curframe->data, curframe->len);
} else if (now >= playtime) {
LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read);
ctx->callback(silence_frame, ctx->frame_size * 4);
ctx->silent_frames++;
} else break;
*/
if (curframe->ready) {
ctx->callback((const u8_t*) curframe->data, curframe->len);
} else if (now >= playtime) {
LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read);
ctx->callback(silence_frame, ctx->frame_size * 4);
ctx->silent_frames++;
} else break;
ctx->ab_read++;
ctx->out_frames++;
} while (ctx->ab_write - ctx->ab_read + 1 > 0);
if (ctx->out_frames > 1000) {
LOG_INFO("[%p]: drain [level:%hd gap:%d] [W:%hu R:%hu] [R:%u S:%u F:%u]",
ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read,
ctx->resent_frames, ctx->silent_frames, ctx->filled_frames);
ctx->out_frames = 0;
}
LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready);
// each missing packet will be requested up to (latency_frames / 16) times
for (i = 16; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) {
abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
if (!frame->ready && now - frame->last_resend > RESEND_TO) {
rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i);
frame->last_resend = now;
}
}
}
/*---------------------------------------------------------------------------*/
static void *rtp_thread_func(void *arg) {
fd_set fds;
int i, sock = -1;
int count = 0;
bool ntp_sent;
char *packet = malloc(MAX_PACKET);
rtp_t *ctx = (rtp_t*) arg;
for (i = 0; i < 3; i++) {
if (ctx->rtp_sockets[i].sock > sock) sock = ctx->rtp_sockets[i].sock;
// send synchro requets 3 times
ntp_sent = rtp_request_timing(ctx);
}
while (ctx->running) {
ssize_t plen;
char type;
socklen_t rtp_client_len = sizeof(struct sockaddr_storage);
int idx = 0;
char *pktp = packet;
struct timeval timeout = {0, 50*1000};
FD_ZERO(&fds);
for (i = 0; i < 3; i++) { FD_SET(ctx->rtp_sockets[i].sock, &fds); }
if (select(sock + 1, &fds, NULL, NULL, &timeout) <= 0) continue;
for (i = 0; i < 3; i++)
if (FD_ISSET(ctx->rtp_sockets[i].sock, &fds)) idx = i;
plen = recvfrom(ctx->rtp_sockets[idx].sock, packet, MAX_PACKET, 0, (struct sockaddr*) &ctx->rtp_host, &rtp_client_len);
if (!ntp_sent) {
LOG_WARN("[%p]: NTP request not send yet", ctx);
ntp_sent = rtp_request_timing(ctx);
}
if (plen < 0) continue;
assert(plen <= MAX_PACKET);
type = packet[1] & ~0x80;
pktp = packet;
switch (type) {
seq_t seqno;
unsigned rtptime;
// re-sent packet
case 0x56: {
pktp += 4;
plen -= 4;
}
// data packet
case 0x60: {
seqno = ntohs(*(u16_t*)(pktp+2));
rtptime = ntohl(*(u32_t*)(pktp+4));
// adjust pointer and length
pktp += 12;
plen -= 12;
LOG_SDEBUG("[%p]: seqno:%hu rtp:%u (type: %x, first: %u)", ctx, seqno, rtptime, type, packet[1] & 0x80);
// check if packet contains enough content to be reasonable
if (plen < 16) break;
if ((packet[1] & 0x80) && (type != 0x56)) {
LOG_INFO("[%p]: 1st audio packet received", ctx);
}
buffer_put_packet(ctx, seqno, rtptime, packet[1] & 0x80, pktp, plen);
break;
}
// sync packet
case 0x54: {
u32_t rtp_now_latency = ntohl(*(u32_t*)(pktp+4));
u64_t remote = (((u64_t) ntohl(*(u32_t*)(pktp+8))) << 32) + ntohl(*(u32_t*)(pktp+12));
u32_t rtp_now = ntohl(*(u32_t*)(pktp+16));
pthread_mutex_lock(&ctx->ab_mutex);
// re-align timestamp and expected local playback time
if (!ctx->latency) ctx->latency = rtp_now - rtp_now_latency;
ctx->synchro.rtp = rtp_now - ctx->latency;
ctx->synchro.time = ctx->timing.local + (u32_t) NTP2MS(remote - ctx->timing.remote);
// now we are synced on RTP frames
ctx->synchro.status |= RTP_SYNC;
// 1st sync packet received (signals a restart of playback)
if (packet[0] & 0x10) {
ctx->synchro.first = true;
LOG_INFO("[%p]: 1st sync packet received", ctx);
}
pthread_mutex_unlock(&ctx->ab_mutex);
LOG_DEBUG("[%p]: sync packet rtp_latency:%u rtp:%u remote ntp:%Lx, local time %u (now:%u)",
ctx, rtp_now_latency, rtp_now, remote, ctx->synchro.time, gettime_ms());
if (!count--) {
rtp_request_timing(ctx);
count = 3;
}
break;
}
// NTP timing packet
case 0x53: {
u64_t expected;
s64_t delta = 0;
u32_t reference = ntohl(*(u32_t*)(pktp+12)); // only low 32 bits in our case
u64_t remote =(((u64_t) ntohl(*(u32_t*)(pktp+16))) << 32) + ntohl(*(u32_t*)(pktp+20));
u32_t roundtrip = gettime_ms() - reference;
// better discard sync packets when roundtrip is suspicious
if (roundtrip > 100) {
LOG_WARN("[%p]: discarding NTP roundtrip of %u ms", ctx, roundtrip);
break;
}
/*
The expected elapsed remote time should be exactly the same as
elapsed local time between the two request, corrected by the
drifting
*/
expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local);
ctx->timing.remote = remote;
ctx->timing.local = reference;
ctx->timing.count++;
if (!ctx->timing.drift && (ctx->synchro.status & NTP_SYNC)) {
delta = NTP2MS((s64_t) expected - (s64_t) ctx->timing.remote);
ctx->timing.gap_sum += delta;
pthread_mutex_lock(&ctx->ab_mutex);
/*
if expected time is more than remote, then our time is
running faster and we are transmitting frames too quickly,
so we'll run out of frames, need to add one
*/
if (ctx->timing.gap_sum > GAP_THRES && ctx->timing.gap_count++ > GAP_COUNT) {
LOG_INFO("[%p]: Sending packets too fast %Ld [W:%hu R:%hu]", ctx, ctx->timing.gap_sum, ctx->ab_write, ctx->ab_read);
ctx->ab_read--;
ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 1;
ctx->timing.gap_sum -= GAP_THRES;
ctx->timing.gap_adjust -= GAP_THRES;
/*
if expected time is less than remote, then our time is
running slower and we are transmitting frames too slowly,
so we'll overflow frames buffer, need to remove one
*/
} else if (ctx->timing.gap_sum < -GAP_THRES && ctx->timing.gap_count++ > GAP_COUNT) {
if (seq_order(ctx->ab_read, ctx->ab_write + 1)) {
ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 0;
ctx->ab_read++;
} else ctx->skip++;
ctx->timing.gap_sum += GAP_THRES;
ctx->timing.gap_adjust += GAP_THRES;
LOG_INFO("[%p]: Sending packets too slow %Ld (skip: %d) [W:%hu R:%hu]", ctx, ctx->timing.gap_sum, ctx->skip, ctx->ab_write, ctx->ab_read);
}
if (llabs(ctx->timing.gap_sum) < 8) ctx->timing.gap_count = 0;
pthread_mutex_unlock(&ctx->ab_mutex);
}
// now we are synced on NTP (mutex not needed)
ctx->synchro.status |= NTP_SYNC;
LOG_DEBUG("[%p]: Timing references local:%Lu, remote:%Lx (delta:%Ld, sum:%Ld, adjust:%Ld, gaps:%d)",
ctx, ctx->timing.local, ctx->timing.remote, delta, ctx->timing.gap_sum, ctx->timing.gap_adjust, ctx->timing.gap_count);
break;
}
}
}
free(packet);
LOG_INFO("[%p]: terminating", ctx);
#ifndef WIN32
xTaskNotify(ctx->joiner, 0, eNoAction);
vTaskDelete(NULL);
#endif
return NULL;
}
/*---------------------------------------------------------------------------*/
static bool rtp_request_timing(rtp_t *ctx) {
unsigned char req[32];
u32_t now = gettime_ms();
int i;
struct sockaddr_in host;
LOG_DEBUG("[%p]: timing request now:%u (port: %hu)", ctx, now, ctx->rtp_sockets[TIMING].rport);
req[0] = 0x80;
req[1] = 0x52|0x80;
*(u16_t*)(req+2) = htons(7);
*(u32_t*)(req+4) = htonl(0); // dummy
for (i = 0; i < 16; i++) req[i+8] = 0;
*(u32_t*)(req+24) = 0;
*(u32_t*)(req+28) = htonl(now); // this is not a real NTP, but a 32 ms counter in the low part of the NTP
if (ctx->host.s_addr != INADDR_ANY) {
host.sin_family = AF_INET;
host.sin_addr = ctx->host;
} else host = ctx->rtp_host;
// no address from sender, need to wait for 1st packet to be received
if (host.sin_addr.s_addr == INADDR_ANY) return false;
host.sin_port = htons(ctx->rtp_sockets[TIMING].rport);
if (sizeof(req) != sendto(ctx->rtp_sockets[TIMING].sock, req, sizeof(req), 0, (struct sockaddr*) &host, sizeof(host))) {
LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno));
}
return true;
}
/*---------------------------------------------------------------------------*/
static bool rtp_request_resend(rtp_t *ctx, seq_t first, seq_t last) {
unsigned char req[8]; // *not* a standard RTCP NACK
// do not request silly ranges (happens in case of network large blackouts)
if (seq_order(last, first) || last - first > BUFFER_FRAMES / 2) return false;
ctx->resent_frames += last - first + 1;
LOG_DEBUG("resend request [W:%hu R:%hu first=%hu last=%hu]", ctx->ab_write, ctx->ab_read, first, last);
req[0] = 0x80;
req[1] = 0x55|0x80; // Apple 'resend'
*(u16_t*)(req+2) = htons(1); // our seqnum
*(u16_t*)(req+4) = htons(first); // missed seqnum
*(u16_t*)(req+6) = htons(last-first+1); // count
ctx->rtp_host.sin_port = htons(ctx->rtp_sockets[CONTROL].rport);
if (sizeof(req) != sendto(ctx->rtp_sockets[CONTROL].sock, req, sizeof(req), 0, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) {
LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno));
}
return true;
}
#if 0
/*---------------------------------------------------------------------------*/
// get the next frame, when available. return 0 if underrun/stream reset.
static short *_buffer_get_frame(rtp_t *ctx, int *len) {
short buf_fill;
abuf_t *curframe = 0;
int i;
u32_t now, playtime;
if (!ctx->playing) return NULL;
// skip frames if we are running late and skip could not be done in SYNC
while (ctx->skip && seq_order(ctx->ab_read, ctx->ab_write + 1)) {
ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = 0;
ctx->ab_read++;
ctx->skip--;
LOG_INFO("[%p]: Sending packets too slow (skip: %d) [W:%hu R:%hu]", ctx, ctx->skip, ctx->ab_write, ctx->ab_read);
}
buf_fill = ctx->ab_write - ctx->ab_read + 1;
if (buf_fill >= BUFFER_FRAMES) {
LOG_ERROR("[%p]: Buffer overrun %hu", ctx, buf_fill);
ctx->ab_read = ctx->ab_write - (BUFFER_FRAMES - 64);
buf_fill = ctx->ab_write - ctx->ab_read + 1;
}
now = gettime_ms();
curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read);
// use next frame when buffer is empty or silence continues to be sent
if (!buf_fill) curframe->rtptime = ctx->audio_buffer[BUFIDX(ctx->ab_read - 1)].rtptime + ctx->frame_size;
playtime = ctx->synchro.time + (((s32_t)(curframe->rtptime - ctx->synchro.rtp))*1000)/44100;
LOG_SDEBUG("playtime %u %d [W:%hu R:%hu] %d", playtime, playtime - now, ctx->ab_write, ctx->ab_read, curframe->ready);
// wait if not ready but have time, otherwise send silence
if (!buf_fill || ctx->synchro.status != (RTP_SYNC | NTP_SYNC) || (now < playtime && !curframe->ready)) {
LOG_SDEBUG("[%p]: waiting (fill:%hd, W:%hu R:%hu) now:%u, playtime:%u, wait:%d", ctx, buf_fill, ctx->ab_write, ctx->ab_read, now, playtime, playtime - now);
// look for "blocking" frames at the top of the queue and try to catch-up
for (i = 0; i < min(16, buf_fill); i++) {
abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
if (!frame->ready && now - frame->last_resend > RESEND_TO) {
rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i);
frame->last_resend = now;
}
}
return NULL;
}
// when silence is inserted at the top, need to move write pointer
if (!buf_fill) {
if (!ctx->filled_frames) {
LOG_WARN("[%p]: start silence (late %d ms) [W:%hu R:%hu]", ctx, now - playtime, ctx->ab_write, ctx->ab_read);
}
ctx->ab_write++;
ctx->filled_frames++;
} else ctx->filled_frames = 0;
if (!(ctx->out_frames++ & 0x1ff)) {
LOG_INFO("[%p]: drain [level:%hd gap:%d] [W:%hu R:%hu] [R:%u S:%u F:%u]",
ctx, buf_fill-1, playtime - now, ctx->ab_write, ctx->ab_read,
ctx->resent_frames, ctx->silent_frames, ctx->filled_frames);
}
// each missing packet will be requested up to (latency_frames / 16) times
for (i = 16; seq_order(ctx->ab_read + i, ctx->ab_write); i += 16) {
abuf_t *frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
if (!frame->ready && now - frame->last_resend > RESEND_TO) {
rtp_request_resend(ctx, ctx->ab_read + i, ctx->ab_read + i);
frame->last_resend = now;
}
}
if (!curframe->ready) {
LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read);
memset(curframe->data, 0, ctx->frame_size*4);
curframe->len = ctx->frame_size * 4;
ctx->silent_frames++;
} else {
LOG_SDEBUG("[%p]: prepared frame (fill:%hd, W:%hu R:%hu)", ctx, buf_fill-1, ctx->ab_write, ctx->ab_read);
}
*len = curframe->len;
curframe->ready = 0;
ctx->ab_read++;
return curframe->data;
}

21
components/raop/rtp.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef _HAIRTUNES_H_
#define _HAIRTUNES_H_
#include "util.h"
typedef struct {
unsigned short cport, tport, aport;
struct rtp_s *ctx;
} rtp_resp_t;
typedef void (*rtp_data_cb_t)(const u8_t *data, size_t len);
rtp_resp_t rtp_init(struct in_addr host, bool sync, bool drift, bool range, int latency,
char *aeskey, char *aesiv, char *fmtpstr,
short unsigned pCtrlPort, short unsigned pTimingPort, rtp_data_cb_t data_cb);
void rtp_end(struct rtp_s *ctx);
bool rtp_flush(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime);
void rtp_record(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime);
void rtp_metadata(struct rtp_s *ctx, struct metadata_s *metadata);
#endif

601
components/raop/util.c Normal file
View File

@@ -0,0 +1,601 @@
/*
* AirConnect: Chromecast & UPnP to AirPlay
*
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "platform.h"
#ifdef WIN32
#include <iphlpapi.h>
#else
/*
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <netdb.h>
*/
#include <ctype.h>
#endif
#include <stdarg.h>
#include "pthread.h"
#include "util.h"
#include "log_util.h"
/*----------------------------------------------------------------------------*/
/* globals */
/*----------------------------------------------------------------------------*/
extern log_level util_loglevel;
/*----------------------------------------------------------------------------*/
/* locals */
/*----------------------------------------------------------------------------*/
static log_level *loglevel = &util_loglevel;
static char *ltrim(char *s);
static int read_line(int fd, char *line, int maxlen, int timeout);
/*----------------------------------------------------------------------------*/
/* */
/* NETWORKING utils */
/* */
/*----------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
#define MAX_INTERFACES 256
#define DEFAULT_INTERFACE 1
#if !defined(WIN32)
#define INVALID_SOCKET (-1)
#endif
in_addr_t get_localhost(char **name)
{
#ifdef WIN32
char buf[256];
struct hostent *h = NULL;
struct sockaddr_in LocalAddr;
memset(&LocalAddr, 0, sizeof(LocalAddr));
gethostname(buf, 256);
h = gethostbyname(buf);
if (name) *name = strdup(buf);
if (h != NULL) {
memcpy(&LocalAddr.sin_addr, h->h_addr_list[0], 4);
return LocalAddr.sin_addr.s_addr;
}
else return INADDR_ANY;
#else
// missing platform here ...
return INADDR_ANY;
#endif
}
/*----------------------------------------------------------------------------*/
#ifdef WIN32
void winsock_init(void) {
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(2, 2);
int WSerr = WSAStartup(wVersionRequested, &wsaData);
if (WSerr != 0) {
LOG_ERROR("Bad winsock version", NULL);
exit(1);
}
}
/*----------------------------------------------------------------------------*/
void winsock_close(void) {
WSACleanup();
}
#endif
/*----------------------------------------------------------------------------*/
int shutdown_socket(int sd)
{
if (sd <= 0) return -1;
#ifdef WIN32
shutdown(sd, SD_BOTH);
#else
shutdown(sd, SHUT_RDWR);
#endif
LOG_DEBUG("closed socket %d", sd);
return closesocket(sd);
}
/*----------------------------------------------------------------------------*/
int bind_socket(unsigned short *port, int mode)
{
int sock;
socklen_t len = sizeof(struct sockaddr);
struct sockaddr_in addr;
if ((sock = socket(AF_INET, mode, 0)) < 0) {
LOG_ERROR("cannot create socket %d", sock);
return sock;
}
/* Populate socket address structure */
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(*port);
#ifdef SIN_LEN
si.sin_len = sizeof(si);
#endif
if (bind(sock, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
closesocket(sock);
LOG_ERROR("cannot bind socket %d", sock);
return -1;
}
if (!*port) {
getsockname(sock, (struct sockaddr *) &addr, &len);
*port = ntohs(addr.sin_port);
}
LOG_DEBUG("socket binding %d on port %d", sock, *port);
return sock;
}
/*----------------------------------------------------------------------------*/
int conn_socket(unsigned short port)
{
struct sockaddr_in addr;
int sd;
sd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons(port);
if (sd < 0 || connect(sd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
close(sd);
return -1;
}
LOG_DEBUG("created socket %d", sd);
return sd;
}
/*----------------------------------------------------------------------------*/
/* */
/* SYSTEM utils */
/* */
/*----------------------------------------------------------------------------*/
#ifdef WIN32
/*----------------------------------------------------------------------------*/
void *dlopen(const char *filename, int flag) {
SetLastError(0);
return LoadLibrary((LPCTSTR)filename);
}
/*----------------------------------------------------------------------------*/
void *dlsym(void *handle, const char *symbol) {
SetLastError(0);
return (void *)GetProcAddress(handle, symbol);
}
/*----------------------------------------------------------------------------*/
char *dlerror(void) {
static char ret[32];
int last = GetLastError();
if (last) {
sprintf(ret, "code: %i", last);
SetLastError(0);
return ret;
}
return NULL;
}
#endif
/*----------------------------------------------------------------------------*/
/* */
/* STDLIB extensions */
/* */
/*----------------------------------------------------------------------------*/
#ifdef WIN32
/*---------------------------------------------------------------------------*/
char *strcasestr(const char *haystack, const char *needle) {
size_t length_needle;
size_t length_haystack;
size_t i;
if (!haystack || !needle)
return NULL;
length_needle = strlen(needle);
length_haystack = strlen(haystack);
if (length_haystack < length_needle) return NULL;
length_haystack -= length_needle - 1;
for (i = 0; i < length_haystack; i++)
{
size_t j;
for (j = 0; j < length_needle; j++)
{
unsigned char c1;
unsigned char c2;
c1 = haystack[i+j];
c2 = needle[j];
if (toupper(c1) != toupper(c2))
goto next;
}
return (char *) haystack + i;
next:
;
}
return NULL;
}
/*---------------------------------------------------------------------------*/
char* strsep(char** stringp, const char* delim)
{
char* start = *stringp;
char* p;
p = (start != NULL) ? strpbrk(start, delim) : NULL;
if (p == NULL) {
*stringp = NULL;
} else {
*p = '\0';
*stringp = p + 1;
}
return start;
}
/*---------------------------------------------------------------------------*/
char *strndup(const char *s, size_t n) {
char *p = malloc(n + 1);
strncpy(p, s, n);
p[n] = '\0';
return p;
}
#endif
/*----------------------------------------------------------------------------*/
char* strextract(char *s1, char *beg, char *end)
{
char *p1, *p2, *res;
p1 = strcasestr(s1, beg);
if (!p1) return NULL;
p1 += strlen(beg);
p2 = strcasestr(p1, end);
if (!p2) return strdup(p1);
res = malloc(p2 - p1 + 1);
memcpy(res, p1, p2 - p1);
res[p2 - p1] = '\0';
return res;
}
#ifdef WIN32
/*----------------------------------------------------------------------------*/
int asprintf(char **strp, const char *fmt, ...)
{
va_list args, cp;
int len, ret = 0;
va_start(args, fmt);
len = vsnprintf(NULL, 0, fmt, args);
*strp = malloc(len + 1);
if (*strp) ret = vsprintf(*strp, fmt, args);
va_end(args);
return ret;
}
#endif
/*---------------------------------------------------------------------------*/
static char *ltrim(char *s)
{
while(isspace((int) *s)) s++;
return s;
}
/*----------------------------------------------------------------------------*/
/* */
/* HTTP management */
/* */
/*----------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------*/
bool http_parse(int sock, char *method, key_data_t *rkd, char **body, int *len)
{
char line[256], *dp;
unsigned j;
int i, timeout = 100;
rkd[0].key = NULL;
if ((i = read_line(sock, line, sizeof(line), timeout)) <= 0) {
if (i < 0) {
LOG_ERROR("cannot read method", NULL);
}
return false;
}
if (!sscanf(line, "%s", method)) {
LOG_ERROR("missing method", NULL);
return false;
}
i = *len = 0;
while (read_line(sock, line, sizeof(line), timeout) > 0) {
LOG_SDEBUG("sock: %u, received %s", line);
// line folding should be deprecated
if (i && rkd[i].key && (line[0] == ' ' || line[0] == '\t')) {
for(j = 0; j < strlen(line); j++) if (line[j] != ' ' && line[j] != '\t') break;
rkd[i].data = realloc(rkd[i].data, strlen(rkd[i].data) + strlen(line + j) + 1);
strcat(rkd[i].data, line + j);
continue;
}
dp = strstr(line,":");
if (!dp){
LOG_ERROR("Request failed, bad header", NULL);
kd_free(rkd);
return false;
}
*dp = 0;
rkd[i].key = strdup(line);
rkd[i].data = strdup(ltrim(dp + 1));
if (!strcasecmp(rkd[i].key, "Content-Length")) *len = atol(rkd[i].data);
i++;
rkd[i].key = NULL;
}
if (*len) {
int size = 0;
*body = malloc(*len + 1);
while (*body && size < *len) {
int bytes = recv(sock, *body + size, *len - size, 0);
if (bytes <= 0) break;
size += bytes;
}
(*body)[*len] = '\0';
if (!*body || size != *len) {
LOG_ERROR("content length receive error %d %d", *len, size);
}
}
return true;
}
/*----------------------------------------------------------------------------*/
static int read_line(int fd, char *line, int maxlen, int timeout)
{
int i,rval;
int count=0;
struct pollfd pfds;
char ch;
*line = 0;
pfds.fd = fd;
pfds.events = POLLIN;
for(i = 0; i < maxlen; i++){
if (poll(&pfds, 1, timeout)) rval=recv(fd, &ch, 1, 0);
else return 0;
if (rval == -1) {
if (errno == EAGAIN) return 0;
LOG_ERROR("fd: %d read error: %s", fd, strerror(errno));
return -1;
}
if (rval == 0) {
LOG_INFO("disconnected on the other end %u", fd);
return 0;
}
if (ch == '\n') {
*line=0;
return count;
}
if (ch=='\r') continue;
*line++=ch;
count++;
if (count >= maxlen-1) break;
}
*line = 0;
return count;
}
/*----------------------------------------------------------------------------*/
char *http_send(int sock, char *method, key_data_t *rkd)
{
unsigned sent, len;
char *resp = kd_dump(rkd);
char *data = malloc(strlen(method) + 2 + strlen(resp) + 2 + 1);
len = sprintf(data, "%s\r\n%s\r\n", method, resp);
NFREE(resp);
sent = send(sock, data, len, 0);
if (sent != len) {
LOG_ERROR("HTTP send() error:%s %u (strlen=%u)", data, sent, len);
NFREE(data);
}
return data;
}
/*----------------------------------------------------------------------------*/
char *kd_lookup(key_data_t *kd, char *key)
{
int i = 0;
while (kd && kd[i].key){
if (!strcasecmp(kd[i].key, key)) return kd[i].data;
i++;
}
return NULL;
}
/*----------------------------------------------------------------------------*/
bool kd_add(key_data_t *kd, char *key, char *data)
{
int i = 0;
while (kd && kd[i].key) i++;
kd[i].key = strdup(key);
kd[i].data = strdup(data);
kd[i+1].key = NULL;
return NULL;
}
/*----------------------------------------------------------------------------*/
void kd_free(key_data_t *kd)
{
int i = 0;
while (kd && kd[i].key){
free(kd[i].key);
if (kd[i].data) free(kd[i].data);
i++;
}
kd[0].key = NULL;
}
/*----------------------------------------------------------------------------*/
char *kd_dump(key_data_t *kd)
{
int i = 0;
int pos = 0, size = 0;
char *str = NULL;
if (!kd || !kd[0].key) return strdup("\r\n");
while (kd && kd[i].key) {
char *buf;
int len;
len = asprintf(&buf, "%s: %s\r\n", kd[i].key, kd[i].data);
while (pos + len >= size) {
void *p = realloc(str, size + 1024);
size += 1024;
if (!p) {
free(str);
return NULL;
}
str = p;
}
memcpy(str + pos, buf, len);
pos += len;
free(buf);
i++;
}
str[pos] = '\0';
return str;
}
/*--------------------------------------------------------------------------*/
void free_metadata(struct metadata_s *metadata)
{
NFREE(metadata->artist);
NFREE(metadata->album);
NFREE(metadata->title);
NFREE(metadata->genre);
NFREE(metadata->path);
NFREE(metadata->artwork);
NFREE(metadata->remote_title);
}
/*----------------------------------------------------------------------------*/
int _fprintf(FILE *file, ...)
{
va_list args;
char *fmt;
int n;
va_start(args, file);
fmt = va_arg(args, char*);
n = vfprintf(file, fmt, args);
va_end(args);
return n;
}

85
components/raop/util.h Normal file
View File

@@ -0,0 +1,85 @@
/*
* Misc utilities
*
* (c) Adrian Smith 2012-2014, triode1@btinternet.com
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef __UTIL_H
#define __UTIL_H
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "platform.h"
#include "pthread.h"
#define NFREE(p) if (p) { free(p); p = NULL; }
typedef struct metadata_s {
char *artist;
char *album;
char *title;
char *genre;
char *path;
char *artwork;
char *remote_title;
u32_t track;
u32_t duration;
u32_t track_hash;
u32_t sample_rate;
u8_t sample_size;
u8_t channels;
} metadata_t;
/*
void free_metadata(struct metadata_s *metadata);
void dup_metadata(struct metadata_s *dst, struct metadata_s *src);
*/
u32_t gettime_ms(void);
#ifdef WIN32
char* strsep(char** stringp, const char* delim);
char *strndup(const char *s, size_t n);
int asprintf(char **strp, const char *fmt, ...);
void winsock_init(void);
void winsock_close(void);
#else
char *strlwr(char *str);
#endif
char* strextract(char *s1, char *beg, char *end);
in_addr_t get_localhost(char **name);
void get_mac(u8_t mac[]);
int shutdown_socket(int sd);
int bind_socket(short unsigned *port, int mode);
int conn_socket(unsigned short port);
typedef struct {
char *key;
char *data;
} key_data_t;
bool http_parse(int sock, char *method, key_data_t *rkd, char **body, int *len);
char* http_send(int sock, char *method, key_data_t *rkd);
char* kd_lookup(key_data_t *kd, char *key);
bool kd_add(key_data_t *kd, char *key, char *value);

View File

@@ -14,7 +14,7 @@ CFLAGS += -O3 -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE16 -DEMBEDDED -DTREMOR_ON
-I$(COMPONENT_PATH)/../codecs/inc/opus \
-I$(COMPONENT_PATH)/../codecs/inc/opusfile \
-I$(COMPONENT_PATH)/../driver_bt \
-I$(COMPONENT_PATH)/../airplay
-I$(COMPONENT_PATH)/../raop
# -I$(COMPONENT_PATH)/../codecs/inc/faad2

View File

@@ -21,7 +21,7 @@
#include "squeezelite.h"
#include "bt_app_sink.h"
#include "airplay_sink.h"
#include "raop_sink.h"
#define LOCK_O mutex_lock(outputbuf->mutex)
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
@@ -34,16 +34,19 @@ extern struct buffer *outputbuf;
// this is the only system-wide loglevel variable
extern log_level loglevel;
static raop_event_t raop_state;
static bool raop_expect_stop = false;
/****************************************************************************************
* BT sink data handler
* Common sink data handler
*/
static void bt_sink_data_handler(const uint8_t *data, uint32_t len)
static void sink_data_handler(const uint8_t *data, uint32_t len)
{
size_t bytes;
size_t bytes, space;
// would be better to lock decoder, but really, it does not matter
if (decode.state != DECODE_STOPPED) {
LOG_SDEBUG("Cannot use BT sink while LMS is controlling player");
LOG_SDEBUG("Cannot use external sink while LMS is controlling player");
return;
}
@@ -63,13 +66,15 @@ static void bt_sink_data_handler(const uint8_t *data, uint32_t len)
}
#endif
_buf_inc_writep(outputbuf, bytes);
space = _buf_space(outputbuf);
len -= bytes;
data += bytes;
UNLOCK_O;
// allow i2s to empty the buffer if needed
if (len) usleep(50000);
if (len && !space) usleep(50000);
}
}
@@ -81,6 +86,7 @@ static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...)
va_list args;
LOCK_D;
if (decode.state != DECODE_STOPPED) {
LOG_WARN("Cannot use BT sink while LMS is controlling player");
UNLOCK_D;
@@ -128,14 +134,78 @@ static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...)
va_end(args);
}
/****************************************************************************************
* AirPlay sink command handler
*/
void raop_sink_cmd_handler(raop_event_t event, void *param)
{
LOCK_D;
if (decode.state != DECODE_STOPPED) {
LOG_WARN("Cannot use Airplay sink while LMS is controlling player");
UNLOCK_D;
return;
}
if (event != RAOP_VOLUME) LOCK_O;
// this is async, so player might have been deleted
switch (event) {
case RAOP_STREAM:
// a PLAY will come later, so we'll do the load at that time
LOG_INFO("Stream", NULL);
raop_state = event;
output.external = true;
output.current_sample_rate = 44100;
output.state = OUTPUT_BUFFER;
output.threshold = 5;
break;
case RAOP_STOP:
LOG_INFO("Stop", NULL);
output.external = false;
output.state = OUTPUT_OFF;
raop_state = event;
break;
case RAOP_FLUSH:
LOG_INFO("Flush", NULL);
raop_expect_stop = true;
raop_state = event;
output.state = OUTPUT_STOPPED;
break;
case RAOP_PLAY: {
LOG_INFO("Play", NULL);
// this where we need the OUTPUT_START_AT
if (raop_state != RAOP_PLAY) {
output.external = true;
output.state = OUTPUT_RUNNING;
}
raop_state = event;
break;
}
case RAOP_VOLUME: {
float volume = *((float*) param);
LOG_INFO("Volume[0..1] %0.4f", volume);
volume *= 65536;
set_volume((u16_t) volume, (u16_t) volume);
break;
}
default:
break;
}
if (event != RAOP_VOLUME) UNLOCK_O;
UNLOCK_D;
}
/****************************************************************************************
* We provide the generic codec register option
*/
void register_other(void) {
#ifdef CONFIG_BT_SINK
if (!strcasestr(output.device, "BT ")) {
bt_sink_init(bt_sink_cmd_handler, bt_sink_data_handler);
bt_sink_init(bt_sink_cmd_handler, sink_data_handler);
LOG_INFO("Initializing BT sink");
} else {
LOG_WARN("Cannot be a BT sink and source");
@@ -143,7 +213,7 @@ void register_other(void) {
#endif
#ifdef CONFIG_AIRPLAY_SINK
if (!strcasestr(output.device, "BT ")) {
airplay_sink_init();
raop_sink_init(raop_sink_cmd_handler, sink_data_handler);
LOG_INFO("Initializing AirPlay sink");
} else {
LOG_WARN("Cannot be an AirPlay sink and BT source");