applied platformio structure

This commit is contained in:
2026-03-13 17:03:22 +00:00
parent c5233cf15c
commit db7d90e736
3510 changed files with 691878 additions and 0 deletions

10
lib/raop/CMakeLists.txt Normal file
View File

@@ -0,0 +1,10 @@
idf_component_register(SRC_DIRS .
INCLUDE_DIRS .
PRIV_REQUIRES newlib freertos pthread platform_config mdns services codecs tools display wifi-manager
)
set_source_files_properties(raop.c
PROPERTIES COMPILE_FLAGS
-Wno-misleading-indentation
)

15
lib/raop/component.mk Normal file
View File

@@ -0,0 +1,15 @@
#
# Component Makefile
#
# This Makefile should, at the very least, just include $(SDK_PATH)/Makefile. By default,
# this will take the sources in the src/ directory, compile them and link them into
# lib(subdirectory_name).a in the build directory. This behaviour is entirely configurable,
# please read the SDK documents if you need to do this.
#
CFLAGS += -fstack-usage\
-I$(PROJECT_PATH)/components/tools \
-I$(PROJECT_PATH)/components/codecs/inc/alac \
-I$(PROJECT_PATH)/main/
COMPONENT_ADD_INCLUDEDIRS := .
COMPONENT_SRCDIRS := .

552
lib/raop/dmap_parser.c Normal file
View File

@@ -0,0 +1,552 @@
#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[] = {
#ifdef DMAP_FULL
{ "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" },
#endif
{ "asal", DMAP_STR, 0, "daap.songalbum" },
{ "asar", DMAP_STR, 0, "daap.songartist" },
#ifdef DMAP_FULL
{ "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" },
#endif
{ "minm", DMAP_STR, 0, "dmap.itemname" },
#ifdef DMAP_FULL
{ "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" }
#endif
};
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;
}
}
#ifdef DMAP_FULL
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;
}
#endif
}
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);
}

90
lib/raop/dmap_parser.h Normal file
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

40
lib/raop/log_util.h Normal file
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

117
lib/raop/platform.h Normal file
View File

@@ -0,0 +1,117 @@
/*
* platform setting definition
*
* (c) Philippe, philippe_44@outlook.com
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*
*/
#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_

975
lib/raop/raop.c Normal file
View File

