mirror of
https://github.com/sle118/squeezelite-esp32.git
synced 2026-05-10 07:19:32 +01:00
applied platformio structure
This commit is contained in:
10
lib/raop/CMakeLists.txt
Normal file
10
lib/raop/CMakeLists.txt
Normal 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
15
lib/raop/component.mk
Normal 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
552
lib/raop/dmap_parser.c
Normal 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
90
lib/raop/dmap_parser.h
Normal 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
40
lib/raop/log_util.h
Normal 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
117
lib/raop/platform.h
Normal 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
975
lib/raop/raop.c
Normal 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, ¤t, &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", ×tamp);
|
||||
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", ×tamp);
|
||||
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
19
lib/raop/raop.h
Normal 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
202
lib/raop/raop_sink.c
Normal 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
40
lib/raop/raop_sink.h
Normal 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
864
lib/raop/rtp.c
Normal 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
23
lib/raop/rtp.h
Normal 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
606
lib/raop/util.c
Normal 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
89
lib/raop/util.h
Normal 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);
|
||||
Reference in New Issue
Block a user