@@ -0,0 +1,975 @@
/*
*
* (c) Philippe 2019, philippe_44@outlook.com
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*
*/
#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"
#define RTSP_STACK_SIZE (8*1024)
#define SEARCH_STACK_SIZE (3*1024)
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;
#ifdef WIN32
pthread_t thread;
#else
TaskHandle_t thread, joiner;
StaticTask_t *xTaskBuffer;
StackType_t xStack[RTSP_STACK_SIZE] __attribute__ ((aligned (4)));
#endif
/*
Compiler/Execution bug: if this bool is next to 'running', the rtsp_thread
loop sees 'running' being set to false from at first execution ...
*/
bool abort;
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;
bool running;
#ifdef WIN32
struct mDNShandle_s *handle;
pthread_t thread;
#else
TaskHandle_t thread;
StaticTask_t *xTaskBuffer;
StackType_t xStack[SEARCH_STACK_SIZE] __attribute__ ((aligned (4)));;
SemaphoreHandle_t destroy_mutex;
#endif
} active_remote;
void *owner;
} raop_ctx_t;
extern struct mdnsd* glmDNSServer;
extern log_level raop_loglevel;
static log_level *loglevel = &raop_loglevel;
#ifdef WIN32
static void* rtsp_thread(void *arg);
static void* search_remote(void *args);
#else
static void rtsp_thread(void *arg);
static void search_remote(void *args);
#endif
static void cleanup_rtsp(raop_ctx_t *ctx, bool abort);
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);
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(uint32_t 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=airesp32", "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
const mdns_txt_item_t txt[] = {
{"am", "airesp32"},
{"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.s_addr = host;
ctx->sock = socket(AF_INET, SOCK_STREAM, 0);
ctx->cmd_cb = cmd_cb;
ctx->data_cb = data_cb;
ctx->latency = min(latency, 88200);
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;
addr.sin_family = AF_INET;
#ifdef WIN32
ctx->port = 0;
addr.sin_port = htons(ctx->port);
#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));
closesocket(ctx->sock);
free(ctx);
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);
mdns_service_add(id, "_raop", "_tcp", ctx->port, (mdns_txt_item_t*) txt, sizeof(txt) / sizeof(mdns_txt_item_t));
ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
ctx->thread = xTaskCreateStaticPinnedToCore( (TaskFunction_t) rtsp_thread, "RTSP", RTSP_STACK_SIZE, ctx,
ESP_TASK_PRIO_MIN + 2, ctx->xStack, ctx->xTaskBuffer, CONFIG_PTHREAD_TASK_CORE_DEFAULT);
#endif
return ctx;
}
/*----------------------------------------------------------------------------*/
void raop_abort(struct raop_ctx_s *ctx) {
LOG_INFO("[%p]: aborting RTSP session at next select() wakeup", ctx);
ctx->abort = true;
}
/*----------------------------------------------------------------------------*/
void raop_delete(struct raop_ctx_s *ctx) {
#ifdef WIN32
int sock;
struct sockaddr addr;
socklen_t nlen = sizeof(struct sockaddr);
#endif
if (!ctx) return;
#ifdef WIN32
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);
pthread_join(ctx->thread, NULL);
rtp_end(ctx->rtp);
shutdown(ctx->sock, SD_BOTH);
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->active_remote.thread, NULL);
}
// stop broadcasting devices
mdns_service_remove(ctx->svr, ctx->svc);
mdnsd_stop(ctx->svr);
#else
// then the RTSP task
ctx->joiner = xTaskGetCurrentTaskHandle();
ctx->running = false;
// brute-force exit of accept()
shutdown(ctx->sock, SHUT_RDWR);
closesocket(ctx->sock);
// wait to make sure LWIP if scheduled (avoid issue with NotifyTake)
vTaskDelay(100 / portTICK_PERIOD_MS);
ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
vTaskDelete(ctx->thread);
SAFE_PTR_FREE(ctx->xTaskBuffer);
// cleanup all session-created items
cleanup_rtsp(ctx, true);
mdns_service_remove("_raop", "_tcp");
#endif
NFREE(ctx->rtsp.aeskey);
NFREE(ctx->rtsp.aesiv);
NFREE(ctx->rtsp.fmtp);
free(ctx);
}
/*----------------------------------------------------------------------------*/
bool raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param) {
struct sockaddr_in addr;
int sock;
char *command = NULL;
bool success = false;
// first notify the remote controller (if any)
switch(event) {
case RAOP_REW:
command = strdup("beginrew");
break;
case RAOP_FWD:
command = strdup("beginff");
break;
case RAOP_PREV:
command = strdup("previtem");
break;
case RAOP_NEXT:
command = strdup("nextitem");
break;
case RAOP_TOGGLE:
command = strdup("playpause");
break;
case RAOP_PAUSE:
command = strdup("pause");
break;
case RAOP_PLAY:
command = strdup("play");
break;
case RAOP_RESUME:
command = strdup("playresume");
break;
case RAOP_STOP:
command = strdup("stop");
break;
case RAOP_VOLUME_UP:
command = strdup("volumeup");
break;
case RAOP_VOLUME_DOWN:
command = strdup("volumedown");
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 success;
}
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);
success = true;
} else {
LOG_INFO("[%p]: can't connect to remote for %s", ctx, command);
}
free(command);
closesocket(sock);
return success;
}
/*----------------------------------------------------------------------------*/
#ifdef WIN32
static void *rtsp_thread(void *arg) {
#else
static void rtsp_thread(void *arg) {
#endif
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;
ctx->abort = false;
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 && !ctx->abort) continue;
if (n > 0) res = handle_rtsp(ctx, sock);
if (n < 0 || !res || ctx->abort) {
cleanup_rtsp(ctx, true);
closesocket(sock);
LOG_INFO("RTSP close %u", sock);
sock = -1;
}
}
if (sock != -1) closesocket(sock);
#ifndef WIN32
xTaskNotifyGive(ctx->joiner);
vTaskSuspend(NULL);
#else
return NULL;
#endif
}
/*----------------------------------------------------------------------------*/
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);
// try to re-acquire IP address if we were missing it
if (S_ADDR(ctx->host) == INADDR_ANY) {
S_ADDR(ctx->host) = get_localhost(NULL);
LOG_INFO("[%p]: IP was missing, trying to get it %s", ctx, inet_ntoa(ctx->host));
}
// 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);
#ifdef WIN32
ctx->active_remote.handle = init_mDNS(false, ctx->host);
pthread_create(&ctx->active_remote.thread, NULL, &search_remote, ctx);
#else
ctx->active_remote.running = true;
ctx->active_remote.destroy_mutex = xSemaphoreCreateBinary();
ctx->active_remote.xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
ctx->active_remote.thread = xTaskCreateStaticPinnedToCore( (TaskFunction_t) search_remote, "search_remote", SEARCH_STACK_SIZE, ctx,
ESP_TASK_PRIO_MIN + 2, ctx->active_remote.xStack, ctx->active_remote.xTaskBuffer,
CONFIG_PTHREAD_TASK_CORE_DEFAULT );
#endif
} else if (!strcmp(method, "SETUP") && ((buf = kd_lookup(headers, "Transport")) != NULL)) {
char *p;
rtp_resp_t rtp = { 0 };
short unsigned tport = 0, cport = 0;
uint8_t *buffer = NULL;
size_t size = 0;
// we are about to stream, do something if needed and optionally give buffers to play with
success = ctx->cmd_cb(RAOP_SETUP, &buffer, &size);
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, ctx->latency, ctx->rtsp.aeskey, ctx->rtsp.aesiv,
ctx->rtsp.fmtp, cport, tport, buffer, size, ctx->cmd_cb, ctx->data_cb);
ctx->rtp = rtp.ctx;
if ( (cport * tport * rtp.cport * rtp.tport * rtp.aport) != 0 && 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 (buf && (p = strcasestr(buf, "seq")) != NULL) sscanf(p, "%*[^=]=%hu", &seqno);
if (buf && (p = strcasestr(buf, "rtptime")) != NULL) sscanf(p, "%*[^=]=%u", &rtptime);
if (ctx->rtp) rtp_record(ctx->rtp, seqno, rtptime);
success = ctx->cmd_cb(RAOP_STREAM);
} 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, true)) {
success = ctx->cmd_cb(RAOP_FLUSH);
rtp_flush_release(ctx->rtp);
}
} else if (!strcmp(method, "TEARDOWN")) {
cleanup_rtsp(ctx, false);
success = ctx->cmd_cb(RAOP_STOP);
} else if (!strcmp(method, "SET_PARAMETER")) {
char *p;
if (body && (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);
success = ctx->cmd_cb(RAOP_VOLUME, volume);
} else if (body && (p = strcasestr(body, "progress")) != NULL) {
int start, current, stop = 0;
// we want ms, not s
sscanf(p, "%*[^:]:%u/%u/%u", &start, &current, &stop);
current = ((current - start) / 44100) * 1000;
if (stop) stop = ((stop - start) / 44100) * 1000;
LOG_INFO("[%p]: SET PARAMETER progress %d/%u %s", ctx, current, stop, p);
success = ctx->cmd_cb(RAOP_PROGRESS, max(current, 0), stop);
} else if (body && ((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)) {
uint32_t timestamp = 0;
if ((p = kd_lookup(headers, "RTP-Info")) != NULL) sscanf(p, "%*[^=]=%d", &timestamp);
LOG_INFO("[%p]: received metadata (ts: %d)\n\tartist: %s\n\talbum: %s\n\ttitle: %s",
ctx, timestamp, metadata.artist ? metadata.artist : "", metadata.album ? metadata.album : "",
metadata.title ? metadata.title : "");
success = ctx->cmd_cb(RAOP_METADATA, metadata.artist, metadata.album, metadata.title, timestamp);
free_metadata(&metadata);
}
} else if (body && ((p = kd_lookup(headers, "Content-Type")) != NULL) && strcasestr(p, "image/jpeg")) {
uint32_t timestamp = 0;
if ((p = kd_lookup(headers, "RTP-Info")) != NULL) sscanf(p, "%*[^=]=%d", &timestamp);
LOG_INFO("[%p]: received JPEG image of %d bytes (ts:%d)", ctx, len, timestamp);
ctx->cmd_cb(RAOP_ARTWORK, body, len, timestamp);
} else {
char *dump = kd_dump(headers);
LOG_INFO("Unhandled SET PARAMETER\n%s", dump);
free(dump);
}
}
// 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 503 ERROR", NULL);
closesocket(sock);
}
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;
}
/*----------------------------------------------------------------------------*/
void cleanup_rtsp(raop_ctx_t *ctx, bool abort) {
// first stop RTP process
if (ctx->rtp) {
rtp_end(ctx->rtp);
ctx->rtp = NULL;
if (abort) LOG_INFO("[%p]: RTP thread aborted", ctx);
}
if (ctx->active_remote.running) {
#ifdef WIN32
pthread_join(ctx->active_remote.thread, NULL);
close_mDNS(ctx->active_remote.handle);
#else
// need to make sure no search is on-going and reclaim task memory
ctx->active_remote.running = false;
xSemaphoreTake(ctx->active_remote.destroy_mutex, portMAX_DELAY);
vTaskDelete(ctx->active_remote.thread);
SAFE_PTR_FREE(ctx->active_remote.xTaskBuffer);
vSemaphoreDelete(ctx->active_remote.destroy_mutex);
#endif
memset(&ctx->active_remote, 0, sizeof(ctx->active_remote));
LOG_INFO("[%p]: Remote search thread aborted", ctx);
}
NFREE(ctx->rtsp.aeskey);
NFREE(ctx->rtsp.aesiv);
NFREE(ctx->rtsp.fmtp);
}
/*----------------------------------------------------------------------------*/
#ifdef WIN32
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;
}
#else
/*----------------------------------------------------------------------------*/
static void search_remote(void *args) {
raop_ctx_t *ctx = (raop_ctx_t*) args;
bool found = false;
LOG_INFO("starting remote search");
while (ctx->active_remote.running && !found) {
mdns_result_t *results = NULL;
mdns_result_t *r;
mdns_ip_addr_t *a;
if (mdns_query_ptr("_dacp", "_tcp", 3000, 32, &results)) {
LOG_ERROR("mDNS active remote query Failed");
continue;
}
for (r = results; r && !strcasestr(r->instance_name, ctx->active_remote.DACPid); r = r->next);
if (r) {
for (a = r->addr; a && a->addr.type != IPADDR_TYPE_V4; a = a->next);
if (a) {
found = true;
ctx->active_remote.host.s_addr = a->addr.u_addr.ip4.addr;
ctx->active_remote.port = r->port;
LOG_INFO("found remote %s %s:%hu", r->instance_name, inet_ntoa(ctx->active_remote.host), ctx->active_remote.port);
}
}
mdns_query_results_free(results);
}
// can't use xNotifyGive as it seems LWIP is using it as well
xSemaphoreGive(ctx->active_remote.destroy_mutex);
vTaskSuspend(NULL);
}
#endif
/*----------------------------------------------------------------------------*/
static char *rsa_apply(unsigned char *input, int inlen, int *outlen, int mode)
{
const 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
const 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)
{
const 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;
q = data;
for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) {
unsigned int val = token_decode(p);
unsigned int marker = (val >> 24) & 0xff;
if (val == DECODE_ERROR)
return -1;
*q++ = (val >> 16) & 0xff;
if (marker < 2)
*q++ = (val >> 8) & 0xff;
if (marker < 1)
*q++ = val & 0xff;
}
return q - (unsigned char *) data;
}
/*----------------------------------------------------------------------------*/
static void on_dmap_string(void *ctx, const char *code, const char *name, const char *buf, size_t len) {

19
lib/raop/raop.h Normal file
View File

@@ -0,0 +1,19 @@
/*
* (c) Philippe 2020, philippe_44@outlook.com
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*
*/
#pragma once
#include "platform.h"
#include "raop_sink.h"
struct raop_ctx_s* raop_create(uint32_t 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_abort(struct raop_ctx_s *ctx);
bool raop_cmd(struct raop_ctx_s *ctx, raop_event_t event, void *param);

202
lib/raop/raop_sink.c Normal file
View File

@@ -0,0 +1,202 @@
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include "nvs.h"
#include "esp_netif.h"
#include "esp_log.h"
#include "esp_console.h"
#include "esp_pthread.h"
#include "esp_system.h"
#include "freertos/timers.h"
#include "platform_config.h"
#include "raop.h"
#include "audio_controls.h"
#include "display.h"
#include "accessors.h"
#include "log_util.h"
#include "network_services.h"
#ifndef CONFIG_AIRPLAY_NAME
#define CONFIG_AIRPLAY_NAME "ESP32-AirPlay"
#endif
static EXT_RAM_ATTR struct raop_cb_s {
raop_cmd_vcb_t cmd;
raop_data_cb_t data;
} raop_cbs;
log_level raop_loglevel = lINFO;
log_level util_loglevel;
static log_level *loglevel = &raop_loglevel;
static struct raop_ctx_s *raop;
static raop_cmd_vcb_t cmd_handler_chain;
static void raop_volume_up(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_VOLUME_UP, NULL);
LOG_INFO("AirPlay volume up");
}
static void raop_volume_down(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_VOLUME_DOWN, NULL);
LOG_INFO("AirPlay volume down");
}
static void raop_toggle(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_TOGGLE, NULL);
LOG_INFO("AirPlay play/pause");
}
static void raop_pause(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_PAUSE, NULL);
LOG_INFO("AirPlay pause");
}
static void raop_play(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_PLAY, NULL);
LOG_INFO("AirPlay play");
}
static void raop_stop(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_STOP, NULL);
LOG_INFO("AirPlay stop");
}
static void raop_prev(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_PREV, NULL);
LOG_INFO("AirPlay previous");
}
static void raop_next(bool pressed) {
if (!pressed) return;
raop_cmd(raop, RAOP_NEXT, NULL);
LOG_INFO("AirPlay next");
}
const static actrls_t controls = {
NULL, // power
raop_volume_up, raop_volume_down, // volume up, volume down
raop_toggle, raop_play, // toggle, play
raop_pause, raop_stop, // pause, stop
NULL, NULL, // rew, fwd
raop_prev, raop_next, // prev, next
NULL, NULL, NULL, NULL, // left, right, up, down
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, // pre1-10
raop_volume_down, raop_volume_up, raop_toggle// knob left, knob_right, knob push
};
/****************************************************************************************
* Command handler
*/
static bool cmd_handler(raop_event_t event, ...) {
va_list args;
va_start(args, event);
// handle audio event and stop if forbidden
if (!cmd_handler_chain(event, args)) {
va_end(args);
return false;
}
// now handle events for display
switch(event) {
case RAOP_SETUP:
actrls_set(controls, false, NULL, actrls_ir_action);
displayer_control(DISPLAYER_ACTIVATE, "AIRPLAY", true);
displayer_artwork(NULL);
break;
case RAOP_PLAY:
displayer_control(DISPLAYER_TIMER_RUN);
break;
case RAOP_FLUSH:
displayer_control(DISPLAYER_TIMER_PAUSE);
break;
case RAOP_STALLED:
raop_abort(raop);
actrls_unset();
displayer_control(DISPLAYER_SHUTDOWN);
break;
case RAOP_STOP:
actrls_unset();
displayer_control(DISPLAYER_SUSPEND);
break;
case RAOP_METADATA: {
char *artist = va_arg(args, char*), *album = va_arg(args, char*), *title = va_arg(args, char*);
displayer_metadata(artist, album, title);
break;
}
case RAOP_ARTWORK: {
uint8_t *data = va_arg(args, uint8_t*);
displayer_artwork(data);
break;
}
case RAOP_PROGRESS: {
int elapsed = va_arg(args, int), duration = va_arg(args, int);
displayer_timer(DISPLAYER_ELAPSED, elapsed, duration);
break;
}
default:
break;
}
va_end(args);
return true;
}
/****************************************************************************************
* Airplay sink de-initialization
*/
void raop_sink_deinit(void) {
raop_delete(raop);
}
/****************************************************************************************
* Airplay sink startup
*/
static void raop_sink_start(nm_state_t state_id, int sub_state) {
esp_netif_t* netif;
esp_netif_ip_info_t ipInfo = { };
uint8_t mac[6];
char* sink_name = (char*) config_alloc_get_default(NVS_TYPE_STR, "airplay_name", CONFIG_AIRPLAY_NAME, 0);
netif = network_get_active_interface();
esp_netif_get_ip_info(netif, &ipInfo);
esp_netif_get_mac(netif, mac);
cmd_handler_chain = raop_cbs.cmd;
LOG_INFO( "starting Airplay for ip %s with servicename %s", inet_ntoa(ipInfo.ip.addr), sink_name);
raop = raop_create(ipInfo.ip.addr, sink_name, mac, 0, cmd_handler, raop_cbs.data);
free(sink_name);
}
/****************************************************************************************
* Airplay sink initialization
*/
void raop_sink_init(raop_cmd_vcb_t cmd_cb, raop_data_cb_t data_cb) {
raop_cbs.cmd = cmd_cb;
raop_cbs.data = data_cb;
network_register_state_callback(NETWORK_WIFI_ACTIVE_STATE, WIFI_CONNECTED_STATE, "raop_sink_start", raop_sink_start);
network_register_state_callback(NETWORK_ETH_ACTIVE_STATE, ETH_ACTIVE_CONNECTED_STATE, "raop_sink_start", raop_sink_start);
}
/****************************************************************************************
* Airplay forced disconnection
*/
void raop_disconnect(void) {
LOG_INFO("forced disconnection");
// in case we can't communicate with AirPlay controller, abort session
if (!raop_cmd(raop, RAOP_STOP, NULL)) cmd_handler(RAOP_STALLED);
else displayer_control(DISPLAYER_SHUTDOWN);
}

40
lib/raop/raop_sink.h Normal file
View File

@@ -0,0 +1,40 @@
/*
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>
#include <stdarg.h>
#define RAOP_SAMPLE_RATE 44100
typedef enum { RAOP_SETUP, RAOP_STREAM, RAOP_PLAY, RAOP_FLUSH, RAOP_METADATA, RAOP_ARTWORK, RAOP_PROGRESS, RAOP_PAUSE, RAOP_STOP, RAOP_STALLED,
RAOP_VOLUME, RAOP_TIMING, RAOP_PREV, RAOP_NEXT, RAOP_REW, RAOP_FWD,
RAOP_VOLUME_UP, RAOP_VOLUME_DOWN, RAOP_RESUME, RAOP_TOGGLE } raop_event_t ;
typedef bool (*raop_cmd_cb_t)(raop_event_t event, ...);
typedef bool (*raop_cmd_vcb_t)(raop_event_t event, va_list args);
typedef void (*raop_data_cb_t)(const u8_t *data, size_t len, u32_t playtime);
/**
* @brief init sink mode (need to be provided)
*/
void raop_sink_init(raop_cmd_vcb_t cmd_cb, raop_data_cb_t data_cb);
/**
* @brief deinit sink mode (need to be provided)
*/
void raop_sink_deinit(void);
/**
* @brief force disconnection
*/
void raop_disconnect(void);
#endif /* RAOP_SINK_H*/

864
lib/raop/rtp.c Normal file
View File

@@ -0,0 +1,864 @@
/*
* 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 "raop_sink.h"
#include "log_util.h"
#include "util.h"
#ifdef WIN32
#include <openssl/aes.h>
#include "alac_wrapper.h"
#define MSG_DONTWAIT 0
#else
#include "esp_pthread.h"
#include "esp_system.h"
#include <mbedtls/version.h>
#include <mbedtls/aes.h>
#include "alac_wrapper.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))
extern log_level raop_loglevel;
static log_level *loglevel = &raop_loglevel;
//#define __RTP_STORE
// default buffer size
#define BUFFER_FRAMES_MAX ((RAOP_SAMPLE_RATE * 10) / 352 )
#define BUFFER_FRAMES_MIN ( (150 * RAOP_SAMPLE_RATE * 2) / (352 * 100) )
#define MAX_PACKET 1408
#define MIN_LATENCY 11025
#define MAX_LATENCY ( (120 * RAOP_SAMPLE_RATE * 2) / 100 )
#define RTP_STACK_SIZE (4*1024)
#define RTP_SYNC (0x01)
#define NTP_SYNC (0x02)
#define RESEND_TO 250
enum { DATA = 0, CONTROL, TIMING };
static const u8_t silence_frame[MAX_PACKET] = { 0 };
uint32_t buffer_frames = ((150 * RAOP_SAMPLE_RATE * 2) / (352 * 100));
typedef u16_t seq_t;
typedef struct __attribute__((__packed__)) audio_buffer_entry { // decoded audio packets
u32_t rtptime, last_resend;
s16_t *data;
u16_t len;
u8_t ready;
u8_t allocated;
u8_t missed;
} 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;
u8_t *decrypt_buf;
u32_t frame_size, frame_duration;
u32_t 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 {
u64_t local, remote;
} timing;
struct {
u32_t rtp, time;
u8_t status;
} synchro;
int latency; // rtp hold depth in samples
u32_t resent_req, resent_rec; // total resent + recovered frames
u32_t silent_frames; // total silence frames
u32_t discarded;
abuf_t audio_buffer[BUFFER_FRAMES_MAX];
seq_t ab_read, ab_write;
pthread_mutex_t ab_mutex;
#ifdef WIN32
pthread_t thread;
#else
TaskHandle_t thread, joiner;
StaticTask_t *xTaskBuffer;
StackType_t xStack[RTP_STACK_SIZE] __attribute__ ((aligned (4)));
#endif
struct alac_codec_s *alac_codec;
int first_seqno;
enum { RTP_WAIT, RTP_STREAM, RTP_PLAY } state;
int stalled;
raop_data_cb_t data_cb;
raop_cmd_cb_t cmd_cb;
} rtp_t;
#define BUFIDX(seqno) ((seq_t)(seqno) % buffer_frames)
static void buffer_alloc(abuf_t *audio_buffer, int size, uint8_t *buf, size_t buf_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 int seq_order(seq_t a, seq_t b);
#ifdef WIN32
static void *rtp_thread_func(void *arg);
#else
static void rtp_thread_func(void *arg);
#endif
/*---------------------------------------------------------------------------*/
static struct alac_codec_s* alac_init(int fmtp[32]) {
struct alac_codec_s *alac;
unsigned sample_rate, block_size;
unsigned char sample_size, channels;
struct {
uint32_t frameLength;
uint8_t compatibleVersion;
uint8_t bitDepth;
uint8_t pb;
uint8_t mb;
uint8_t kb;
uint8_t numChannels;
uint16_t maxRun;
uint32_t maxFrameBytes;
uint32_t avgBitRate;
uint32_t sampleRate;
} config;
config.frameLength = htonl(fmtp[1]);
config.compatibleVersion = fmtp[2];
config.bitDepth = fmtp[3];
config.pb = fmtp[4];
config.mb = fmtp[5];
config.kb = fmtp[6];
config.numChannels = fmtp[7];
config.maxRun = htons(fmtp[8]);
config.maxFrameBytes = htonl(fmtp[9]);
config.avgBitRate = htonl(fmtp[10]);
config.sampleRate = htonl(fmtp[11]);
alac = alac_create_decoder(sizeof(config), (unsigned char*) &config, &sample_size, &sample_rate, &channels, &block_size);
if (!alac) {
LOG_ERROR("cannot create alac codec", NULL);
return NULL;
}
return alac;
}
/*---------------------------------------------------------------------------*/
rtp_resp_t rtp_init(struct in_addr host, int latency, char *aeskey, char *aesiv, char *fmtpstr,
short unsigned pCtrlPort, short unsigned pTimingPort,
uint8_t *buffer, size_t size,
raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb)
{
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->cmd_cb = cmd_cb;
ctx->data_cb = data_cb;
ctx->rtp_host.sin_family = AF_INET;
ctx->rtp_host.sin_addr.s_addr = INADDR_ANY;
pthread_mutex_init(&ctx->ab_mutex, 0);
ctx->first_seqno = -1;
ctx->latency = latency;
ctx->ab_read = ctx->ab_write;
#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;
ctx->decrypt_buf = malloc(MAX_PACKET);
}
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) / RAOP_SAMPLE_RATE;
// alac decoder
ctx->alac_codec = alac_init(fmtp);
rc &= ctx->alac_codec != NULL;
buffer_alloc(ctx->audio_buffer, ctx->frame_size*4, buffer, size);
// 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;
ctx->running = true;
#ifdef WIN32
pthread_create(&ctx->thread, NULL, rtp_thread_func, (void *) ctx);
#else
ctx->xTaskBuffer = (StaticTask_t*) heap_caps_malloc(sizeof(StaticTask_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
ctx->thread = xTaskCreateStaticPinnedToCore( (TaskFunction_t) rtp_thread_func, "RTP_thread", RTP_STACK_SIZE, ctx,
CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1, ctx->xStack, ctx->xTaskBuffer,
CONFIG_PTHREAD_TASK_CORE_DEFAULT );
#endif
// cleanup everything if we failed
if (!rc) {
LOG_ERROR("[%p]: cannot start RTP", ctx);
rtp_end(ctx);
ctx = NULL;
}
resp.ctx = ctx;
return resp;
}
/*---------------------------------------------------------------------------*/
void rtp_end(rtp_t *ctx)
{
int i;
if (!ctx) return;
if (ctx->running) {
#if !defined WIN32
ctx->joiner = xTaskGetCurrentTaskHandle();
#endif
ctx->running = false;
#ifdef WIN32
pthread_join(ctx->thread, NULL);
#else
ulTaskNotifyTake(pdFALSE, portMAX_DELAY);
vTaskDelete(ctx->thread);
SAFE_PTR_FREE(ctx->xTaskBuffer);
#endif
}
for (i = 0; i < 3; i++) closesocket(ctx->rtp_sockets[i].sock);
if (ctx->alac_codec) alac_delete_decoder(ctx->alac_codec);
if (ctx->decrypt_buf) free(ctx->decrypt_buf);
pthread_mutex_destroy(&ctx->ab_mutex);
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 exit_locked)
{
pthread_mutex_lock(&ctx->ab_mutex);
// always store flush seqno as we only want stricly above it, even when equal to RECORD
ctx->first_seqno = seqno;
bool flushed = false;
// no need to stop playing if recent or equal to record - but first_seqno is needed
if (ctx->state == RTP_PLAY) {
buffer_reset(ctx->audio_buffer);
ctx->state = RTP_WAIT;
flushed = true;
LOG_INFO("[%p]: FLUSH packets below %hu - %u", ctx, seqno, rtptime);
}
if (!exit_locked || !flushed) pthread_mutex_unlock(&ctx->ab_mutex);
return flushed;
}
/*---------------------------------------------------------------------------*/
void rtp_flush_release(rtp_t *ctx) {
pthread_mutex_unlock(&ctx->ab_mutex);
}
/*---------------------------------------------------------------------------*/
void rtp_record(rtp_t *ctx, unsigned short seqno, unsigned rtptime) {
ctx->first_seqno = (seqno || rtptime) ? seqno : -1;
ctx->state = RTP_WAIT;
LOG_INFO("[%p]: record %hu - %u", ctx, seqno, rtptime);
}
/*---------------------------------------------------------------------------*/
static void buffer_alloc(abuf_t *audio_buffer, int size, uint8_t *buf, size_t buf_size) {
for (buffer_frames = 0; buf && buf_size >= size && buffer_frames < BUFFER_FRAMES_MAX; buffer_frames++) {
audio_buffer[buffer_frames].data = (s16_t*) buf;
audio_buffer[buffer_frames].allocated = 0;
audio_buffer[buffer_frames].ready = 0;
buf += size;
buf_size -= size;
}
LOG_INFO("allocated %d buffers (min=%d) from buffer of %zu bytes", buffer_frames, BUFFER_FRAMES_MIN, buf_size + buffer_frames * size);
for(; buffer_frames < BUFFER_FRAMES_MIN; buffer_frames++) {
audio_buffer[buffer_frames].data = malloc(size);
audio_buffer[buffer_frames].allocated = 1;
audio_buffer[buffer_frames].ready = 0;
}
}
/*---------------------------------------------------------------------------*/
static void buffer_release(abuf_t *audio_buffer) {
int i;
for (i = 0; i < buffer_frames; i++) {
if (audio_buffer[i].allocated) 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, u16_t *outsize) {
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, ctx->decrypt_buf, aeslen, &ctx->aes, iv, AES_DECRYPT);
#else
mbedtls_aes_crypt_cbc(&ctx->aes, MBEDTLS_AES_DECRYPT, aeslen, iv, (unsigned char*) buf, ctx->decrypt_buf);
#endif
memcpy(ctx->decrypt_buf+aeslen, buf+aeslen, len-aeslen);
alac_to_pcm(ctx->alac_codec, (unsigned char*) ctx->decrypt_buf, (unsigned char*) dest, 2, (unsigned int*) outsize);
} else {
alac_to_pcm(ctx->alac_codec, (unsigned char*) buf, (unsigned char*) dest, 2, (unsigned int*) outsize);
}
*outsize *= 4;
}
/*---------------------------------------------------------------------------*/
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 we have received a RECORD with a seqno, then this is the first allowed rtp sequence number
* and we are in RTP_WAIT state. If seqno was 0, then we are waiting for a flush that will tell
* us what should be our first allowed packet but we must accept everything, wait and clean when
* we the it arrives. This means that first packet moves us to RTP_STREAM state where we accept
* frames but wait for the FLUSH. If this was a FLUSH while playing, then we are also in RTP_WAIT
* state but we do have an allowed seqno and we should not accept any frame before we have it */
// if we have a pending first seqno and we are below, always ignore it
if (ctx->first_seqno != -1 && seq_order(seqno, ctx->first_seqno)) {
pthread_mutex_unlock(&ctx->ab_mutex);
return;
}
if (ctx->state == RTP_WAIT) {
ctx->ab_write = seqno - 1;
ctx->ab_read = ctx->ab_write + 1;
ctx->resent_req = ctx->resent_rec = ctx->silent_frames = ctx->discarded = 0;
if (ctx->first_seqno != -1) {
LOG_INFO("[%p]: 1st accepted packet:%d, now playing", ctx, seqno);
ctx->state = RTP_PLAY;
ctx->first_seqno = -1;
u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
ctx->cmd_cb(RAOP_PLAY, playtime);
} else {
ctx->state = RTP_STREAM;
LOG_INFO("[%p]: 1st accepted packet:%hu, waiting for FLUSH", ctx, seqno);
}
} else if (ctx->state == RTP_STREAM && ctx->first_seqno != -1 && seq_order(ctx->first_seqno, seqno + 1)) {
// now we're talking, but first discard all packets with a seqno below first_seqno AND not ready
while (seq_order(ctx->ab_read, ctx->first_seqno) ||
!ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready) {
ctx->audio_buffer[BUFIDX(ctx->ab_read)].ready = false;
ctx->ab_read++;
}
LOG_INFO("[%p]: done waiting for FLUSH with packet:%d, now playing starting:%hu", ctx, seqno, ctx->ab_read);
ctx->state = RTP_PLAY;
ctx->first_seqno = -1;
u32_t playtime = ctx->synchro.time + ((rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
ctx->cmd_cb(RAOP_PLAY, playtime);
}
abuf = ctx->audio_buffer + BUFIDX(seqno);
if (seqno == (u16_t) (ctx->ab_write+1)) {
// expected packet
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 (ctx->latency && seq_order(ctx->latency / ctx->frame_size, seqno - ctx->ab_write - 1)) {
// this is a shitstorm, reset buffer
LOG_WARN("[%p] too many missing frames %hu seq: %hu, (W:%hu R:%hu)", ctx, seqno - ctx->ab_write - 1, seqno, ctx->ab_write, ctx->ab_read);
ctx->ab_read = seqno;
} else {
// request re-send missed frames and evaluate resent date as a whole *after*
if (ctx->state == RTP_PLAY) rtp_request_resend(ctx, ctx->ab_write + 1, seqno-1);
// resend date is after all requests have been sent
u32_t now = gettime_ms();
// set expected timing of missed frames for buffer_push_packet and set last_resend date
for (seq_t i = ctx->ab_write + 1; seq_order(i, seqno); 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);
}
ctx->ab_write = seqno;
} else if (seq_order(ctx->ab_read, seqno + 1)) {
// recovered packet, not yet sent
ctx->resent_rec++;
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
if (abuf->missed) LOG_INFO("[%p]: packet too late seqno:%hu rtptime:%u (W:%hu R:%hu)", ctx, seqno, rtptime, ctx->ab_write, ctx->ab_read);
abuf = NULL;
}
if (ctx->in_frames++ > 1000) {
LOG_INFO("[%p]: fill [level:%hu rec:%u] [W:%hu R:%hu]", ctx, ctx->ab_write - ctx->ab_read, ctx->resent_rec, ctx->ab_write, ctx->ab_read);
ctx->in_frames = 0;
}
if (abuf) {
alac_decode(ctx, abuf->data, data, len, &abuf->len);
abuf->ready = 1;
abuf->missed = 0;
// this is the local rtptime when this frame is expected to play
abuf->rtptime = rtptime;
buffer_push_packet(ctx);
#ifdef __RTP_STORE
fwrite(data, len, 1, ctx->rtpIN);
fwrite(abuf->data, abuf->len, 1, ctx->rtpOUT);
#endif
}
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, hold = max((ctx->latency * 1000) / (8 * RAOP_SAMPLE_RATE), 100);
// not ready to play yet
if (ctx->state != RTP_PLAY || ctx->synchro.status != (RTP_SYNC | NTP_SYNC)) return;
// there is always at least one frame in the buffer
do {
// re-evaluate time in loop in case data callback blocks ...
now = gettime_ms();
// try to manage playtime so that we overflow as late as possible if we miss NTP (2^31 / 10 / 44100)
curframe = ctx->audio_buffer + BUFIDX(ctx->ab_read);
playtime = ctx->synchro.time + ((curframe->rtptime - ctx->synchro.rtp) * 10) / (RAOP_SAMPLE_RATE / 100);
if (now > playtime) {
LOG_DEBUG("[%p]: discarded frame now:%u missed by:%d (W:%hu R:%hu)", ctx, now, now - playtime, ctx->ab_write, ctx->ab_read);
ctx->discarded++;
curframe->ready = 0;
} else if (playtime - now <= hold) {
if (curframe->ready) {
ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime);
curframe->ready = 0;
} else {
LOG_DEBUG("[%p]: created zero frame (W:%hu R:%hu)", ctx, ctx->ab_write, ctx->ab_read);
ctx->data_cb(silence_frame, ctx->frame_size * 4, playtime);
ctx->silent_frames++;
curframe->missed = 1;
}
} else if (curframe->ready) {
ctx->data_cb((const u8_t*) curframe->data, curframe->len, playtime);
curframe->ready = 0;
} else {
break;
}
ctx->ab_read++;
ctx->out_frames++;
} while (seq_order(ctx->ab_read, ctx->ab_write));
if (ctx->out_frames > 1000) {
LOG_INFO("[%p]: drain [level:%hd head:%d ms] [W:%hu R:%hu] [req:%u sil:%u dis:%u]",
ctx, ctx->ab_write - ctx->ab_read, playtime - now, ctx->ab_write, ctx->ab_read,
ctx->resent_req, ctx->silent_frames, ctx->discarded);
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);
// try to request resend missing packet in order, explore up to 32 frames
for (int step = max((ctx->ab_write - ctx->ab_read + 1) / 32, 1),
i = 0, first = 0;
seq_order(ctx->ab_read + i, ctx->ab_write); i += step) {
abuf_t* frame = ctx->audio_buffer + BUFIDX(ctx->ab_read + i);
// stop when we reach a ready frame or a recent pending resend
if (first && (frame->ready || now - frame->last_resend <= RESEND_TO)) {
if (!rtp_request_resend(ctx, first, ctx->ab_read + i - 1)) break;
first = 0;
i += step - 1;
} else if (!frame->ready && now - frame->last_resend > RESEND_TO) {
if (!first) first = ctx->ab_read + i;
frame->last_resend = now;
}
}
}
/*---------------------------------------------------------------------------*/
#ifdef WIN32
static void *rtp_thread_func(void *arg) {
#else
static void rtp_thread_func(void *arg) {
#endif
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 request 3 times
ntp_sent = rtp_request_timing(ctx);
}
while (ctx->running) {
ssize_t plen;
char type;
socklen_t rtp_client_len = sizeof(struct sockaddr_in);
int idx = 0;
char *pktp = packet;
struct timeval timeout = {0, 100*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) {
if (ctx->stalled++ == 30*10) ctx->cmd_cb(RAOP_STALLED);
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, MSG_DONTWAIT, (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) {
LOG_WARN("Nothing received on a readable socket %d", plen);
continue;
}
assert(plen <= MAX_PACKET);
ctx->stalled = 0;
type = packet[1] & ~0x80;
pktp = packet;
switch (type) {
seq_t seqno;
unsigned rtptime;
// re-sent packet
case 0x56: {
pktp += 4;
plen -= 4;
}
// fall through
// 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));
u16_t flags = ntohs(*(u16_t*)(pktp+2));
u32_t remote_gap = NTP2MS(remote - ctx->timing.remote);
// try to get NTP every 3 sec or every time if we are not synced
if (!count-- || !(ctx->synchro.status & NTP_SYNC)) {
rtp_request_timing(ctx);
count = 3;
}
// something is wrong, we should not have such gap
if (remote_gap > 10000) {
LOG_WARN("discarding remote timing information %u", remote_gap);
break;
}
pthread_mutex_lock(&ctx->ab_mutex);
// re-align timestamp and expected local playback time (and magic 11025 latency)
ctx->latency = rtp_now - rtp_now_latency;
if (flags == 7 || flags == 4) ctx->latency += 11025;
if (ctx->latency < MIN_LATENCY) ctx->latency = MIN_LATENCY;
else if (ctx->latency > MAX_LATENCY) ctx->latency = MAX_LATENCY;
ctx->synchro.rtp = rtp_now - ctx->latency;
ctx->synchro.time = ctx->timing.local + remote_gap;
// 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) {
LOG_INFO("[%p]: 1st sync packet received", ctx);
}
pthread_mutex_unlock(&ctx->ab_mutex);
LOG_DEBUG("[%p]: sync packet latency:%d rtp_latency:%u rtp:%u remote ntp:%llx, local time:%u local rtp:%u (now:%u)",
ctx, ctx->latency, rtp_now_latency, rtp_now, remote, ctx->synchro.time, ctx->synchro.rtp, gettime_ms());
if ((ctx->synchro.status & RTP_SYNC) && (ctx->synchro.status & NTP_SYNC)) ctx->cmd_cb(RAOP_TIMING);
break;
}
// NTP timing packet
case 0x53: {
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) {
// ask for another one only if we are not synced already
if (!(ctx->synchro.status & NTP_SYNC)) rtp_request_timing(ctx);
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
u64_t expected = ctx->timing.remote + MS2NTP(reference - ctx->timing.local);
*/
ctx->timing.remote = remote;
ctx->timing.local = reference;
// now we are synced on NTP (mutex not needed)
ctx->synchro.status |= NTP_SYNC;
LOG_DEBUG("[%p]: Timing references local:%llu, remote:%llx (delta:%lld, sum:%lld, adjust:%lld, gaps:%d)",
ctx, ctx->timing.local, ctx->timing.remote);
break;
}
default: {
LOG_WARN("Unknown packet received %x", (int) type);
break;
}
}
}
free(packet);
LOG_INFO("[%p]: terminating", ctx);
#ifndef WIN32
xTaskNotifyGive(ctx->joiner);
vTaskSuspend(NULL);
#else
return NULL;
#endif
}
/*---------------------------------------------------------------------------*/
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), MSG_DONTWAIT, (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_req += (seq_t) (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), MSG_DONTWAIT, (struct sockaddr*) &ctx->rtp_host, sizeof(ctx->rtp_host))) {
LOG_WARN("[%p]: SENDTO failed (%s)", ctx, strerror(errno));
return false;
}
return true;
}

23
lib/raop/rtp.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef _HAIRTUNES_H_
#define _HAIRTUNES_H_
#include "raop_sink.h"
#include "util.h"
typedef struct {
unsigned short cport, tport, aport;
struct rtp_s *ctx;
} rtp_resp_t;
rtp_resp_t rtp_init(struct in_addr host, int latency,
char *aeskey, char *aesiv, char *fmtpstr,
short unsigned pCtrlPort, short unsigned pTimingPort,
uint8_t *buffer, size_t size,
raop_cmd_cb_t cmd_cb, raop_data_cb_t data_cb);
void rtp_end(struct rtp_s *ctx);
bool rtp_flush(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime, bool exit_locked);
void rtp_flush_release(struct rtp_s *ctx);
void rtp_record(struct rtp_s *ctx, unsigned short seqno, unsigned rtptime);
void rtp_metadata(struct rtp_s *ctx, struct metadata_s *metadata);
#endif

606
lib/raop/util.c Normal file
View File

@@ -0,0 +1,606 @@
/*
* AirConnect: Chromecast & UPnP to AirPlay
*
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*
*/
#include "platform.h"
#ifdef WIN32
#include <iphlpapi.h>
#else
#include "esp_netif.h"
// IDF-V4++ #include "esp_netif.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
tcpip_adapter_ip_info_t ipInfo;
tcpip_adapter_if_t if_type = TCPIP_ADAPTER_IF_STA;
// then get IP address
tcpip_adapter_get_ip_info(if_type, &ipInfo);
// we might be in AP mode
if (ipInfo.ip.addr == INADDR_ANY) {
if_type = TCPIP_ADAPTER_IF_AP;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ipInfo);
}
// get hostname if required
if (name) {
const char *hostname;
tcpip_adapter_get_hostname(if_type, &hostname);
*name = strdup(hostname);
}
return ipInfo.ip.addr;
#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;
}

89
lib/raop/util.h Normal file
View File

@@ -0,0 +1,89 @@
/*
* Misc utilities
*
* (c) Philippe 2016-2017, philippe_44@outlook.com
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*
*/
#ifndef __UTIL_H
#define __UTIL_H
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "platform.h"
#include "pthread.h"
#ifndef WIN32
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
#endif
#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);
#define SAFE_PTR_FREE(P) free(P)
#else
char *strlwr(char *str);
// reason is that TCB might be cleanup in idle task
#define SAFE_PTR_FREE(P) \
do { \
TimerHandle_t timer = xTimerCreate("cleanup", pdMS_TO_TICKS(10000), pdFALSE, P, _delayed_free); \
xTimerStart(timer, portMAX_DELAY); \
} while (0)
static void inline _delayed_free(TimerHandle_t xTimer) {
free(pvTimerGetTimerID(xTimer));
xTimerDelete(xTimer, portMAX_DELAY);
}
#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);
char* kd_dump(key_data_t *kd);
void kd_free(key_data_t *kd);