├── .gitattributes ├── .gitignore ├── .gitmodules ├── AirConnect-1.8.3.zip ├── CHANGELOG ├── LICENSE ├── README.md ├── aircast ├── AirCast.vcxproj ├── CastMessage.options ├── CastMessage.proto ├── Makefile ├── aircast.vcxproj.filters └── src │ ├── aircast.c │ ├── aircast.h │ ├── cast_parse.c │ ├── cast_parse.h │ ├── cast_util.c │ ├── cast_util.h │ ├── castcore.c │ ├── castcore.h │ ├── castitf.h │ ├── castmessage.pb.c │ ├── castmessage.pb.h │ ├── config_cast.c │ └── config_cast.h ├── airupnp.service ├── airupnp ├── AirUPnP.vcxproj ├── Makefile ├── airupnp.vcxproj.filters └── src │ ├── airupnp.c │ ├── airupnp.h │ ├── avt_util.c │ ├── avt_util.h │ ├── config_upnp.c │ ├── config_upnp.h │ ├── mr_util.c │ └── mr_util.h ├── build.cmd ├── build.sh ├── buildall.sh ├── common ├── manifest.xml └── metadata.h └── updater /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | RC* 7 | 8 | # Precompiled Headers 9 | *.gch 10 | *.pch 11 | 12 | # Compiled Dynamic libraries 13 | *.so 14 | *.dylib 15 | #*.dll 16 | 17 | # Fortran module files 18 | *.mod 19 | 20 | # Compiled Static libraries 21 | *.lai 22 | *.la 23 | *.a 24 | *.lib 25 | 26 | # Executables 27 | #*.exe 28 | *.out 29 | *.app 30 | 31 | # ========================= 32 | # Operating System Files 33 | # ========================= 34 | 35 | # OSX 36 | # ========================= 37 | 38 | .DS_Store 39 | .AppleDouble 40 | .LSOverride 41 | 42 | # Thumbnails 43 | ._* 44 | 45 | # Files that might appear in the root of a volume 46 | .DocumentRevisions-V100 47 | .fseventsd 48 | .Spotlight-V100 49 | .TemporaryItems 50 | .Trashes 51 | .VolumeIcon.icns 52 | 53 | # Directories potentially created on remote AFP share 54 | .AppleDB 55 | .AppleDesktop 56 | Network Trash Folder 57 | Temporary Items 58 | .apdisk 59 | 60 | # Windows 61 | # ========================= 62 | 63 | # Windows image file caches 64 | Thumbs.db 65 | ehthumbs.db 66 | 67 | # Folder config file 68 | Desktop.ini 69 | 70 | # Recycle Bin used on file shares 71 | $RECYCLE.BIN/ 72 | 73 | # Windows Installer files 74 | *.cab 75 | *.msi 76 | *.msm 77 | *.msp 78 | 79 | # Windows shortcuts 80 | *.lnk 81 | 82 | # C++ builder related 83 | test/ 84 | misc/ 85 | __history 86 | /package*.* 87 | _backup/ 88 | *.pcm 89 | *.cbproj 90 | *.groupproj 91 | *.zip 92 | *.z 93 | *.local 94 | *.$$$ 95 | *.res 96 | *.i?? 97 | *.map 98 | *.tds 99 | *-idx* 100 | *.save* 101 | *.pcm 102 | *.#* 103 | *.bin 104 | *.log 105 | *.bak 106 | *.pdb 107 | *.user 108 | tests/ 109 | bin/bcc/ 110 | bin/msvc 111 | .vs/ 112 | __recovery/ 113 | *.sln 114 | build/ 115 | air*/bin 116 | *.bat 117 | bin/ 118 | !AirConnect*.zip 119 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "common/dmap-parser"] 2 | path = common/dmap-parser 3 | url = https://github.com/philippe44/dmap-parser 4 | [submodule "aircast/nanopb"] 5 | path = aircast/nanopb 6 | url = https://github.com/nanopb/nanopb 7 | [submodule "aircast/libjansson"] 8 | path = aircast/libjansson 9 | url = https://github.com/philippe44/libjansson 10 | fetchRecurseSubmodules = no 11 | [submodule "common/libpupnp"] 12 | path = common/libpupnp 13 | url = https://github.com/philippe44/libpupnp 14 | fetchRecurseSubmodules = no 15 | [submodule "common/libopenssl"] 16 | path = common/libopenssl 17 | url = https://github.com/philippe44/libopenssl 18 | fetchRecurseSubmodules = no 19 | [submodule "common/libmdns"] 20 | path = common/libmdns 21 | url = https://github.com/philippe44/libmdns 22 | fetchRecurseSubmodules = no 23 | [submodule "common/libcodecs"] 24 | path = common/libcodecs 25 | url = https://github.com/philippe44/libcodecs 26 | fetchRecurseSubmodules = no 27 | [submodule "common/crosstools"] 28 | path = common/crosstools 29 | url = https://github.com/philippe44/crosstools 30 | [submodule "common/libraop"] 31 | path = common/libraop 32 | url = https://github.com/philippe44/libraop 33 | fetchRecurseSubmodules = no 34 | [submodule "common/libpthreads4w"] 35 | path = common/libpthreads4w 36 | url = https://github.com/philippe44/libpthreads4w 37 | fetchRecurseSubmodules = no 38 | -------------------------------------------------------------------------------- /AirConnect-1.8.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philippe44/AirConnect/263806c92bf39cecb0f347d5bc1ca0806759bae3/AirConnect-1.8.3.zip -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.8.3 2 | - (aircast) stream type is LIVE and duration is not part of metadata 3 | 4 | 1.8.2 5 | - UpnpResolveURL needs one extra byte if RelURL parameters does not start with a '/' 6 | - use setlocale() so that atof does not fail on '.' 7 | 8 | 1.8.1 9 | - handle double resend from iOS and silence after flush 10 | 11 | 1.8.0 12 | - refactor raop flush/first packet 13 | 14 | 1.7.1 15 | - alternate getting of TransportInfo and PositionInfo (increase polling speed) 16 | - ignore any frame seq below or equal flush seqno, even after a record 17 | 18 | 1.7.0 19 | - allow icy on every codec, just provide interval, let user config and requesting player decide 20 | - (airupnp) only re-acquire state on play action completion. This is not needed on stop/pause and will create issues like fake stops 21 | 22 | 1.6.3 23 | - (airupnp) on error, reset counter to 0 if player responds 24 | 25 | 1.6.2 26 | - (airupnp) when player timeout's, use its DescDocURL, not the UpdateData which is NULL... 27 | 28 | 1.6.1 29 | - be more relax wrt player deletion and verify with either failed doc download (airupnp) or no response to ping (aircast) 30 | 31 | 1.6.0 32 | - (airupnp) remove possibility to tweak ProtocolInfo, it's useless 33 | - only send silence on first GET (after a flush) and close socket on flush 34 | - close socket immediately on flush which should stop playback right away 35 | 36 | 1.5.4 37 | - use updated libcodec and libraop for alac 38 | 39 | 1.5.3 40 | - previous version had a miss build 41 | - fully add aac 42 | - add extension to stream's url 43 | - flc -> flac (but flc still works) 44 | 45 | 1.5.2 46 | - updated libraries with alac fixes (zero-initialized encoded buffer) 47 | 48 | 1.5.1 49 | - (airupnp) call http_pico_close on exit 50 | - fix all known memory leaks 51 | - use updated libcodecs that fixes codec close crash (NULL ptr) 52 | 53 | 1.5.0 54 | - add aac codec (use refactored libraop and move encoder to libcodecs 55 | 56 | 1.4.0 57 | - proper fix of HTTP frame sending & filling 58 | 59 | 1.3.4 60 | - libraop was sending 1 instead of 0 on icy retransmit since day one ... can't believe it 61 | 62 | 1.3.3 63 | - RTP and NTP order was not fixed either 64 | 65 | 1.3.2 66 | - fix http_fill 67 | 68 | 1.3.1 69 | - updated libraop allows subsequent GET, even with no range, to restart from 0 for players that open, close and re-open (for no reason) 70 | 71 | 1.3.0 72 | - add -N to allow simple name transformation 73 | 74 | 1.2.5 75 | - (aircast) ignore SIGPIPE (picohttp) 76 | - (aircast) fix queue walker 77 | 78 | 1.2.4 79 | - use fixed libraop for unknown RTSP queries that should return no headers 80 | 81 | 1.2.3 82 | - (airupnp) test errorcoutn for < 0, not just -1 83 | - (aircast) must increase number of clients for a source 84 | 85 | 1.2.2 86 | - (airupnp) fix upnp socket error deteection 87 | 88 | 1.2.1 89 | - (airupnp) unified source with spotconnect 90 | - (aircast) revert exclusion of devices based on netmask 91 | - display iface and network at start 92 | - update XML_UpdateNode bug 93 | 94 | 1.2.0 95 | - use libraop version that re-sends HTTP when *explictely* request a range 0-XXX to try to cope with player that don't understand what HTTP protocol is and keep askign for a small "chunk" 96 | - aircast display iface name upon startup 97 | 98 | 1.1.9 99 | - HTTP exec function should not crash on error when there is no token returned 100 | 101 | 1.1.8 102 | - json_pack with "so" does not require object's ref to be decremented 103 | 104 | 1.1.7 105 | - Don't call metadata callback on flush as it causes spurious play, call it on "STREAMER_PLAY" if we have metadata 106 | 107 | 1.1.6 108 | - update openssl to 1.1.1u 109 | 110 | 1.1.5 111 | - use shutdown instead of close in picohttp server when source has not been found or HEAD was used 112 | 113 | 1.1.4 114 | - Make static few function of aircast.c that should have been 115 | - (aircast) update of config file was not done on new device creation 116 | 117 | 1.1.3 118 | - (aircast) fix netmask endianness which created false expiries 119 | 120 | 1.1.2 121 | - (aircast) mDNS searcher was not regularly calling back 122 | 123 | 1.1.1 124 | - remove group volume scaling on SetVolume 125 | 126 | 1.1.0 127 | - airupnp : add artwork in mp3 mode 128 | - aircast : add metadata (including artwork) 129 | 130 | 1.0.16 131 | - bring back armv5 build 132 | - fixes big memory leak in upnp 133 | 134 | 1.0.14 135 | - change network binding for UPnP: get name 136 | 137 | 1.0.13 138 | - don't use debug libs for Windows 139 | 140 | 1.0.12 141 | - update libraop (no functional change) 142 | 143 | 1.0.11 144 | - mdnssvc API change 145 | 146 | 1.0.10 147 | - Use (almost) full static GLIBC (-nss) 148 | 149 | 1.0.9 150 | - add armv6 151 | 152 | 1.0.8 153 | - fix mdnssd 154 | 155 | 1.0.7 156 | - update libmdns (no unicast and compliant mode) 157 | 158 | 1.0.6 159 | - request specific libcrypto & libssl version for MacOS 160 | 161 | 1.0.5 162 | - remove UPnP IPv6 support 163 | 164 | 1.0.4 165 | - update mdnssd for unicast 166 | 167 | 1.0.3 168 | - update mdnssd that was binding queries to a port for no reason causing a race failure on freebsd 169 | 170 | 1.0.2 171 | - mips is usually bigendian 172 | 173 | 1.0.1 174 | - use new compilers so that min glib is 2.23 175 | 176 | 1.0.0 177 | - new build system 178 | - fix IGMP snooping (hopefully) 179 | 180 | 0.2.51.2 181 | - fix HTTP2 upgrade response to "Connection" 182 | - audio/mp3 is not a correct mimetype... 183 | 184 | 0.2.51.1 185 | - Fix CVE-2017-12087 186 | 187 | 0.2.51.0 188 | - invoke stop callback on TEARDOWN first 189 | 190 | 0.2.50.5 191 | - mdns fix for blank name field 192 | 193 | 0.2.50.4 194 | - fix sslsym.c macros that were still incorrect 195 | 196 | 0.2.50.3 197 | - noflush was not effective for aircast 198 | 199 | 0.2.50.2 200 | - make sslsym compatible with openSSL 1.1.x 201 | 202 | 0.2.50.1 203 | - noflush properly send silent frames at the right speed 204 | 205 | 0.2.50.0 206 | - flush was broken since 0.2.27.0 207 | - add --noflush options (and force :f in latencies if set) 208 | - increment stream index at each SetURI/LOAD to make it unique 209 | 210 | 0.2.44.1 211 | - (aircast) reset "Remove" flag when adding 212 | 213 | 0.2.44.0 214 | - openssl 1.1.x compatibility 215 | 216 | 0.2.43.1 217 | - address valgrind complaints 218 | 219 | 0.2.43.0 220 | - don't remove unresponsive UPnP devices when playing 221 | - don't remove CC devices that still respond to ping 222 | 223 | 0.2.42.1 224 | - re-align airupnp & aircast version numbers 225 | 226 | 0.2.42.0 227 | - a bit of cleaning wrt below issue 228 | - use "binding" as general parameter name for upnp/cast 229 | 230 | 0.2.41.2 231 | - %&@!* of Linux does not handle sscanf with %[^:]:%u when the first string is empty 232 | 233 | 0.2.41.1 234 | - RTSP port was not bound to range! 235 | 236 | 0.2.41.0 237 | - allow chunked encoding and fixed length 238 | - DLNA.ORG_PN was missing for mp3 239 | 240 | 0.2.40.0 241 | - add HTTP header callback for better DLNA handling 242 | - answer to getcontentFeatures 243 | 244 | 0.2.30.0 245 | - add port range 246 | 247 | 0.2.28.5 248 | - tweak HTTP logs 249 | 250 | 0.2.28.4 251 | - http_parse SDEBUG statement did not print received line 252 | 253 | 0.2.28.3 254 | - remove stristr and clean strcasestr case called with NULL 255 | 256 | 0.2.28.2 257 | - exclude NULL modelnumber when -o is specified unless they are explicitely autorized 258 | 259 | 0.2.28.1 260 | - check that model & modelnumber are not NULL before looking of exclusion 261 | 262 | 0.2.28.0 263 | - make max_players a parameter 264 | 265 | 0.2.27.1 266 | - handle range request beyond count better 267 | 268 | 0.2.27.0 269 | - (airupnp) add -u and UPnPMax config options to set the max upnp search 270 | 271 | 0.2.26.1 272 | - (airupnp) config update shall not be done when there is not config 273 | 274 | 0.2.26.0 275 | - (aircast) add -v to globally set group media volume 276 | 277 | 0.2.25.0 278 | - (airupnp) search for mediarenderer:2 279 | 280 | 0.2.24.7 281 | - garbadge left in airupnp.c 282 | 283 | 0.2.24.6 284 | - Can't re-send more bytes than we've received 285 | 286 | 0.2.24.5 287 | - (aircast) clear SSL context on shutdown 288 | 289 | 0.2.24.4 290 | - race condition where the UPnP/CC device could be stopped by a flush after it started to play due to de-sync of flush/play (RTP) detetcion 291 | 292 | 0.2.24.3 293 | - (airupnp) add remote_title 294 | 295 | 0.2.24.2 296 | - restore trailing '+' UPnP name changes 297 | 298 | 0.2.24.1 299 | - do not copy NULL strings ... (util.c) 300 | 301 | 0.2.24.0 302 | - do not send body when method is HEAD 303 | 304 | 0.2.23.1 305 | - A records expiration 306 | 307 | 0.2.23.0 308 | - improve mDNS detection on multiple VLAN 309 | 310 | 0.2.22.1 311 | - (upnp) -o option matches on exact model numbers only 312 | 313 | 0.2.22.0 314 | - (upnp) add -o option to enable only listed model numbers 315 | 316 | 0.2.21.3 317 | - remove un-necessary muted flag 318 | - in Volume feedback, CalcGroupVolume was called before caller device was updated 319 | 320 | 0.2.21.2 321 | - Can't use Sonos' group volume API as it takes time to "settle down", so need to create our own 322 | 323 | 0.2.21.1 324 | - handle master's volume within all devices loop 325 | 326 | 0.2.21.0 327 | - volume timestamp moved to UPnP/CC side instead of RAOP 328 | 329 | 0.2.20.0 330 | - modify libupnp to search using unicast, but not device seems to support uPnP 1.1 331 | - CheckAndLock waqs not locking device 332 | - double timestamp to handle fast volume changes 333 | - do not delete UPnP context when a Sonos player becomes a slave to manage volume for Sonos groups 334 | 335 | 0.2.13.1 336 | - revert buffer indexing modifications 337 | 338 | 0.2.13.0 339 | - follow original name change 340 | 341 | 0.2.12.2 342 | - correct a key problem with circula buffer management and fill estimation 343 | - integer promotion caused u16 comparisons to be wrong 344 | 345 | 0.2.12.1 346 | - UDP sockets don't need shutdown 347 | - destroy ab_mutex on thread termination 348 | 349 | 0.2.12.0 350 | - static versions now include full static openssl 351 | 352 | 0.2.11.0 353 | - revert previous change but force short int promotio in buf_fill calculation 354 | - add protocolInfo as a parameter 355 | 356 | 0.2.10.3 357 | - ancient 32 bits overflow bug in expected playtime calculation 358 | 359 | 0.2.10.2 360 | - update built-in dll version to ones that do not need VS 361 | - add x86_64 static version 362 | 363 | 0.2.10.1 364 | - Windows 10 does has renamed libeay32.dll to libcrypto.dll and ssleay32.dll to libssl.dll (and does not even include it) 365 | - do not crash when SSL not found 366 | 367 | 0.2.10.0 368 | - openSSL loaded manually for compatibility => no need any more to deal with various openssl versions! 369 | 370 | 0.2.9.0 371 | - (aircast) authorize "on behalf" announces as long as they are not from a group member 372 | 373 | 0.2.8.0 374 | - silence frames count on http reconnect shall be 0 when already sending silence 375 | - limit packet recovery to max of rtp latency and http delay 376 | - misc network congestion corrections 377 | - forced fill http moved to :f option on latencies 378 | - add parameter 379 | - STILL NEED TO KNOW WHY AIRCAST LOCKS ON EXIT 380 | 381 | 0.2.7.0 382 | - add parameter to disable range acceptance in hairtunes (needed for shairtunes2) 383 | - send silence frames as soon as playtime reached to ensure continuous HTTP stream 384 | - reduce initial silence frames by number of already received frames 385 | - discriminate FLUSH send at start to clean pipe from FLUSH used to stop playback using RECORD values and timer 386 | - resend timeout was wrong (in http thread) 387 | - silence frames count on http reconnect shall be 0 when already sending silence 388 | - discard NTP sync that have a larger roundtrip (100ms) 389 | - RTP latency can be negative to force filling of silence frame when buffer is empty but playtime has elapsed (continuous stream) 390 | 391 | 0.2.6.1 392 | - (cast) simplify device removal (no explicit wakeup call) 393 | 394 | 0.2.6.0 395 | - move RTSP server back to using accept() instead of select() and use connect() to close the connection 396 | - remove usage of sleep() to redeuce CPU consumption in idle 397 | 398 | 0.2.5.0 399 | - backport some squeeze2upnp/cast changes with no impact 400 | - add -n option in airupnp to exclude model number 401 | 402 | 0.2.4.0 403 | - Detect main IP address change and restart 404 | 405 | 0.2.3.1 406 | - version mistake with 0.2.3.0 407 | 408 | 0.2.3.0 409 | - add -c command line 410 | - work with CC that require a range request (buffer up to 2MB) 411 | - metadata was freed after ctx was released 412 | - silence frame shall be ignored when range requested 413 | - default codec for upnp was 'flac' instead of 'flc' 414 | 415 | 0.2.2.6 416 | - set length for silent frame now that length must be returned 417 | 418 | 0.2.2.5 419 | - aesiv and aeskey can start with a '\0', use a decrypt explicit boolean 420 | - QueueFlush correction 421 | 422 | 0.2.2.4 423 | - Group update could free NULL pointer (cast only) 424 | 425 | 0.2.2.3 426 | - artwork was not a duplication, hence releasing metadata was failing (upnp only) 427 | 428 | 0.2.2.2 429 | - update build toolchain to full cross compiling, including OSX, FreeBSD and Solaris 430 | - added Linux ppc 431 | - (cast) stop receiver should send STOP to sessionId, not mediaSessionId 432 | 433 | 0.2.2.1 434 | - flac codec name is 'flc' not 'flac' 435 | 436 | 0.2.2.0 437 | - add mp3 for encoding and icy metadata 438 | - use Sonos radio stream url for mp3 439 | - add option to set flac compression level 440 | - encoder was not mutex protected against flush 441 | 442 | 0.2.1.1 443 | - Some controlers like AirParrot do not send encrypted audio 444 | 445 | 0.2.1.0 446 | - add static parameter 447 | - 32 bits comparaison correction 448 | - (aircast) do not set playing state on "buffering" event 449 | 450 | 0.2.0.8 451 | - add aarch64 452 | 453 | 0.2.0.7 454 | - loglimit was not used in airupnp 455 | 456 | 0.2.0.6 457 | - kd_dump was writing in a NULL string when no header was received 458 | 459 | 0.2.0.5 460 | - alac.c compiles depends on endianness 461 | - Some players (Roon) send a FLUSH after a TEARDOWN which caused access to a NULL hairtunes context 462 | - When changing track, the previous HTTP socket was used to send new track, causing miss of flac header & track beginning yielding to very long CC start 463 | 464 | 0.2.0.4 465 | - (airupnp) backporting of squeeze2upnp modifications 466 | - Add TCP_NODELAY to HTTP socket 467 | 468 | 0.2.0.3 469 | - race condition & double free when stopping device (credit @codepeon) 470 | - http parsing error caused memory leak (headers not released) 471 | - cosmetic changes 472 | - (airupnp) backport squeeze2upnp modification (AVTSetURI) 473 | - (airupnp) subscription complete failure could lead to access NULL pointer 474 | 475 | 0.2.0.2 476 | - On keep-alive UPnP result, always move to next item if device found in list (even if removed as a Sonos slave) 477 | - only use SEARCH_RESULT and not ADVERTISEMENT_ALIVE for keep-alive 478 | 479 | 0.2.0.1 480 | - (airupnp) search for master should return true in case of UPnP error 481 | - (aircast) mDNS search for groups was messed-up 482 | 483 | 0.2.0.0 484 | - new handling of discovery (mDNS-SD and UPnP) for on-the-fly player addition / removal (no more discovery time and removal counts) 485 | - Review of all mutex-related issue and various risk of unprotected code 486 | - review of memory leaks (only the SSL remains, which is the same all over execution) 487 | - mDNS records is now 120s (per RFQ) and not 75m so that in case of crash, player do not appear sticky for some applications 488 | - add possibility to dump players ('dump' & 'dumpall' commmands) 489 | - in SETUP response, cport and tport are set to 0 by default which will cause session to fail if none of these are provided 490 | - handle "header folding" in HTTP response 491 | - HTTP headers w/o : should not crash kbd_free 492 | - use closesocket under Windows, not close! 493 | - (aircast) SSL context is not deleted at every disconnect to avoid memory leaks 494 | - (aircast) wrong volume local change when mute was set 495 | 496 | 0.1.6.1 497 | - add Solaris i86pc build 498 | - change some mdns options 499 | 500 | 0.1.5.3 501 | - compile flag to store streams 502 | 503 | 0.1.5.2 504 | - FreeBSD does not exit accept() even when socket shutdown properly, need to use select() 505 | 506 | 0.1.5.1 507 | - memory leak correction in isMatsre & GetGroupVolume (@codepeon) 508 | 509 | 0.1.5.0 510 | - add "+" after default name to identify AirConnect players 511 | 512 | 0.1.4.4 513 | - always remove players on BYEBYE message 514 | 515 | 0.1.4.3 516 | - (airupnp) default name is zone name for Sonos device 517 | 518 | 0.1.4.2 519 | - (airupnp) properly eliminate Sonos slaves 520 | 521 | 0.1.4.1 522 | - misc Sonos group cleaning 523 | 524 | 0.1.4.0 525 | - (airupnp) detect Sonos groups and do not add slaves 526 | - (airupnp) add Sonos groups volume 527 | 528 | 0.1.3.2 529 | - discovery and exit (-i) could abort without searching on aircast 530 | - new devices were not added to existing file when using -I 531 | 532 | 0.1.3.1 533 | - when cannot start, exit with no-zero code 534 | 535 | 0.1.3.0 536 | - title changed to "Streaming from AirConnect" 537 | - code cleaning 538 | 539 | 0.1.2.0 540 | - send minimum metadata 541 | - (aircast) remove unused config items 542 | 543 | 0.1.1.0 544 | - support wav and pcm codecs 545 | 546 | 0.1.0.5 547 | - rare race condition in UPnP search timeout/result 548 | 549 | 0.1.0.4 550 | - flac_ready boolean was un-necessary 551 | - wait for playing (RTP) before sending silent frames 552 | - was sending a few raw frames at the beginning when using flac 553 | - functions renaming for better consistency 554 | 555 | 0.1.0.3 556 | - pthread_join of remote search could be called twice 557 | - update renderer thread cleaned for better exit 558 | - memory leaks correction 559 | 560 | 0.1.0.2 561 | - code cleaning 562 | - exclusion (-m) parsing was wrong and ";" is not a valid separator 563 | 564 | 0.1.0.0 565 | - HTTP delay uses silence frames 566 | 567 | 0.0.2.7 568 | - do not hold RTP frames when they are available! only hold missing one, up to RTP latency 569 | - add HTTP delay for Chromecast as well 570 | 571 | 0.0.2.6 572 | - NULL (empty xml item) was causing crash and default save set to empty 573 | 574 | 0.0.2.5 575 | - in config file was not taken into account 576 | 577 | 0.0.2.4 578 | - only notify of playback when frame is not silence (all 0) to avoid iOS 10.x spurious play 579 | - add wav header so that wav can be used in AirCast as well 580 | 581 | 0.0.2.3 582 | - increase SO_SNDBUF to try to better handle sloppy networks 583 | 584 | 0.0.2.2 585 | - high CPU usage in http frames handling 586 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Philippe, philippe_44@outlook.com 2 | 3 | This is under MIT license (https://mit-license.org) 4 | 5 | This program uses 3rd party software that is licensed by their 6 | author under their own conditions. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirConnect: Send audio to UPnP/Sonos/Chromecast players using AirPlay 2 | Use these applications to add AirPlay capabilities to Chromecast and UPnP (like Sonos) players, to make them appear as AirPlay devices. 3 | 4 | AirConnect can run on any machine that has access to your local network (Windows, MacOS x86 and arm64, Linux x86, x86_64, arm, aarch64, sparc, mips, powerpc, Solaris and FreeBSD). It does not need to be on your main computer. (For example, a Raspberry Pi works well). It will detect UPnP/Sonos/Chromecast players, create as many virtual AirPlay devices as needed, and act as a bridge/proxy between AirPlay clients (iPhone, iPad, iTunes, MacOS, AirFoil ...) and the real UPnP/Sonos/Chromecast players. 5 | 6 | The audio, after being decoded from alac, can be sent in plain, or re-encoded using mp3, aac or flac. Most players will not display metadata (artist, title, album, artwork ...) except when mp3 or aac re-encoding are used and for UPnP/DLNA devices that support icy protocol. Chromecast players support this after version 1.1.x 7 | 8 | ## Installing 9 | 10 | 1. Pre-built binaries are in `AirConnect-.zip`. It can be downloaded manually in a terminal by typing `wget https://raw.githubusercontent.com/philippe44/AirConnect/master/airconnect-.zip`. Unzip the file an select the bianry that works for your system. 11 | 12 | * For **Chromecast**, the file is `aircast--` (so `aircast-macos-x86_64` for Chromecast on MacOS + Intel CPU) 13 | * For **UPnP/Sonos**, the file is `airupnp--` (so `airupnp-macos-arm64` for UPnP/Sonos on MacOS + arm CPU) 14 | 15 | 2. There is a "-static" version of each application that has all static libraries built-in. Use of these is (really) not recommended unless the regular version fails. For MacOS users, you need to install openSSL and do the following steps to use the dynamic load library version: 16 | - install openssl: `brew install openssl`. This creates libraries (or at least links) into `/usr/local/opt/openssl[/x.y.z]/lib` where optional 'x.y.z' is a version number 17 | - create links to these libraries: 18 | ``` 19 | ln -s /usr/local/opt/openssl[/x.y.z]/lib/libcrypto.dylib /usr/local/lib/libcrypto.dylib 20 | ln -s /usr/local/opt/openssl[/x.y.z]/lib/libssl.dylib /usr/local/lib/libssl.dylib 21 | ``` 22 | 23 | 3. For Windows, install the Microsoft VC++ redistributable found [here](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170) 24 | You will also need to grab the 2 dlls files and put them in the same directory as the exe file 25 | 26 | 4. Store the \ (e.g. `airupnp-linux-aarch64`) in any directory. 27 | 28 | 4. On non-Windows machines, open a terminal and change directories to where the executable is stored and run `chmod +x ` (Example: `chmod +x airupnp-macos`). File permissions might need to be set. 29 | 30 | 5. Don't use firewall or set ports using options below and open them. 31 | - Port 5353 (UDP) is needed to listen to mDNS messages 32 | - Each device uses 1 port permanently (RTSP) and when playing adds 1 port for HTTP and 3 ports for RTP (use `-g`or \ parameter, default is random) 33 | - UPnP adds one extra port for discovery (use `-b` or \ parameter, default is 49152 and user value must be *above* this) 34 | 35 | 6. [@faserF](https://github.com/FaserF) has made a [script](https://github.com/philippe44/AirConnect/blob/master/updater) for install/update 36 | ter) 37 | 38 | 7. In Docker, you must use 'host' mode to enable audio webserver. Note that you can't have a NAT between your devices and the machine where AirConnect runs. 39 | 40 | ## Running 41 | 42 | Double click the \ or launch it by typing `./` in the same command line window. 43 | 44 | For Sonos & Heos players, set latency by adding `-l 1000:2000` on the command line. (Example: `./airupnp-macos -l 1000:2000`) 45 | 46 | You should start to see lots of log messages on screen. Using your iOS/Mac/iTunes/Airfoil/other client, you should now see new AirPlay devices and can try to play audio to them. 47 | 48 | If it works, type `exit`, which terminates the executable, and then, on non-Windows/MacOS machines, relaunch it with `-z` so that it can run in the background and you can close the command line window. You can also start it automatically using any startup script or a Linux service as explained below. Nothing else should be required, no library or anything to install. 49 | 50 | *For each platform, there is a normal and a '-static' version. This one includes all libraries directly inside the application, so normally there is no dependence to 3rd party shared libraries, including SSL. You can try it if the normal fails to load (especially on old systems), but static linkage is a blessing a curse (exact reasons out of scope of this README). Now, if the static version still does not work, there are other solutions that are pretty technical, see [here](https://github.com/philippe44/cross-compiling#running-an-application-by-forcing-glibc-and-glibcxx). Best is that you open an issue if you want help with that.* 51 | 52 | ## Common information: 53 | 54 | Use `-h` for command line details 55 | - When started in interactive mode (w/o -Z or -z option) a few commands can be typed at the prompt 56 | - `exit` 57 | - `save ` : save the current configuration in file named [name] 58 | - Volume changes made in native control applications are synchronized with AirPlay client 59 | - Pause, Stop, Next, Prev using native control application are sent to AirPlay client - once paused, "native" play will not work 60 | - Re-scan for new / lost players happens every 30s 61 | - A config file (default `config.xml`) can be created for advanced tweaking (a reference version can be generated using the `-i ` command line) 62 | - Chromecast groups are supported. Use `-v` to set the media volume factor for all devices (0.5 by default) 63 | - use `-c mp3[:]|aac[:]|flac[:0..9]|wav|pcm` to set codec use for re-encoding audio 64 | - When you have more than one ethernet card, you case use `-b [ip]` to set what card to bind to. Note that 0.0.0.0 is not authorized 65 | - Use `-u ` to set the maximum UPnP searched version 66 | - Use `-b [ip|iface][:port]` to set network interface (ip@ or interface name as reported by ifconfig/ipconfig) to use and, for airupnp only, UPnP port to listen to (must be above the default 49152) 67 | - Use `-a [:]` to specify a port range (default count is 128, sets RTP and HTTP ports) 68 | - Use `-g -3|-1|0|` to tweak http transfer mode where -3 = chunked, -1 = no content-length and 0 = fixed (dummy) length (see "HTTP content-length" below)" 69 | - Use `-N ""` to change the default name of AirPlay players (the player name followed by '+' by default). It's a C-string format where '%s' is the player's name, so default is "%s+" 70 | - Use of `-z` disables interactive mode (no TTY) **and** self-daemonizes (use `-p ` to get the PID). Use of `-Z` only disables interactive mode 71 | - Do not daemonize (using & or any other method) the executable w/o disabling interactive mode (`-Z`), otherwise it will consume all CPU. On Linux, FreeBSD and Solaris, best is to use `-z`. Note that -z option is not available on MacOS or Windows 72 | - A 'click' noise can be heard when timings are adjusted by adding or skipping one 8ms frame. Use `-r` to disable such adjustements (or use `` option in config file), but that might cause overrun or underrun on long playbacks 73 | - This is an audio-only application. Do not expect to play a video on your device and have the audio from UPnP/Sonos or ChromeCast synchronized. It does not, cannot and will not work, regardless of any latency parameter. Please do not open tickets requesting this (see details below to understand why) 74 | 75 | ## Config file parameters 76 | 77 | The default configuration file is `config.xml`, stored in the same directory as the \. Each of parameters below can be set in the `` section to apply to all devices. It can also be set in any `` section to apply only to a specific device and overload the value set in ``. Use the `-x `command line option to use a config file of your choice. 78 | 79 | - `latency <[rtp][:http][:f]>` : (default: (0:0))buffering tweaking, needed when audio is shuttering or for bad networks (delay playback start) 80 | * [rtp] : ms of buffering of RTP (AirPlay) audio. Below 500ms is not recommended. 0 = use value from AirPlay. A negative value force sending of silence frames when no AirPlay audio has been received after 'RTP' ms, to force a continuous stream. If not, the UPnP/CC player will be not receive audio and some might close the connection after a while, although most players will simply be silent until stream restarts. This shall not be necessary in most of the case. 81 | * [http] : ms of buffering silence for HTTP audio (not needed normaly, except for Sonos) 82 | * [f] : when network congestion happens, source frames will not be received at all. Set this parameter to force sending silence frame then. Otherwise, no HTTP data will be sent and player might close the connection 83 | - `drift <0|1>` : enable adding or dropping a frame when case source frames producion is too fast or too slow 84 | - `enabled <0|1>` : in common section, enables new discovered players by default. In a dedicated section, enables the player 85 | - `name` : The name that will appear for the device in AirPlay. You can change the default name. 86 | - `upnp_max` : set the maximum UPnP version use to search players (default 1) 87 | - `http_length` : same as `-g` command line parameter 88 | - `metadata <0|1>` : send metadata to player (only for mp3 and aac codecs and if player supports ICY protocol) 89 | - `artwork` : an URL to an artwork to be displayed on player 90 | - `flush <0|1>` : (default 1) set AirPlay *FLUSH* commands response (see also --noflush in [Misc tips](#misc-tips) section) 91 | - `media_volume <0..1>` : (default 0.5) Applies a scaling factor to device's hardware volume (chromecast only) 92 | - `codec ]|aac[:]|flac[:0..9]|wav|pcm>` : format used to send HTTP audio. FLAC is recommended but uses more CPU (pcm only available for UPnP). For example, `mp3:320` for 320Kb/s MP3 encoding. 93 | 94 | These are the global parameters 95 | 96 | - `max_players` : set the maximum of players (default 32) 97 | - `log_limit <-1 | n>` : (default -1) when using log file, limits its size to 'n' MB (-1 = no limit) 98 | - `ports [:]` : set port range to use (see -a) 99 | 100 | ## Start automatically in Linux 101 | 102 | 1. Create a file in `/etc/systemd/system`, e.g. `airupnp.service` with the following content (assuming the airupnp binary is in `/var/lib/airconnect`) 103 | 104 | ``` 105 | [Unit] 106 | Description=AirUPnP bridge 107 | After=network-online.target 108 | Wants=network-online.target 109 | 110 | [Service] 111 | ExecStart=/var/lib/airconnect/airupnp-linux-arm -l 1000:2000 -Z -x /var/lib/airconnect/airupnp.xml 112 | Restart=on-failure 113 | RestartSec=30 114 | 115 | [Install] 116 | WantedBy=multi-user.target 117 | ``` 118 | 2. Enable the service `sudo systemctl enable airupnp.service` 119 | 120 | 3. Start the service `sudo service airupnp start` 121 | 122 | To start or stop the service manually, type `sudo service airupnp start|stop` in a command line window 123 | 124 | To disable the service, type `sudo systemctl disable airupnp.service` 125 | 126 | To view the log, `journalctl -u airupnp.service` 127 | 128 | On rPi lite, add the following to the /boot/cmdline.txt: init=/bin/systemd 129 | 130 | Obviously, from the above example, only use -x if you want a custom configuration. Thanks [@cactus](https://github.com/cactus) for systemd cleaning 131 | 132 | [@1activegeek](https://github.com/1activegeek) has made a docker container [here](https://github.com/1activegeek/docker-airconnect) that follows the update of this repository - thanks! 133 | 134 | ## Start automatically in MacOS (credits @aiwipro) 135 | 136 | Create the file com.aircast.bridge.plist in ~/Library/LaunchAgents/ 137 | 138 | ``` 139 | 140 | 141 | 142 | 143 | Label 144 | com.aircast.bridge 145 | ProgramArguments 146 | 147 | /[path]/aircast-macos 148 | -Z 149 | -x 150 | /[path]/aircast.xml 151 | -f 152 | /[path]/aircast.log 153 | 154 | RunAtLoad 155 | 156 | LaunchOnlyOnce 157 | 158 | KeepAlive 159 | 160 | 161 | 162 | ``` 163 | 164 | Where `[path]` is the path where you've stored the aircast executable (without the []). It can be for example `Users/xxx/airconnect` where `xxx` is your user name 165 | 166 | ## Start automatically under Windows 167 | 168 | There are many tools that allow an application to be run as a service. You can try this [one](http://nssm.cc/) 169 | 170 | ## Synology installation 171 | 172 | [@eizedev](https://github.com/eizedev) is now maitaining a package for automatic installation & launch of airupnp on Syno's [here](https://github.com/eizedev/AirConnect-Synology) 173 | 174 | ## Player specific hints and tips 175 | 176 | #### Sonos 177 | The upnp version is often used with Sonos players. When a Sonos group is created, only the master of that group will appear as an AirPlay player and others will be removed if they were already detected. If the group is later split, then individual players will re-appear. 178 | 179 | When changing volume of a group, each player's volume is changed trying to respect the relative values. It's not perfect and stil under test now. To reset all volumes to the same value, simply move the cursor to 0 and then to the new value. All players will have the same volume then. You need to use the Sonos application to change individual volumes. 180 | 181 | To identify your Sonos players, pick an identified IP address, and visit the Sonos status page in your browser, like `http://192.168.1.126:1400/support/review`. Click `Zone Players` and you will see the identifiers for your players in the `UUID` column. 182 | 183 | #### Bose SoundTouch 184 | [@chpusch](https://github.com/chpusch) has found that Bose SoundTouch work well including synchonisation (as for Sonos, you need to use Bose's native application for grouping / ungrouping). I don't have a SoundTouch system so I cannot do the level of slave/master detection I did for Sonos 185 | 186 | #### Pioneer/Phorus/Play-Fi 187 | Some of these speakers only support mp3 188 | ## Misc tips 189 | 190 | - When players disappear regularly, it might be that your router is filtering out multicast packets. For example, for a Asus AC-RT68U, you have to login by ssh and run echo 0 > /sys/class/net/br0/bridge/multicast_snooping but it does not stay after a reboot. 191 | 192 | - Lots of users seems to have problem with Unify and broadcasting / finding players. Here is a guide https://www.neilgrogan.com/ubnt-sonos/ made by somebody who fixes the issue for his Sonos 193 | 194 | - Some AirPlay controller send a FLUSH and immediately start sending new audio when skipping track. This causes AirConnect to issue a STOP and almost immediately a PLAY command which seems to be a problem for certain players (Sonos in some cases). A possible workaround is to ignore FLUSH request (see config file or use --noflush on the command line) but this has side effect on pause as silence frames are sent. At best restart is delayed and worse case it might not work with some codec (flac) 195 | 196 | - Some older Avahi distributions grab the port mDNS port 5353 for exclusive use, preventing AirConnect to respond to queries. Please set `disallow-other-stacks=no`in `/etc/avahi/avahi-daemon.conf` 197 | 198 | - If the non-static version fails to load complaining that GLIBCXX_3.4.29 is missing, please have a look [there](https://github.com/philippe44/cross-compiling#running-an-application-by-forcing-glibc-and-glibcxx) and use the existing libraries I've provided in that repository. You can simply copy the right `libstdc++.so.6.0.29` in the directory where AirConnect is and create symlink for `libstdc++.so` and `libstdc++.so.6`, then use the `LD_LIBRARY_PATH='$ORIGIN' ` trick, it will work without messing anything in your system. 199 | 200 | ## HTTP & UPnP specificities 201 | ### HTTP content-length and transfer modes 202 | Lots of UPnP player have very poor quality HTTP and UPnP stacks, in addition of UPnP itself being a poorly defined/certified standard. One of the main difficulty comes from the fact that AirConnect cannot provide the length of the file being streamed as the source is an infinite real time RTP flow coming from the AirPlay source. 203 | 204 | The HTTP standard is clear that the "content-length" header is optional and can be omitted when server does not know the size of the source. If the client is HTTP 1.1 there is another possibility which is to use "chunked" mode where the body of the message is divided into chunks of variable length. This is *explicitely* made for case of unknown source length and an HTTP client that claims to support 1.1 **must** support chunked-encoding. 205 | 206 | The default mode of AirUPnP is "no content-length" (\ = -1) but unfortunately, some players can't deal with that. You can then try "chunked-encoding" (\ = -3) but some players who claim to be HTTP 1.1 do not support it. There is a last resort option to add a large fake `content-length` (\ = 0). It is set to 2^31-1, so around 5 hours of playback with flac re-encoding. Note that if player is HTTP 1.0 and http_header is set to -3, AirUPnP will fallback no content-length. The command line option `-g` has the same effect that \ in the \ section of a config file. 207 | 208 | This might still not work as some players do not understand that the source is not a randomly accessible (searchable) file and want to get the first(e.g.) 128kB to try to do some smart guess on the length, close the connection, re-open it from the beginning and expect to have the same content. I'm trying to keep a buffer of last recently sent bytes to be able to resend-it, but that does not always works. Normally, players should understand that when they ask for a range and the response is 200 (full content), it *means* the source does not support range request but some don't (I've tried to add a header "accept: no-range but that makes things worse most of the time). 209 | 210 | ## Delay when switching track or source 211 | 212 | I've received that question many times: why is there (sometimes) many seconds of delay when I switch track (or source) from my iPhone before I hear the change? 213 | 214 | To understand, it's better that you read the next paragraph, but as you probably won't, here is a quick summary of how AirPlay works. As far as the sender (e.g. your iPhone) is concerned, once the connection with an AirPlay 'speaker' is established, this connection is almost like a analogue wire with a delay (buffer) of 1 or 2 seconds. 215 | 216 | What iOS does nowadays is that when you switch between tracks, instead of closing the connection and re-creating one, it just pushes the new audio through the existing connection, so you might have the 1~2 seconds of previous audio in the pipe before the new audio plays. Same thing when stopping/pausing playback, iOS simply stops pushing audio through the wire. 217 | 218 | There is a function to "flush" the audio in the pipe so that new audio plays immediately, but I've seen that recent versions of iOS don't use it anymore (or some applications decide to not flush while they could). That's not a big deal with most AirPlay speakers, it's a 1~2 second delay. 219 | 220 | But with AirConnect, the AirPlay speaker is not a speaker, it's a UPnP or Chromecast player. They do not at all act like virtual wires, they instead expect to have the whole track available as a file and retrieve data from it as needed. In fact, one of the key functions that AirConnect does is looking like a wire to iPhone and looking like a file to the UPnP/CC. 221 | 222 | Usually, UPnP/CC players consume a large chunk of that 'file' before they start to play to handle network congestion, but some don't and simply start playing at the first received byte, counting that the large chunk will come quickly. But that chunk/buffer does not exist for AirConnect as audio is produced in real time by the iPhone. So if a player starts at the first byte, it will very likely lack audio data when a network congestion occurs and playback will stutter. The parameter `http latency` solves this issue by creating a silence buffer sent in a burst when establishing a connection, but this creates a permanent delay between the iPhone and the player. Some UPnP/CC players wait to have buffered enough data before they start playing and again, because that data is built in real time by AirConnect, this other delay adds up to the latency parameter (even if http latency is 0). 223 | 224 | When you switch between tracks or sources (or pause/stop), if your iPhone sends this "flush" command, then AirConnect immediately stops the UPnP/CC player. But if there is no flush command, it will play until these silence + self buffers are consumed ... that can be more than a few seconds. 225 | 226 | In addition the delay can increase with time depending of clock speed difference between the iPhone and the UPnP/CC. Say that the iPhone's clock is 1% faster than the player's clock, then when it has produced 300s (5mins) of audio, the player has received it all but it has only played 297s, so there is an additional delay of 3s. If the iPhone moves track without the flush command, then the UPnP/CC player will start playing new audio (or stop) `http latency` + self-buffer length + 3 seconds later ... that can be a lot! 227 | 228 | Unfortunately, there is nothing I can do about that. By not using the "flush" command, iOS or application using AirPlay create an issue that AirConnect has no way to identify or avoid. 229 | 230 | ## Latency parameters explained: 231 | 232 | These bridges receive realtime "synchronous" audio from the AirPlay controller in the format of RTP frames and forward it to the Chromecast/UPnP/Sonos player in an HTTP "asynchronous" continuous audio binary format (notion of frames does not exist on that side). In other words, the AirPlay clients "push" the audio using RTP and the Chromecast/UPnP/Sonos players "pull" the audio using an HTTP GET request. 233 | 234 | A player using HTTP to get its audio expects to receive an initial large portion of audio as the response to its GET and this creates a large enough buffer to handle most further network congestion/delays. The rest of the audio transmission is regulated by the player using TCP flow control. But when the source is an AirPlay RTP device, there is no such large portion of audio available in advance to be sent to the Player, as the audio comes to the bridge in real time. Every 8ms, a RTP frame is received and is immediately forwarded as the continuation of the HTTP body. If the CC/UPnP/Sonos players starts to play immediately the 1st received audio sample, expecting an initial burst to follow, then any network congestion delaying RTP audio will starve the player and create shuttering. 235 | 236 | The [http] parameter allow a certain amount of silence frames to be sent to the Chromecast/UPnP/Sonos player, in a burst at the beginning. Then, while this "artificial" silence is being played, it's possible for the bridge to build a buffer of RTP frames that will then hide network delays that might happen in further RTP frames transmission. This delays the start of the playback by [http] ms. 237 | 238 | But RTP frames are transmitted using UDP, which means there is no guarantee of delivery, so frames might be lost from time to time (happens often on WiFi networks). To allow detection of lost frames, they are numbered sequentially (1,2 ... n) so every time two received frames are not consecutive, the missing ones can be requested again by the AirPlay receiver. 239 | 240 | Normally, the bridge forwards immediately every RTP frame using HTTP and again, in HTTP, the notion of frame numbers does not exit, it's just the continuous binary audio. So it's not possible to send audio non-sequentially when using HTTP 241 | 242 | For example, if received RTP frames are numbered 1,2,3,6, this bridge will forward (once decoded and transformed into raw audio) 1,2,3 immediately using HTTP but when it receives 6, it will re-request 4 and 5 to be resent and hold 6 while waiting (if 6 were to be transmitted immediately, the Chromecast/UPnP/Sonos will play 1,2,3,6 ... not nice). The [rtp] parameter sets for how long frame 6 shall be held before adding two silence frames for 4 and 5 and send sending 4,5,6. Obviously, if this delay is larger than the buffer in the Chromecast/UPnP/Sonos player, playback will stop by lack of audio. Note that [rtp] does not delay playback start. 243 | 244 | When [f] is set, silence frames will be inserted as soon as no RTP frames have been received during [rtp] ms. This ensure that a continuous stream of audio is available on the HTTP server. This might be necessary for some players that close the HTTP connection if they have not received data for some time. It's unlikely though. Note that otherwise when RTP stream is interrupted for more than [http] ms, the UPnP/CC player will stop anyway as it will have empty buffers. Still, as soon as the RTP stream resumes, the bridge will receive frame N, where the last received one might be N-500. So it will request the (up to) [rtp] missing ones (might be less than 500), and restart playing at N-[http], so potentially silence will be inserted. 245 | 246 | Many have asked for a way to do video/audio synchronisation so that UPnP (Sonos) players can be used as speakers when playing video on a computer or tablet (YouTube for example). Due to this RTP-to-HTTP bridging, this cannot be done as the exact time when an audio frame is played cannot be controlled on the HTTP client. AirPlay speakers can achieve that because the iPhone/iPad/MAC player will "delay" the video by a known amount, send the audio in advance (usually 2 sec) and then control the exact time when this audio is output by the speaker. But although AirConnect has the exact request timing and maintains synchronization with the player, it cannot "relay" that synchronization to the speakers. UPnP protocol does not allow this and Sonos has not made their protocol public. Sometimes you might get lucky because the video-to-audio delay will almost match the HTTP player delay, but it is not reproductible and will not be stable over time. 247 | 248 | ## Compiling from source 249 | It's a Makefile-oriented build, and there is a bash script (built.sh) and Windows one (build.cmd). The bash script is intended for cross-platform build and you might be able to call directly your native compiler, but have a look at the command line in the build.sh to make sure it can work. 250 | 251 | Please see [here](https://github.com/philippe44/cross-compiling/blob/master/README.md#organizing-submodules--packages) to know how to rebuild my apps in general: 252 | 253 | Otherwise, you can just get the source code and pre-built binaries: 254 | ``` 255 | cd ~ 256 | git clone http://github.com/philippe44/airconnect 257 | cd ~/airconnect 258 | git submodule update --init 259 | 260 | ``` 261 | NB: you can speed up all clonings by a lot by adding `--depth 1` option to just to a shallow clone (you probably don't need all the commits) 262 | 263 | and build doing: 264 | ``` 265 | cd ~/airconnect/airupnp 266 | make 267 | ``` 268 | 269 | -------------------------------------------------------------------------------- /aircast/AirCast.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Static 14 | Win32 15 | 16 | 17 | 18 | 19 | 20 | 21 | stdc11 22 | stdc11 23 | stdc11 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 16.0 49 | Win32Proj 50 | {B9B39729-0AF8-473D-BAC9-7E130808975A} 51 | 10.0 52 | .. 53 | $(BaseDir)\common 54 | 55 | 56 | 57 | Application 58 | v143 59 | Unicode 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | $(SolutionDir)\$(ProjectName)\build\$(Platform)\$(Configuration)\ 72 | $(SolutionDir)\bin\ 73 | 74 | 75 | $(ProjectName)-static 76 | 77 | 78 | 79 | Level3 80 | 4129;4018;4244;4101;4267;4102;4068;4142 81 | true 82 | UPNP_STATIC_LIB;PB_NO_STATIC_ASSERT;__PTW32_STATIC_LIB;FLAC__NO_DLL;_CRT_NONSTDC_NO_DEPRECATE;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 83 | true 84 | $(BaseCommon);$(BaseCommon)\libraop\targets\include;$(BaseCommon)\crosstools\src;$(BaseCommon)\libopenssl\targets\win32\$(PlatformTarget)\include;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\addons;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\upnp;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\ixml;;nanopb;$(BaseCommon)\libmdns\targets\include\mdnssvc;$(BaseCommon)\libmdns\targets\include\mdnssd;$(BaseCommon)\libpthreads4w\targets\win32\$(PlatformTarget)\include;$(BaseDir)\tools;$(BaseCommon)\dmap-parser\;libjansson\targets\win32\$(PlatformTarget)\include;$(BaseCommon)\libcodecs\targets\include\flac;$(BaseCommon)\libcodecs\targets\include\shine;$(BaseCommon)\libcodecs\targets\include\addons;%(AdditionalIncludeDirectories) 85 | stdcpp20 86 | $(TEMP)vc$(PlatformToolsetVersion)$(ProjectName).pdb 87 | 88 | 89 | Console 90 | true 91 | 92 | 93 | ws2_32.lib;wsock32.lib;%(AdditionalDependencies) 94 | libcmt;libcmtd 95 | 96 | 97 | ..\common\manifest.xml 98 | 99 | 100 | ..\common\manifest.xml 101 | 102 | 103 | 104 | 105 | SSL_LIB_STATIC;%(PreprocessorDefinitions) 106 | $(TEMP)vc$(PlatformToolsetVersion)$(ProjectName).pd 107 | 108 | 109 | $(BaseCommon)\libopenssl\targets\win32\$(PlatformTarget);%(AdditionalLibraryDirectories) 110 | libopenssl_static.lib;%(AdditionalDependencies) 111 | 112 | 113 | ..\common\manifest.xml 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /aircast/CastMessage.options: -------------------------------------------------------------------------------- 1 | CastMessage.source_id max_size:128 2 | CastMessage.destination_id max_size:128 3 | CastMessage.namespace max_size:128 4 | CastMessage.payload_utf8 max_size:2048 5 | -------------------------------------------------------------------------------- /aircast/CastMessage.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | syntax = "proto2"; 6 | 7 | //option optimize_for = LITE_RUNTIME; 8 | 9 | //package extensions.api.cast_channel; 10 | 11 | message CastMessage { 12 | // Always pass a version of the protocol for future compatibility 13 | // requirements. 14 | enum ProtocolVersion { 15 | CASTV2_1_0 = 0; 16 | } 17 | required ProtocolVersion protocol_version = 1 [default = CASTV2_1_0]; 18 | 19 | // source and destination ids identify the origin and destination of the 20 | // message. They are used to route messages between endpoints that share a 21 | // device-to-device channel. 22 | // 23 | // For messages between applications: 24 | // - The sender application id is a unique identifier generated on behalf of 25 | // the sender application. 26 | // - The receiver id is always the the session id for the application. 27 | // 28 | // For messages to or from the sender or receiver platform, the special ids 29 | // 'sender-0' and 'receiver-0' can be used. 30 | // 31 | // For messages intended for all endpoints using a given channel, the 32 | // wildcard destination_id '*' can be used. 33 | required string source_id = 2 [default = "sender-0"]; 34 | required string destination_id = 3 [default = "receiver-0"]; 35 | 36 | // This is the core multiplexing key. All messages are sent on a namespace 37 | // and endpoints sharing a channel listen on one or more namespaces. The 38 | // namespace defines the protocol and semantics of the message. 39 | required string namespace = 4; 40 | 41 | // Encoding and payload info follows. 42 | 43 | // What type of data do we have in this message. 44 | enum PayloadType { 45 | STRING = 0; 46 | BINARY = 1; 47 | } 48 | required PayloadType payload_type = 5 [default = STRING]; 49 | 50 | // Depending on payload_type, exactly one of the following optional fields 51 | // will always be set. 52 | optional string payload_utf8 = 6; 53 | optional bytes payload_binary = 7; 54 | } 55 | 56 | // Messages for authentication protocol between a sender and a receiver. 57 | message AuthChallenge { 58 | } 59 | 60 | message AuthResponse { 61 | required bytes signature = 1; 62 | required bytes client_auth_certificate = 2; 63 | repeated bytes client_ca = 3; 64 | } 65 | 66 | message AuthError { 67 | enum ErrorType { 68 | INTERNAL_ERROR = 0; 69 | NO_TLS = 1; // The underlying connection is not TLS 70 | } 71 | required ErrorType error_type = 1; 72 | } 73 | 74 | message DeviceAuthMessage { 75 | // Request fields 76 | optional AuthChallenge challenge = 1; 77 | // Response fields 78 | optional AuthResponse response = 2; 79 | optional AuthError error = 3; 80 | } -------------------------------------------------------------------------------- /aircast/Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(CC),cc) 2 | CC=$(lastword $(subst /, ,$(shell readlink -f `which cc`))) 3 | endif 4 | 5 | ifeq ($(findstring gcc,$(CC)),gcc) 6 | CFLAGS += -Wno-deprecated-declarations -Wno-format-truncation -Wno-stringop-truncation 7 | LDFLAGS += -s 8 | else 9 | CFLAGS += -fno-temp-file 10 | endif 11 | 12 | PLATFORM ?= $(firstword $(subst -, ,$(CC))) 13 | HOST ?= $(word 2, $(subst -, ,$(CC))) 14 | 15 | ifneq ($(HOST),macos) 16 | ifneq ($(HOST),solaris) 17 | LINKSTATIC = -static 18 | else 19 | LDFLAGS += -lssp 20 | endif 21 | endif 22 | 23 | BASE = .. 24 | CORE = $(BASE)/bin/aircast-$(HOST) 25 | BUILDDIR = $(dir $(CORE))$(HOST)/$(PLATFORM) 26 | EXECUTABLE = $(CORE)-$(PLATFORM) 27 | EXECUTABLE_STATIC = $(EXECUTABLE)-static 28 | 29 | SRC = src 30 | TOOLS = $(COMMON)/crosstools/src 31 | COMMON = $(BASE)/common 32 | MDNS = $(COMMON)/libmdns/targets 33 | RAOP = $(COMMON)/libraop/targets 34 | #VALGRIND = $(BASE)/valgrind 35 | PUPNP = $(COMMON)/libpupnp/targets/$(HOST)/$(PLATFORM) 36 | CODECS = $(COMMON)/libcodecs/targets 37 | OPENSSL = $(COMMON)/libopenssl/targets/$(HOST)/$(PLATFORM) 38 | NANOPB = nanopb 39 | JANSSON = libjansson/targets/$(HOST)/$(PLATFORM) 40 | 41 | DEFINES += -D_FILE_OFFSET_BITS=64 -DPB_FIELD_16BIT -DNDEBUG -D_GNU_SOURCE -DUPNP_STATIC_LIB 42 | CFLAGS += -Wall -fPIC -ggdb -O2 $(DEFINES) -fdata-sections -ffunction-sections -std=gnu11 43 | LDFLAGS += -lpthread -ldl -lm -L. 44 | 45 | vpath %.c $(TOOLS):$(SRC):$(NANOPB):$(COMMON) 46 | 47 | INCLUDE = -I$(OPENSSL)/include \ 48 | -I$(TOOLS) \ 49 | -I$(COMMON) \ 50 | -I$(SRC)/inc \ 51 | -I$(RAOP)/include \ 52 | -I$(PUPNP)/include/upnp -I$(PUPNP)/include/ixml -I$(PUPNP)/include/addons \ 53 | -I$(CODECS)/include/flac -I$(CODECS)/include/shine \ 54 | -I$(MDNS)/include/mdnssvc -I$(MDNS)/include/mdnssd \ 55 | -I$(NANOPB) \ 56 | -I$(JANSSON)/include 57 | 58 | DEPS = $(SRC)/aircast.h $(LIBRARY) $(LIBRARY_STATIC) 59 | 60 | SOURCES = castcore.c castmessage.pb.c aircast.c cast_util.c cast_parse.c config_cast.c \ 61 | cross_util.c cross_log.c cross_net.c cross_thread.c platform.c \ 62 | pb_common.c pb_decode.c pb_encode.c 63 | 64 | SOURCES_LIBS = cross_ssl.c 65 | 66 | OBJECTS = $(patsubst %.c,$(BUILDDIR)/%.o,$(SOURCES) $(SOURCES_LIBS)) 67 | OBJECTS_STATIC = $(patsubst %.c,$(BUILDDIR)/%.o,$(SOURCES)) $(patsubst %.c,$(BUILDDIR)/%-static.o,$(SOURCES_LIBS)) 68 | 69 | LIBRARY = $(RAOP)/$(HOST)/$(PLATFORM)/libraop.a \ 70 | $(PUPNP)/libpupnp.a \ 71 | $(CODECS)/$(HOST)/$(PLATFORM)/libcodecs.a \ 72 | $(MDNS)/$(HOST)/$(PLATFORM)/libmdns.a \ 73 | $(JANSSON)/libjansson.a 74 | 75 | LIBRARY_STATIC = $(LIBRARY) $(OPENSSL)/libopenssl.a 76 | 77 | all: directory $(EXECUTABLE) $(EXECUTABLE_STATIC) 78 | 79 | $(EXECUTABLE): $(OBJECTS) 80 | $(CC) $(OBJECTS) $(LIBRARY) $(CFLAGS) $(LDFLAGS) -o $@ 81 | ifeq ($(HOST),macos) 82 | rm -f $(CORE) 83 | lipo -create -output $(CORE) $$(ls $(CORE)* | grep -v '\-static') 84 | endif 85 | 86 | $(EXECUTABLE_STATIC): $(OBJECTS_STATIC) 87 | $(CC) $(OBJECTS_STATIC) $(LIBRARY_STATIC) $(CFLAGS) $(LDFLAGS) $(LINKSTATIC) -o $@ 88 | ifeq ($(HOST),macos) 89 | rm -f $(CORE)-static 90 | lipo -create -output $(CORE)-static $(CORE)-*-static 91 | endif 92 | 93 | $(OBJECTS) $(OBJECTS_STATIC): $(DEPS) 94 | 95 | directory: 96 | @mkdir -p $(BUILDDIR) 97 | @mkdir -p bin 98 | 99 | $(BUILDDIR)/%.o : %.c 100 | $(CC) $(CFLAGS) $(CPPFLAGS) $(INCLUDE) $< -c -o $@ 101 | 102 | $(BUILDDIR)/%-static.o : %.c 103 | $(CC) $(CFLAGS) $(CPPFLAGS) -DSSL_STATIC_LIB $(INCLUDE) $< -c -o $(BUILDDIR)/$*-static.o 104 | 105 | clean: 106 | rm -f $(OBJECTS) $(EXECUTABLE) $(OBJECTS_STATIC) $(EXECUTABLE_STATIC) $(CORE) $(CORE)-static 107 | 108 | -------------------------------------------------------------------------------- /aircast/aircast.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | crosstools 16 | 17 | 18 | crosstools 19 | 20 | 21 | crosstools 22 | 23 | 24 | crosstools 25 | 26 | 27 | crosstools 28 | 29 | 30 | crosstools 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {206efa88-95f2-4ef6-866d-71dce0afa8f9} 44 | 45 | 46 | -------------------------------------------------------------------------------- /aircast/src/aircast.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AirCast: Chromecast to AirPlay 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "platform.h" 17 | #include "pthread.h" 18 | #include "raop_server.h" 19 | #include "cast_util.h" 20 | 21 | #define VERSION "v1.8.3"" ("__DATE__" @ "__TIME__")" 22 | 23 | /*----------------------------------------------------------------------------*/ 24 | /* typedefs */ 25 | /*----------------------------------------------------------------------------*/ 26 | 27 | #define STR_LEN 256 28 | 29 | #define MAX_PROTO 128 30 | #define MAX_RENDERERS 32 31 | #define AV_TRANSPORT "urn:schemas-upnp-org:service:AVTransport:1" 32 | #define RENDERING_CTRL "urn:schemas-upnp-org:service:RenderingControl:1" 33 | #define CONNECTION_MGR "urn:schemas-upnp-org:service:ConnectionManager:1" 34 | #define MAGIC 0xAABBCCDD 35 | #define RESOURCE_LENGTH 250 36 | #define SCAN_TIMEOUT 15 37 | #define SCAN_INTERVAL 30 38 | 39 | enum eMRstate { STOPPED, PLAYING, PAUSED }; 40 | 41 | typedef struct sMRConfig 42 | { 43 | bool Enabled; 44 | bool StopReceiver; 45 | char Name[STR_LEN]; 46 | char Codec[STR_LEN]; 47 | bool Metadata; 48 | bool Flush; 49 | double MediaVolume; 50 | uint8_t mac[6]; 51 | char Latency[STR_LEN]; 52 | bool Drift; 53 | char ArtWork[4*STR_LEN]; 54 | } tMRConfig; 55 | 56 | struct sMR { 57 | uint32_t Magic; 58 | bool Running; 59 | tMRConfig Config; 60 | struct raopsr_s *Raop; 61 | raopsr_event_t RaopState; 62 | char UDN [RESOURCE_LENGTH]; 63 | char Name [STR_LEN]; 64 | enum eMRstate State; 65 | bool ExpectStop; 66 | uint32_t Elapsed; 67 | unsigned TrackPoll; 68 | void *CastCtx; 69 | pthread_mutex_t Mutex; 70 | pthread_t Thread; 71 | double Volume; 72 | uint32_t VolumeStampRx, VolumeStampTx; 73 | bool Group; 74 | struct sGroupMember { 75 | struct sGroupMember *Next; 76 | struct in_addr Host; 77 | uint16_t Port; 78 | } *GroupMaster; 79 | bool Remove; 80 | }; 81 | 82 | extern int32_t glLogLimit; 83 | extern tMRConfig glMRConfig; 84 | extern struct sMR *glMRDevices; 85 | extern int glMaxDevices; 86 | extern unsigned short glPortBase, glPortRange; 87 | extern char glBinding[16]; 88 | -------------------------------------------------------------------------------- /aircast/src/cast_parse.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast parse utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | 11 | #include 12 | 13 | #include "platform.h" 14 | #include "cross_log.h" 15 | #include "jansson.h" 16 | #include "cast_parse.h" 17 | 18 | extern log_level cast_loglevel; 19 | //static log_level *loglevel = &cast_loglevel; 20 | 21 | /*----------------------------------------------------------------------------*/ 22 | /* */ 23 | /* JSON parsing */ 24 | /* */ 25 | /*----------------------------------------------------------------------------*/ 26 | 27 | 28 | /*----------------------------------------------------------------------------*/ 29 | const char *GetAppIdItem(json_t *root, char* appId, char *item) 30 | { 31 | json_t *elm; 32 | 33 | if ((elm = json_object_get(root, "status")) == NULL) return NULL; 34 | if ((elm = json_object_get(elm, "applications")) == NULL) return NULL; 35 | for (int i = 0; i < json_array_size(elm); i++) { 36 | json_t *id, *data = json_array_get(elm, i); 37 | id = json_object_get(data, "appId"); 38 | if (strcasecmp(json_string_value(id), appId)) continue; 39 | id = json_object_get(data, item); 40 | return json_string_value(id); 41 | } 42 | 43 | return NULL; 44 | } 45 | 46 | 47 | /*----------------------------------------------------------------------------*/ 48 | int GetMediaItem_I(json_t *root, int n, char *item) 49 | { 50 | json_t *elm; 51 | 52 | if ((elm = json_object_get(root, "status")) == NULL) return 0; 53 | if ((elm = json_array_get(elm, n)) == NULL) return 0; 54 | if ((elm = json_object_get(elm, item)) == NULL) return 0; 55 | return json_integer_value(elm); 56 | } 57 | 58 | 59 | /*----------------------------------------------------------------------------*/ 60 | double GetMediaItem_F(json_t *root, int n, char *item) 61 | { 62 | json_t *elm; 63 | 64 | if ((elm = json_object_get(root, "status")) == NULL) return 0; 65 | if ((elm = json_array_get(elm, n)) == NULL) return 0; 66 | if ((elm = json_object_get(elm, item)) == NULL) return 0; 67 | return json_number_value(elm); 68 | } 69 | 70 | 71 | /*----------------------------------------------------------------------------*/ 72 | bool GetMediaVolume(json_t *root, int n, double *volume, bool *muted) 73 | { 74 | json_t *elm, *data; 75 | *volume = -1; 76 | *muted = false; 77 | 78 | if ((elm = json_object_get(root, "status")) == NULL) return false; 79 | if ((elm = json_object_get(elm, "volume")) == NULL) return false; 80 | 81 | if ((data = json_object_get(elm, "level")) != NULL) *volume = json_number_value(data); 82 | if ((data = json_object_get(elm, "muted")) != NULL) *muted = json_boolean_value(data); 83 | 84 | return true; 85 | } 86 | 87 | 88 | /*----------------------------------------------------------------------------*/ 89 | const char *GetMediaItem_S(json_t *root, int n, char *item) 90 | { 91 | json_t *elm; 92 | const char *str; 93 | 94 | if ((elm = json_object_get(root, "status")) == NULL) return NULL; 95 | elm = json_array_get(elm, n); 96 | elm = json_object_get(elm, item); 97 | str = json_string_value(elm); 98 | return str; 99 | } 100 | 101 | /*----------------------------------------------------------------------------*/ 102 | const char *GetMediaInfoItem_S(json_t *root, int n, char *item) 103 | { 104 | json_t *elm; 105 | const char *str; 106 | 107 | if ((elm = json_object_get(root, "status")) == NULL) return NULL; 108 | if ((elm = json_array_get(elm, n)) == NULL) return NULL; 109 | if ((elm = json_object_get(elm, "media")) == NULL) return NULL; 110 | elm = json_object_get(elm, item); 111 | str = json_string_value(elm); 112 | return str; 113 | } 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /aircast/src/cast_parse.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast parse utils 3 | * 4 | * (c) Philippe 2016-2017, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include "jansson.h" 14 | 15 | int GetMediaItem_I(json_t *root, int n, char *item); 16 | double GetMediaItem_F(json_t *root, int n, char *item); 17 | const char* GetMediaItem_S(json_t *root, int n, char *item); 18 | const char* GetAppIdItem(json_t *root, char* appId, char *item); 19 | const char* GetMediaInfoItem_S(json_t *root, int n, char *item); 20 | bool GetMediaVolume(json_t *root, int n, double *volume, bool *muted); 21 | 22 | 23 | -------------------------------------------------------------------------------- /aircast/src/cast_util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast misc utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | 12 | #include "platform.h" 13 | #include "metadata.h" 14 | #include "cross_log.h" 15 | #include "castcore.h" 16 | #include "cast_util.h" 17 | #include "castitf.h" 18 | 19 | extern log_level cast_loglevel; 20 | static log_level *loglevel = &cast_loglevel; 21 | 22 | /*----------------------------------------------------------------------------*/ 23 | static json_t* BuildMetaData(struct metadata_s* MetaData) { 24 | if (!MetaData) return NULL; 25 | 26 | json_t* json = json_pack("{si,ss,ss,ss,ss,si}", 27 | "metadataType", 3, 28 | "albumName", MetaData->album, "title", MetaData->title, 29 | "albumArtist", MetaData->artist, "artist", MetaData->artist, 30 | "trackNumber", MetaData->track); 31 | 32 | if (MetaData->artwork) { 33 | json_t* artwork = json_pack("{s[{ss}]}", "images", "url", MetaData->artwork); 34 | json_object_update(json, artwork); 35 | json_decref(artwork); 36 | } 37 | 38 | return json_pack("{so}", "metadata", json); 39 | } 40 | 41 | 42 | /*----------------------------------------------------------------------------*/ 43 | bool CastIsConnected(struct sCastCtx *Ctx) { 44 | if (!Ctx) return false; 45 | 46 | pthread_mutex_lock(&Ctx->Mutex); 47 | bool status = Ctx->Status >= CAST_CONNECTED; 48 | pthread_mutex_unlock(&Ctx->Mutex); 49 | return status; 50 | } 51 | 52 | /*----------------------------------------------------------------------------*/ 53 | bool CastIsMediaSession(struct sCastCtx *Ctx) { 54 | if (!Ctx) return false; 55 | 56 | pthread_mutex_lock(&Ctx->Mutex); 57 | bool status = Ctx->mediaSessionId != 0; 58 | pthread_mutex_unlock(&Ctx->Mutex); 59 | 60 | return status; 61 | } 62 | 63 | /*----------------------------------------------------------------------------*/ 64 | void CastGetStatus(struct sCastCtx* Ctx) { 65 | // SSL context might not be set yet 66 | if (!Ctx) return; 67 | 68 | pthread_mutex_lock(&Ctx->Mutex); 69 | 70 | json_t* msg = json_pack("{ss,si}", "type", "GET_STATUS", "requestId", Ctx->reqId++); 71 | 72 | char* str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 73 | json_decref(msg); 74 | 75 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, "%s", str); 76 | NFREE(str); 77 | 78 | pthread_mutex_unlock(&Ctx->Mutex); 79 | } 80 | 81 | /*----------------------------------------------------------------------------*/ 82 | void CastGetMediaStatus(struct sCastCtx *Ctx) { 83 | // SSL context might not be set yet 84 | if (!Ctx) return; 85 | 86 | pthread_mutex_lock(&Ctx->Mutex); 87 | 88 | if (Ctx->mediaSessionId) { 89 | json_t* msg = json_pack("{ss,si,si}", "type", "GET_STATUS", 90 | "mediaSessionId", Ctx->mediaSessionId, 91 | "requestId", Ctx->reqId++); 92 | 93 | char* str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 94 | json_decref(msg); 95 | 96 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, "%s", str); 97 | NFREE(str); 98 | } 99 | 100 | pthread_mutex_unlock(&Ctx->Mutex); 101 | } 102 | 103 | /*----------------------------------------------------------------------------*/ 104 | #define LOAD_FLUSH 105 | bool CastLoad(struct sCastCtx *Ctx, char *URI, char *ContentType, const char *Name, struct metadata_s *MetaData, uint64_t StartTime) { 106 | json_t *msg, *customData; 107 | char* str; 108 | 109 | if (!LaunchReceiver(Ctx)) { 110 | LOG_ERROR("[%p]: Cannot connect Cast receiver", Ctx->owner); 111 | return false; 112 | } 113 | 114 | msg = json_pack("{ss,ss,ss}", "contentId", URI, "streamType", (MetaData && !MetaData->duration) ? "LIVE" : "BUFFERED", 115 | "contentType", ContentType); 116 | 117 | if (MetaData && MetaData->duration) { 118 | json_t* duration = json_pack("{sf}", "duration", (double)MetaData->duration / 1000); 119 | json_object_update(msg, duration); 120 | json_decref(duration); 121 | } 122 | 123 | if (StartTime) customData = json_pack("{s{sssI}}", "customData", "deviceName", Name, "startTime", StartTime); 124 | else customData = json_pack("{s{ss}}", "customData", "deviceName", Name); 125 | json_object_update(msg, customData); 126 | json_decref(customData); 127 | 128 | json_t* jsonMetaData = BuildMetaData(MetaData); 129 | 130 | if (jsonMetaData) { 131 | json_object_update(msg, jsonMetaData); 132 | json_decref(jsonMetaData); 133 | } 134 | 135 | pthread_mutex_lock(&Ctx->Mutex); 136 | 137 | #ifdef LOAD_FLUSH 138 | if (Ctx->Status == CAST_LAUNCHED && (!Ctx->waitId || Ctx->waitMedia)) { 139 | 140 | /* 141 | For some reason a LOAD request is pending (maybe not enough data have 142 | been buffered yet, so LOAD has not been acknowledged, a stop might 143 | be stuck in the queue and the source does not send any more data, so 144 | this is a deadlock (see usage with iOS 10.x). Best is to have LOAD 145 | request flushing the queue then 146 | */ 147 | if (Ctx->waitMedia) CastQueueFlush(&Ctx->reqQueue); 148 | #else 149 | if (Ctx->Status == CAST_LAUNCHED && !Ctx->waitId) { 150 | #endif 151 | 152 | Ctx->waitId = Ctx->reqId++; 153 | Ctx->waitMedia = Ctx->waitId; 154 | Ctx->mediaSessionId = 0; 155 | 156 | msg = json_pack("{ss,si,ss,sf,sb,so}", "type", "LOAD", 157 | "requestId", Ctx->waitId, "sessionId", Ctx->sessionId, 158 | "currentTime", 0.0, "autoplay", 0, 159 | "media", msg); 160 | 161 | str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 162 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, "%s", str); 163 | json_decref(msg); 164 | NFREE(str); 165 | 166 | LOG_INFO("[%p]: Immediate LOAD (id:%u)", Ctx->owner, Ctx->waitId); 167 | } else { 168 | // otherwise queue it for later 169 | tReqItem *req = malloc(sizeof(tReqItem)); 170 | #ifndef LOAD_FLUSH 171 | // if waiting for a media, need to unlock queue and take precedence 172 | Ctx->waitMedia = 0; 173 | #endif 174 | strcpy(req->Type, "LOAD"); 175 | req->data.msg = msg; 176 | queue_insert(&Ctx->reqQueue, req); 177 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 178 | } 179 | 180 | pthread_mutex_unlock(&Ctx->Mutex); 181 | 182 | 183 | return true; 184 | } 185 | 186 | /*----------------------------------------------------------------------------*/ 187 | void CastSimple(struct sCastCtx *Ctx, char *Type) { 188 | // lock on wait for a Cast response 189 | pthread_mutex_lock(&Ctx->Mutex); 190 | 191 | if (Ctx->Status == CAST_LAUNCHED && !Ctx->waitId) { 192 | // no media session, nothing to do 193 | if (Ctx->mediaSessionId) { 194 | Ctx->waitId = Ctx->reqId++; 195 | 196 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, 197 | "{\"type\":\"%s\",\"requestId\":%d,\"mediaSessionId\":%d}", 198 | Type, Ctx->waitId, Ctx->mediaSessionId); 199 | 200 | LOG_INFO("[%p]: Immediate %s (id:%u)", Ctx->owner, Type, Ctx->waitId); 201 | 202 | } else { 203 | LOG_WARN("[%p]: %s req w/o a session", Ctx->owner, Type); 204 | } 205 | 206 | } else { 207 | tReqItem *req = malloc(sizeof(tReqItem)); 208 | strcpy(req->Type, Type); 209 | queue_insert(&Ctx->reqQueue, req); 210 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 211 | } 212 | 213 | pthread_mutex_unlock(&Ctx->Mutex); 214 | } 215 | 216 | /*----------------------------------------------------------------------------*/ 217 | void CastPlay(struct sCastCtx* Ctx, struct metadata_s* MetaData) { 218 | // lock on wait for a Cast response 219 | pthread_mutex_lock(&Ctx->Mutex); 220 | 221 | json_t* customData; 222 | if (MetaData && MetaData->live_duration != -1) customData = json_pack("{si}", "liveDuration", MetaData->live_duration); 223 | else customData = json_object(); 224 | 225 | json_t* item = BuildMetaData(MetaData); 226 | 227 | if (item) { 228 | json_object_update(customData, item); 229 | json_decref(item); 230 | } 231 | 232 | if (Ctx->Status == CAST_LAUNCHED && !Ctx->waitId) { 233 | // no media session, nothing to do 234 | if (Ctx->mediaSessionId) { 235 | Ctx->waitId = Ctx->reqId++; 236 | 237 | json_t* msg = json_pack("{ss,si,si}", "type", "PLAY", "requestId", Ctx->waitId, 238 | "mediaSessionId", Ctx->mediaSessionId); 239 | 240 | item = json_pack("{so}", "customData", customData); 241 | json_object_update(msg, item); 242 | json_decref(item); 243 | 244 | char* str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 245 | json_decref(msg); 246 | 247 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, "%s", str); 248 | NFREE(str); 249 | 250 | LOG_INFO("[%p]: Immediate PLAY (id:%u)", Ctx->owner, Ctx->waitId); 251 | 252 | } else { 253 | json_decref(customData); 254 | LOG_WARN("[%p]: PLAY req w/o a session", Ctx->owner); 255 | } 256 | 257 | } else { 258 | tReqItem* req = malloc(sizeof(tReqItem)); 259 | req->data.customData = customData; 260 | strcpy(req->Type, "PLAY"); 261 | queue_insert(&Ctx->reqQueue, req); 262 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 263 | } 264 | 265 | pthread_mutex_unlock(&Ctx->Mutex); 266 | } 267 | 268 | /*----------------------------------------------------------------------------*/ 269 | void CastStop(struct sCastCtx *Ctx) { 270 | // lock on wait for a Cast response 271 | pthread_mutex_lock(&Ctx->Mutex); 272 | 273 | CastQueueFlush(&Ctx->reqQueue); 274 | 275 | // if a session is active, stop can be sent-immediately 276 | if (Ctx->mediaSessionId) { 277 | 278 | Ctx->waitId = Ctx->reqId++; 279 | 280 | // version 1.24 281 | if (Ctx->stopReceiver) { 282 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 283 | "{\"type\":\"STOP\",\"requestId\":%d}", Ctx->waitId); 284 | Ctx->Status = CAST_CONNECTED; 285 | 286 | } else { 287 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, 288 | "{\"type\":\"STOP\",\"requestId\":%d,\"mediaSessionId\":%d}", 289 | Ctx->waitId, Ctx->mediaSessionId); 290 | } 291 | 292 | Ctx->mediaSessionId = 0; 293 | LOG_INFO("[%p]: Immediate STOP (id:%u)", Ctx->owner, Ctx->waitId); 294 | 295 | // waiting for a session, need to queue the stop 296 | } else if (Ctx->waitMedia) { 297 | 298 | tReqItem *req = malloc(sizeof(tReqItem)); 299 | strcpy(req->Type, "STOP"); 300 | queue_insert(&Ctx->reqQueue, req); 301 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 302 | 303 | // launching happening, just go back to CONNECT mode 304 | } else if (Ctx->Status == CAST_LAUNCHING) { 305 | Ctx->Status = CAST_CONNECTED; 306 | LOG_WARN("[%p]: Stop while still launching receiver", Ctx->owner); 307 | // a random stop 308 | } else { 309 | LOG_WARN("[%p]: Stop w/o session or connect", Ctx->owner); 310 | } 311 | 312 | pthread_mutex_unlock(&Ctx->Mutex); 313 | } 314 | 315 | /*----------------------------------------------------------------------------*/ 316 | void CastPowerOff(struct sCastCtx *Ctx) { 317 | CastRelease(Ctx); 318 | CastDisconnect(Ctx); 319 | } 320 | 321 | /*----------------------------------------------------------------------------*/ 322 | bool CastPowerOn(struct sCastCtx *Ctx) { 323 | return CastConnect(Ctx); 324 | } 325 | 326 | /*----------------------------------------------------------------------------*/ 327 | void CastRelease(struct sCastCtx *Ctx) { 328 | pthread_mutex_lock(&Ctx->Mutex); 329 | if (Ctx->Status != CAST_DISCONNECTED) { 330 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 331 | "{\"type\":\"STOP\",\"requestId\":%d}", Ctx->reqId++); 332 | Ctx->Status = CAST_CONNECTED; 333 | } 334 | pthread_mutex_unlock(&Ctx->Mutex); 335 | } 336 | 337 | 338 | /*----------------------------------------------------------------------------*/ 339 | void CastSetDeviceVolume(struct sCastCtx *Ctx, double Volume, bool Queue) { 340 | pthread_mutex_lock(&Ctx->Mutex); 341 | 342 | if (Volume > 1.0) Volume = 1.0; 343 | 344 | if (Ctx->Status == CAST_LAUNCHED && (!Ctx->waitId || !Queue)) { 345 | 346 | if (Volume) { 347 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 348 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"level\":%0.4lf}}", 349 | Ctx->reqId++, Volume); 350 | 351 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 352 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"muted\":false}}", 353 | Ctx->reqId); 354 | 355 | } else { 356 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 357 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"muted\":true}}", 358 | Ctx->reqId); 359 | } 360 | 361 | // Only set waitId if this is NOT queue bypass 362 | if (Queue) Ctx->waitId = Ctx->reqId; 363 | 364 | LOG_DEBUG("[%p]: Immediate VOLUME (id:%u)", Ctx->owner, Ctx->reqId); 365 | 366 | Ctx->reqId++; 367 | } else { 368 | // otherwise queue it for later 369 | tReqItem *req = malloc(sizeof(tReqItem)); 370 | strcpy(req->Type, "SET_VOLUME"); 371 | req->data.volume = Volume; 372 | queue_insert(&Ctx->reqQueue, req); 373 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 374 | } 375 | 376 | pthread_mutex_unlock(&Ctx->Mutex); 377 | } 378 | 379 | /*----------------------------------------------------------------------------*/ 380 | int CastSeek(char *ControlURL, unsigned Interval) { 381 | return 0; 382 | } 383 | 384 | -------------------------------------------------------------------------------- /aircast/src/cast_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast control utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | * 9 | */ 10 | 11 | # pragma once 12 | 13 | #include "metadata.h" 14 | 15 | typedef enum { CAST_PLAY, CAST_PAUSE, CAST_STOP } tCastAction; 16 | 17 | struct sq_metadata_s; 18 | struct sMRConfig; 19 | struct sCastCtx; 20 | 21 | void CastGetStatus(struct sCastCtx *Ctx); 22 | void CastGetMediaStatus(struct sCastCtx *Ctx); 23 | 24 | void CastPowerOff(struct sCastCtx *Ctx); 25 | bool CastPowerOn(struct sCastCtx *Ctx); 26 | void CastRelease(struct sCastCtx *Ctx); 27 | 28 | void CastStop(struct sCastCtx *Ctx); 29 | void CastPlay(struct sCastCtx* Ctx, struct metadata_s* MetaData); 30 | #define CastPause(Ctx) CastSimple(Ctx, "PAUSE") 31 | void CastSimple(struct sCastCtx *Ctx, char *Type); 32 | bool CastLoad(struct sCastCtx *Ctx, char *URI, char *ContentType, const char* Name, struct metadata_s *MetaData, uint64_t StartTime); 33 | void CastSetDeviceVolume(struct sCastCtx *p, double Volume, bool Queue); 34 | 35 | -------------------------------------------------------------------------------- /aircast/src/castcore.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast core protocol handler 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | #include 12 | 13 | #include "cross_log.h" 14 | #include "cross_net.h" 15 | #include "cross_thread.h" 16 | 17 | #include "cast_parse.h" 18 | #include "castcore.h" 19 | #include "castitf.h" 20 | 21 | #ifdef _WIN32 22 | #define bswap32(n) _byteswap_ulong((n)) 23 | #elif __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 24 | #define bswap32(n) __builtin_bswap32((n)) 25 | #else 26 | #define bswap32(n) (n) 27 | #endif 28 | 29 | #define SELECT_SOCKET 30 | 31 | /*----------------------------------------------------------------------------*/ 32 | /* locals */ 33 | /*----------------------------------------------------------------------------*/ 34 | static SSL_CTX *glSSLctx; 35 | static void *CastSocketThread(void *args); 36 | static void *CastPingThread(void *args); 37 | 38 | extern log_level cast_loglevel; 39 | static log_level *loglevel = &cast_loglevel; 40 | 41 | //#define DEFAULT_RECEIVER "CC1AD845" 42 | #define DEFAULT_RECEIVER "46C1A819" 43 | 44 | /*----------------------------------------------------------------------------*/ 45 | static void CastExit(void) { 46 | if (glSSLctx) SSL_CTX_free(glSSLctx); 47 | } 48 | 49 | /*----------------------------------------------------------------------------*/ 50 | static bool read_bytes(pthread_mutex_t *Mutex, SSL *ssl, void *buffer, uint16_t bytes) { 51 | uint16_t read = 0; 52 | sockfd sock = SSL_get_fd(ssl); 53 | 54 | if (sock == -1) return false; 55 | 56 | while (bytes - read) { 57 | int nb; 58 | #ifdef SELECT_SOCKET 59 | fd_set rfds; 60 | struct timeval timeout = { 0, 100000 }; 61 | FD_ZERO(&rfds); 62 | FD_SET(sock, &rfds); 63 | 64 | if (!SSL_pending(ssl)) { 65 | if (select(sock + 1, &rfds, NULL, NULL, &timeout) == -1) { 66 | LOG_WARN("[s-%p]: socket closed", ssl); 67 | return false; 68 | } 69 | 70 | if (!FD_ISSET(sock, &rfds)) continue; 71 | } 72 | #endif 73 | ERR_clear_error(); 74 | pthread_mutex_lock(Mutex); 75 | nb = SSL_read(ssl, (uint8_t*) buffer + read, bytes - read); 76 | pthread_mutex_unlock(Mutex); 77 | if (nb <= 0) { 78 | LOG_WARN("[s-%p]: SSL error code %d (err:%d)", ssl, SSL_get_error(ssl, nb), ERR_get_error()); 79 | return false; 80 | } 81 | read += nb; 82 | } 83 | 84 | return true; 85 | } 86 | 87 | /*----------------------------------------------------------------------------*/ 88 | static bool write_bytes(pthread_mutex_t *Mutex, SSL *ssl, void *buffer, uint16_t bytes) { 89 | pthread_mutex_lock(Mutex); 90 | bool ret = SSL_write(ssl, buffer, bytes) > 0; 91 | pthread_mutex_unlock(Mutex); 92 | 93 | return ret; 94 | } 95 | 96 | /*----------------------------------------------------------------------------*/ 97 | bool SendCastMessage(struct sCastCtx *Ctx, char *ns, char *dest, char *payload, ...) { 98 | CastMessage message = CastMessage_init_default; 99 | pb_ostream_t stream; 100 | uint8_t *buffer; 101 | uint16_t buffer_len = 4096; 102 | bool status; 103 | uint32_t len; 104 | va_list args; 105 | 106 | if (!Ctx->ssl) return false; 107 | 108 | va_start(args, payload); 109 | 110 | if (dest) strcpy(message.destination_id, dest); 111 | strcpy(message.namespace, ns); 112 | len = vsprintf(message.payload_utf8, payload, args); 113 | message.has_payload_utf8 = true; 114 | if ((buffer = malloc(buffer_len)) == NULL) return false; 115 | stream = pb_ostream_from_buffer(buffer, buffer_len); 116 | status = pb_encode(&stream, CastMessage_fields, &message); 117 | len = bswap32(stream.bytes_written); 118 | 119 | status &= write_bytes(&Ctx->sslMutex, Ctx->ssl, &len, 4); 120 | status &= write_bytes(&Ctx->sslMutex, Ctx->ssl, buffer, stream.bytes_written); 121 | 122 | free(buffer); 123 | 124 | if (!strcasestr(message.payload_utf8, "PING")) { 125 | LOG_DEBUG("[%p]: Cast sending: %s", Ctx->ssl, message.payload_utf8); 126 | } 127 | 128 | return status; 129 | } 130 | 131 | /*----------------------------------------------------------------------------*/ 132 | static bool DecodeCastMessage(uint8_t *buffer, uint16_t len, CastMessage *msg) { 133 | CastMessage message = CastMessage_init_zero; 134 | pb_istream_t stream = pb_istream_from_buffer(buffer, len); 135 | 136 | bool status = pb_decode(&stream, CastMessage_fields, &message); 137 | memcpy(msg, &message, sizeof(CastMessage)); 138 | return status; 139 | } 140 | 141 | /*----------------------------------------------------------------------------*/ 142 | static bool GetNextMessage(pthread_mutex_t *Mutex, SSL *ssl, CastMessage *message) { 143 | uint32_t len; 144 | uint8_t *buf; 145 | 146 | // the SSL might just have been closed by another thread 147 | if (!ssl || !read_bytes(Mutex, ssl, &len, 4)) return false; 148 | 149 | len = bswap32(len); 150 | if ((buf = malloc(len)) == NULL) return false; 151 | bool status = read_bytes(Mutex, ssl, buf, len); 152 | status &= DecodeCastMessage(buf, len, message); 153 | 154 | free(buf); 155 | return status; 156 | } 157 | 158 | /*----------------------------------------------------------------------------*/ 159 | json_t *GetTimedEvent(struct sCastCtx *Ctx, uint32_t msWait) { 160 | pthread_mutex_lock(&Ctx->eventMutex); 161 | pthread_cond_reltimedwait(&Ctx->eventCond, &Ctx->eventMutex, msWait); 162 | json_t* data = queue_extract(&Ctx->eventQueue); 163 | pthread_mutex_unlock(&Ctx->eventMutex); 164 | 165 | return data; 166 | } 167 | 168 | /*----------------------------------------------------------------------------*/ 169 | bool LaunchReceiver(tCastCtx *Ctx) { 170 | // try to reconnect if SSL connection is lost 171 | if (!CastConnect(Ctx)) { 172 | return false; 173 | } 174 | 175 | pthread_mutex_lock(&Ctx->Mutex); 176 | 177 | switch (Ctx->Status) { 178 | case CAST_LAUNCHED: 179 | break; 180 | case CAST_CONNECTING: 181 | Ctx->Status = CAST_AUTOLAUNCH; 182 | break; 183 | case CAST_CONNECTED: 184 | if (!Ctx->waitId) { 185 | Ctx->Status = CAST_LAUNCHING; 186 | Ctx->waitId = Ctx->reqId++; 187 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, "{\"type\":\"LAUNCH\",\"requestId\":%d,\"appId\":\"%s\"}", Ctx->waitId, DEFAULT_RECEIVER); 188 | LOG_INFO("[%p]: Launching receiver %d", Ctx->owner, Ctx->waitId); 189 | } else { 190 | tReqItem *req = malloc(sizeof(tReqItem)); 191 | strcpy(req->Type, "LAUNCH"); 192 | queue_insert(&Ctx->reqQueue, req); 193 | LOG_INFO("[%p]: Queuing %s", Ctx->owner, req->Type); 194 | } 195 | break; 196 | default: 197 | LOG_INFO("[%p]: unhandled state %d", Ctx->owner, Ctx->Status); 198 | break; 199 | } 200 | 201 | pthread_mutex_unlock(&Ctx->Mutex); 202 | 203 | return true; 204 | } 205 | 206 | /*----------------------------------------------------------------------------*/ 207 | bool CastConnect(struct sCastCtx *Ctx) { 208 | int err; 209 | struct sockaddr_in addr; 210 | 211 | pthread_mutex_lock(&Ctx->Mutex); 212 | 213 | if (Ctx->Status != CAST_DISCONNECTED) { 214 | pthread_mutex_unlock(&Ctx->Mutex); 215 | return true; 216 | } 217 | 218 | Ctx->sock = socket(AF_INET, SOCK_STREAM, 0); 219 | set_nonblock(Ctx->sock); 220 | set_nosigpipe(Ctx->sock); 221 | 222 | addr.sin_family = AF_INET; 223 | addr.sin_addr.s_addr = Ctx->ip.s_addr; 224 | addr.sin_port = htons(Ctx->port); 225 | 226 | err = tcp_connect_timeout(Ctx->sock, addr, 3*1000); 227 | 228 | if (err) { 229 | closesocket(Ctx->sock); 230 | LOG_ERROR("[%p]: Cannot open socket connection (%d)", Ctx->owner, err); 231 | pthread_mutex_unlock(&Ctx->Mutex); 232 | return false; 233 | } 234 | 235 | set_block(Ctx->sock); 236 | SSL_set_fd(Ctx->ssl, Ctx->sock); 237 | 238 | if (SSL_connect(Ctx->ssl)) { 239 | LOG_INFO("[%p]: SSL connection opened [%p]", Ctx->owner, Ctx->ssl); 240 | } 241 | else { 242 | err = SSL_get_error(Ctx->ssl,err); 243 | LOG_ERROR("[%p]: Cannot open SSL connection (%d)", Ctx->owner, err); 244 | closesocket(Ctx->sock); 245 | pthread_mutex_unlock(&Ctx->Mutex); 246 | return false; 247 | } 248 | 249 | Ctx->Status = CAST_CONNECTING; 250 | Ctx->lastPong = gettime_ms(); 251 | SendCastMessage(Ctx, CAST_CONNECTION, NULL, "{\"type\":\"CONNECT\"}"); 252 | pthread_mutex_unlock(&Ctx->Mutex); 253 | 254 | // wake up everybody who can be waiting 255 | crossthreads_wake(); 256 | 257 | return true; 258 | } 259 | 260 | /*----------------------------------------------------------------------------*/ 261 | void CastDisconnect(struct sCastCtx *Ctx) { 262 | pthread_mutex_lock(&Ctx->Mutex); 263 | 264 | // powered off already 265 | if (Ctx->Status == CAST_DISCONNECTED) { 266 | pthread_mutex_unlock(&Ctx->Mutex); 267 | return; 268 | } 269 | 270 | Ctx->reqId = 1; 271 | Ctx->waitId = Ctx->waitMedia = Ctx->mediaSessionId = 0; 272 | Ctx->Status = CAST_DISCONNECTED; 273 | NFREE(Ctx->sessionId); 274 | NFREE(Ctx->transportId); 275 | queue_flush(&Ctx->eventQueue); 276 | CastQueueFlush(&Ctx->reqQueue); 277 | 278 | SSL_shutdown(Ctx->ssl); 279 | SSL_clear(Ctx->ssl); 280 | closesocket(Ctx->sock); 281 | 282 | pthread_mutex_unlock(&Ctx->Mutex); 283 | } 284 | 285 | /*----------------------------------------------------------------------------*/ 286 | void SetMediaVolume(tCastCtx *Ctx, double Volume) { 287 | if (Volume > 1.0) Volume = 1.0; 288 | 289 | Ctx->waitId = Ctx->reqId++; 290 | 291 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, 292 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"mediaSessionId\":%d,\"volume\":{\"level\":%0.4lf,\"muted\":false}}", 293 | Ctx->waitId, Ctx->mediaSessionId, Volume); 294 | } 295 | 296 | /*----------------------------------------------------------------------------*/ 297 | void *CreateCastDevice(void *owner, bool group, bool stopReceiver, struct in_addr ip, uint16_t port, double MediaVolume) { 298 | tCastCtx *Ctx = malloc(sizeof(tCastCtx)); 299 | pthread_mutexattr_t mutexAttr; 300 | 301 | if (!glSSLctx) { 302 | const SSL_METHOD* method = SSLv23_client_method(); 303 | glSSLctx = SSL_CTX_new(method); 304 | SSL_CTX_set_options(glSSLctx, SSL_OP_NO_SSLv2); 305 | atexit(CastExit); 306 | } 307 | 308 | Ctx->reqId = 1; 309 | Ctx->waitId = Ctx->waitMedia = Ctx->mediaSessionId = 0; 310 | Ctx->sessionId = Ctx->transportId = NULL; 311 | Ctx->owner = owner; 312 | Ctx->Status = CAST_DISCONNECTED; 313 | Ctx->ip = ip; 314 | Ctx->port = port; 315 | Ctx->mediaVolume = MediaVolume; 316 | Ctx->group = group; 317 | Ctx->stopReceiver = stopReceiver; 318 | Ctx->ssl = SSL_new(glSSLctx); 319 | 320 | queue_init(&Ctx->eventQueue, false, NULL); 321 | queue_init(&Ctx->reqQueue, false, NULL); 322 | pthread_mutexattr_init(&mutexAttr); 323 | pthread_mutexattr_settype(&mutexAttr, PTHREAD_MUTEX_RECURSIVE); 324 | pthread_mutex_init(&Ctx->Mutex, &mutexAttr); 325 | pthread_mutexattr_destroy(&mutexAttr); 326 | pthread_mutex_init(&Ctx->eventMutex, 0); 327 | pthread_mutex_init(&Ctx->sslMutex, 0); 328 | pthread_cond_init(&Ctx->eventCond, 0); 329 | 330 | pthread_create(&Ctx->Thread, NULL, &CastSocketThread, Ctx); 331 | pthread_create(&Ctx->PingThread, NULL, &CastPingThread, Ctx); 332 | 333 | return Ctx; 334 | } 335 | 336 | /*----------------------------------------------------------------------------*/ 337 | bool UpdateCastDevice(struct sCastCtx *Ctx, struct in_addr ip, uint16_t port) { 338 | if (Ctx->port != port || Ctx->ip.s_addr != ip.s_addr) { 339 | LOG_INFO("[%p]: changed ip:port %s:%d", Ctx, inet_ntoa(ip), port); 340 | pthread_mutex_lock(&Ctx->Mutex); 341 | Ctx->ip = ip; 342 | Ctx->port = port; 343 | pthread_mutex_unlock(&Ctx->Mutex); 344 | CastDisconnect(Ctx); 345 | return true; 346 | } 347 | return false; 348 | } 349 | 350 | /*----------------------------------------------------------------------------*/ 351 | struct in_addr CastGetAddr(struct sCastCtx *Ctx) { 352 | return Ctx->ip; 353 | } 354 | 355 | /*----------------------------------------------------------------------------*/ 356 | void DeleteCastDevice(struct sCastCtx *Ctx) { 357 | pthread_mutex_lock(&Ctx->Mutex); 358 | Ctx->running = false; 359 | pthread_mutex_unlock(&Ctx->Mutex); 360 | 361 | CastDisconnect(Ctx); 362 | 363 | // wake up cast communication & ping threads 364 | crossthreads_wake(); 365 | 366 | pthread_join(Ctx->PingThread, NULL); 367 | pthread_join(Ctx->Thread, NULL); 368 | 369 | // wake-up threads locked on GetTimedEvent 370 | pthread_mutex_lock(&Ctx->eventMutex); 371 | pthread_cond_signal(&Ctx->eventCond); 372 | pthread_mutex_unlock(&Ctx->eventMutex); 373 | 374 | // cleanup mutexes & conds 375 | pthread_cond_destroy(&Ctx->eventCond); 376 | pthread_mutex_destroy(&Ctx->eventMutex); 377 | pthread_mutex_destroy(&Ctx->sslMutex); 378 | 379 | LOG_INFO("[%p]: Cast device stopped", Ctx->owner); 380 | SSL_free(Ctx->ssl); 381 | free(Ctx); 382 | } 383 | 384 | /*----------------------------------------------------------------------------*/ 385 | void CastQueueFlush(cross_queue_t *Queue) { 386 | tReqItem *item; 387 | 388 | while ((item = queue_extract(Queue)) != NULL) { 389 | if (!strcasecmp(item->Type,"LOAD")) json_decref(item->data.msg); 390 | else if (!strcasecmp(item->Type, "PLAY") && item->data.customData) json_decref(item->data.customData); 391 | free(item); 392 | } 393 | } 394 | 395 | /*----------------------------------------------------------------------------*/ 396 | static void ProcessQueue(tCastCtx *Ctx) { 397 | tReqItem *item; 398 | 399 | if ((item = queue_extract(&Ctx->reqQueue)) == NULL) return; 400 | 401 | if (!strcasecmp(item->Type, "LAUNCH")) { 402 | Ctx->waitId = Ctx->reqId++; 403 | Ctx->Status = CAST_LAUNCHING; 404 | 405 | LOG_INFO("[%p]: Launching receiver %d", Ctx->owner, Ctx->waitId); 406 | 407 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, "{\"type\":\"LAUNCH\",\"requestId\":%d,\"appId\":\"%s\"}", Ctx->waitId, DEFAULT_RECEIVER); 408 | } 409 | 410 | #if 0 411 | if (!strcasecmp(item->Type, "GET_MEDIA_STATUS") && Ctx->mediaSessionId) { 412 | Ctx->waitId = Ctx->reqId++; 413 | 414 | LOG_INFO("[%p]: Processing GET_MEDIA_STATUS (id:%u)", Ctx->owner, Ctx->waitId); 415 | 416 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, 417 | "{\"type\":\"GET_STATUS\",\"requestId\":%d,\"mediaSessionId\":%d}", 418 | Ctx->waitId, Ctx->mediaSessionId); 419 | } 420 | 421 | if (!strcasecmp(item->Type, "GET_STATUS")) { 422 | Ctx->waitId = Ctx->reqId++; 423 | 424 | LOG_INFO("[%p]: Processing GET_STATUS (id:%u)", Ctx->owner, Ctx->waitId); 425 | 426 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, "{\"type\":\"GET_STATUS\",\"requestId\":%d}", Ctx->waitId); 427 | } 428 | #endif 429 | 430 | if (!strcasecmp(item->Type, "SET_VOLUME")) { 431 | 432 | if (item->data.volume) { 433 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 434 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"level\":%0.4lf}}", 435 | Ctx->reqId++, item->data.volume); 436 | 437 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 438 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"muted\":false}}", 439 | Ctx->reqId); 440 | } else { 441 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 442 | "{\"type\":\"SET_VOLUME\",\"requestId\":%d,\"volume\":{\"muted\":true}}", 443 | Ctx->reqId); 444 | } 445 | 446 | Ctx->waitId = Ctx->reqId++; 447 | 448 | LOG_INFO("[%p]: Processing VOLUME (id:%u)", Ctx->owner, Ctx->waitId); 449 | } 450 | 451 | if (!strcasecmp(item->Type, "PLAY") || !strcasecmp(item->Type, "PAUSE")) { 452 | if (Ctx->mediaSessionId) { 453 | Ctx->waitId = Ctx->reqId++; 454 | 455 | LOG_INFO("[%p]: Processing %s (id:%u)", Ctx->owner, item->Type, Ctx->waitId); 456 | 457 | json_t* msg = json_pack("{ss,si,si}", "type", "PLAY", "requestId", Ctx->waitId, 458 | "mediaSessionId", Ctx->mediaSessionId); 459 | 460 | json_t* customData = json_pack("{so}", "customData", item->data.customData); 461 | json_object_update(msg, customData); 462 | json_decref(customData); 463 | 464 | char* str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 465 | json_decref(msg); 466 | 467 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, "%s", str); 468 | NFREE(str); 469 | } else { 470 | if (item->data.customData) json_decref(item->data.customData); 471 | LOG_WARN("[%p]: PLAY un-queued but no media session", Ctx->owner); 472 | } 473 | } 474 | 475 | if (!strcasecmp(item->Type, "LOAD")) { 476 | json_t *msg = item->data.msg; 477 | char *str; 478 | 479 | Ctx->waitId = Ctx->reqId++; 480 | Ctx->waitMedia = Ctx->waitId; 481 | Ctx->mediaSessionId = 0; 482 | 483 | LOG_INFO("[%p]: Processing LOAD (id:%u)", Ctx->owner, Ctx->waitId); 484 | 485 | msg = json_pack("{ss,si,ss,sf,sb,so}", "type", "LOAD", 486 | "requestId", Ctx->waitId, "sessionId", Ctx->sessionId, 487 | "currentTime", 0.0, "autoplay", 0, 488 | "media", msg); 489 | 490 | str = json_dumps(msg, JSON_ENCODE_ANY | JSON_INDENT(1)); 491 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, "%s", str); 492 | NFREE(str); 493 | 494 | json_decref(msg); 495 | } 496 | 497 | if (!strcasecmp(item->Type, "STOP")) { 498 | 499 | Ctx->waitId = Ctx->reqId++; 500 | 501 | // version 1.24 502 | if (Ctx->stopReceiver) { 503 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, 504 | "{\"type\":\"STOP\",\"requestId\":%d}", Ctx->waitId); 505 | Ctx->Status = CAST_CONNECTED; 506 | 507 | } else if (Ctx->mediaSessionId) { 508 | SendCastMessage(Ctx, CAST_MEDIA, Ctx->transportId, 509 | "{\"type\":\"STOP\",\"requestId\":%d,\"mediaSessionId\":%d}", 510 | Ctx->waitId, Ctx->mediaSessionId); 511 | } 512 | 513 | Ctx->mediaSessionId = 0; 514 | } 515 | 516 | free(item); 517 | } 518 | 519 | /*----------------------------------------------------------------------------*/ 520 | static void *CastPingThread(void *args) { 521 | tCastCtx *Ctx = (tCastCtx*) args; 522 | uint32_t last = gettime_ms(); 523 | 524 | Ctx->running = true; 525 | 526 | while (Ctx->running) { 527 | uint32_t now = gettime_ms(); 528 | 529 | if (now - last > 3000 && Ctx->Status != CAST_DISCONNECTED) { 530 | pthread_mutex_lock(&Ctx->Mutex); 531 | 532 | // ping SSL connection 533 | if (Ctx->ssl) { 534 | SendCastMessage(Ctx, CAST_BEAT, NULL, "{\"type\":\"PING\"}"); 535 | if (now - Ctx->lastPong > 15000) { 536 | LOG_INFO("[%p]: No response to ping", Ctx); 537 | CastDisconnect(Ctx); 538 | } 539 | } 540 | 541 | // then ping RECEIVER connection 542 | if (Ctx->Status == CAST_LAUNCHED) SendCastMessage(Ctx, CAST_BEAT, Ctx->transportId, "{\"type\":\"PING\"}"); 543 | 544 | pthread_mutex_unlock(&Ctx->Mutex); 545 | last = now; 546 | } 547 | 548 | crossthreads_sleep(1500); 549 | } 550 | 551 | // clear SSL error allocated memorry 552 | ERR_remove_thread_state(NULL); 553 | 554 | return NULL; 555 | } 556 | 557 | /*----------------------------------------------------------------------------*/ 558 | static void *CastSocketThread(void *args) { 559 | tCastCtx *Ctx = (tCastCtx*) args; 560 | CastMessage Message; 561 | json_t *root, *val; 562 | json_error_t error; 563 | 564 | Ctx->running = true; 565 | 566 | while (Ctx->running) { 567 | int requestId = 0; 568 | bool forward = true; 569 | const char *str = NULL; 570 | 571 | // allow "virtual" power off 572 | if (Ctx->Status == CAST_DISCONNECTED) { 573 | crossthreads_sleep(0); 574 | continue; 575 | } 576 | 577 | // this SSL access is not mutex protected, but it should be fine 578 | if (!GetNextMessage(&Ctx->sslMutex, Ctx->ssl, &Message)) { 579 | LOG_WARN("[%p]: SSL connection closed", Ctx); 580 | CastDisconnect(Ctx); 581 | continue; 582 | } 583 | 584 | root = json_loads(Message.payload_utf8, 0, &error); 585 | LOG_SDEBUG("[%p]: %s", Ctx->owner, json_dumps(root, JSON_ENCODE_ANY | JSON_INDENT(1))); 586 | 587 | val = json_object_get(root, "requestId"); 588 | if (json_is_integer(val)) requestId = json_integer_value(val); 589 | 590 | val = json_object_get(root, "type"); 591 | 592 | if (json_is_string(val)) { 593 | pthread_mutex_lock(&Ctx->Mutex); 594 | str = json_string_value(val); 595 | 596 | if (!strcasecmp(str, "MEDIA_STATUS")) { 597 | LOG_DEBUG("[%p]: type:%s (id:%d) %s", Ctx->owner, str, requestId, GetMediaItem_S(root, 0, "playerState")); 598 | } 599 | else if (strcasecmp(str, "PONG") || *loglevel == lSDEBUG) { 600 | LOG_DEBUG("[%p]: type:%s (id:%d)", Ctx->owner, str, requestId); 601 | } 602 | 603 | LOG_SDEBUG("(s:%s) (d:%s)\n%s", Message.source_id, Message.destination_id, Message.payload_utf8); 604 | 605 | if (!strcasecmp(str, "CLOSE")) { 606 | // Connection closed by peer 607 | Ctx->Status = CAST_CONNECTED; 608 | Ctx->waitId = 0; 609 | ProcessQueue(Ctx); 610 | // VERSION_1_24 611 | if (Ctx->stopReceiver) { 612 | json_decref(root); 613 | forward = false; 614 | } 615 | } else if (!strcasecmp(str,"PING")) { 616 | // respond to device ping 617 | SendCastMessage(Ctx, CAST_BEAT, Message.source_id, "{\"type\":\"PONG\"}"); 618 | json_decref(root); 619 | forward = false; 620 | } else if (!strcasecmp(str,"PONG")) { 621 | // receiving pong 622 | Ctx->lastPong = gettime_ms(); 623 | // connection established, start receiver was requested 624 | if (Ctx->Status == CAST_AUTOLAUNCH) { 625 | Ctx->Status = CAST_LAUNCHING; 626 | Ctx->waitId = Ctx->reqId++; 627 | SendCastMessage(Ctx, CAST_RECEIVER, NULL, "{\"type\":\"LAUNCH\",\"requestId\":%d,\"appId\":\"%s\"}", Ctx->waitId, DEFAULT_RECEIVER); 628 | LOG_INFO("[%p]: Launching receiver %d", Ctx->owner, Ctx->waitId); 629 | } else if (Ctx->Status == CAST_CONNECTING) Ctx->Status = CAST_CONNECTED; 630 | 631 | json_decref(root); 632 | forward = false; 633 | } 634 | 635 | LOG_SDEBUG("[%p]: recvID %u (waitID %u)", Ctx, requestId, Ctx->waitId); 636 | 637 | // expected request acknowledge (we know that str is still valid) 638 | if (Ctx->waitId && Ctx->waitId == requestId) { 639 | 640 | // reset waitId, might be set below 641 | Ctx->waitId = 0; 642 | 643 | if (!strcasecmp(str,"RECEIVER_STATUS") && Ctx->Status == CAST_LAUNCHING) { 644 | // receiver status before connection is fully established 645 | const char *str; 646 | 647 | NFREE(Ctx->sessionId); 648 | str = GetAppIdItem(root, DEFAULT_RECEIVER, "sessionId"); 649 | if (str) Ctx->sessionId = strdup(str); 650 | NFREE(Ctx->transportId); 651 | str = GetAppIdItem(root, DEFAULT_RECEIVER, "transportId"); 652 | if (str) Ctx->transportId = strdup(str); 653 | 654 | if (Ctx->sessionId && Ctx->transportId) { 655 | Ctx->Status = CAST_LAUNCHED; 656 | LOG_INFO("[%p]: Receiver launched", Ctx->owner); 657 | SendCastMessage(Ctx, CAST_CONNECTION, Ctx->transportId, 658 | "{\"type\":\"CONNECT\",\"origin\":{}}"); 659 | } 660 | 661 | json_decref(root); 662 | forward = false; 663 | } else if (!strcasecmp(str,"MEDIA_STATUS") && Ctx->waitMedia == requestId) { 664 | // media status only acquired for expected id 665 | int id = GetMediaItem_I(root, 0, "mediaSessionId"); 666 | 667 | if (id) { 668 | Ctx->waitMedia = 0; 669 | Ctx->mediaSessionId = id; 670 | LOG_INFO("[%p]: Media session id %d", Ctx->owner, Ctx->mediaSessionId); 671 | // set media volume when session is re-connected 672 | SetMediaVolume(Ctx, Ctx->mediaVolume); 673 | } else { 674 | LOG_ERROR("[%p]: waitMedia match but no session %u", Ctx->owner, Ctx->waitMedia); 675 | } 676 | 677 | // Don't need to forward this, no valuable info 678 | json_decref(root); 679 | forward = false; 680 | } 681 | 682 | // must be done at the end, once all parameters have been acquired 683 | if (!Ctx->waitId && Ctx->Status == CAST_LAUNCHED) ProcessQueue(Ctx); 684 | } 685 | 686 | pthread_mutex_unlock(&Ctx->Mutex); 687 | } 688 | 689 | // queue event and signal handler 690 | if (forward) { 691 | pthread_mutex_lock(&Ctx->eventMutex); 692 | queue_insert(&Ctx->eventQueue, root); 693 | pthread_cond_signal(&Ctx->eventCond); 694 | pthread_mutex_unlock(&Ctx->eventMutex); 695 | } 696 | } 697 | 698 | // clear SSL error allocated memorry 699 | ERR_remove_thread_state(NULL); 700 | 701 | return NULL; 702 | } 703 | -------------------------------------------------------------------------------- /aircast/src/castcore.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast core protocol handler 3 | * 4 | * (c) Philippe 2016-2017, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "openssl/crypto.h" 18 | #include "openssl/x509.h" 19 | #include "openssl/pem.h" 20 | #include "openssl/ssl.h" 21 | #include "openssl/err.h" 22 | 23 | #include "cross_util.h" 24 | #include 25 | #include 26 | #include "jansson.h" 27 | #include "castmessage.pb.h" 28 | 29 | #define CAST_BEAT "urn:x-cast:com.google.cast.tp.heartbeat" 30 | #define CAST_RECEIVER "urn:x-cast:com.google.cast.receiver" 31 | #define CAST_CONNECTION "urn:x-cast:com.google.cast.tp.connection" 32 | #define CAST_MEDIA "urn:x-cast:com.google.cast.media" 33 | 34 | typedef int sockfd; 35 | 36 | typedef struct sCastCtx { 37 | bool running; 38 | enum { CAST_DISCONNECTED, CAST_CONNECTING, CAST_CONNECTED, CAST_AUTOLAUNCH, CAST_LAUNCHING, CAST_LAUNCHED } Status; 39 | void *owner; 40 | SSL *ssl; 41 | sockfd sock; 42 | int reqId, waitId, waitMedia; 43 | pthread_t Thread, PingThread; 44 | pthread_mutex_t Mutex, eventMutex, sslMutex; 45 | pthread_cond_t eventCond; 46 | char *sessionId, *transportId; 47 | int mediaSessionId; 48 | enum { CAST_WAIT, CAST_WAIT_MEDIA } State; 49 | struct in_addr ip; 50 | uint16_t port; 51 | cross_queue_t eventQueue, reqQueue; 52 | double mediaVolume; 53 | uint32_t lastPong; 54 | bool group; 55 | bool stopReceiver; 56 | } tCastCtx; 57 | 58 | typedef struct { 59 | char Type[32] ; 60 | union { 61 | json_t* msg; 62 | json_t* customData; 63 | double volume; 64 | } data; 65 | } tReqItem; 66 | 67 | bool SendCastMessage(struct sCastCtx *Ctx, char *ns, char *dest, char *payload, ...); 68 | bool LaunchReceiver(tCastCtx *Ctx); 69 | void SetVolume(tCastCtx *Ctx, double Volume); 70 | void CastQueueFlush(cross_queue_t *Queue); 71 | bool CastConnect(struct sCastCtx *Ctx); 72 | void CastDisconnect(struct sCastCtx *Ctx); 73 | 74 | -------------------------------------------------------------------------------- /aircast/src/castitf.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Chromecast internal interface 3 | * 4 | * (c) Philippe 2016-2017, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | 14 | #include "jansson.h" 15 | 16 | struct sCastCtx; 17 | 18 | json_t* GetTimedEvent(struct sCastCtx *Ctx, uint32_t msWait); 19 | void* CreateCastDevice(void *owner, bool group, bool stopReceiver, struct in_addr ip, uint16_t port, double MediaVolume); 20 | bool UpdateCastDevice(struct sCastCtx *Ctx, struct in_addr ip, uint16_t port); 21 | void DeleteCastDevice(struct sCastCtx *Ctx); 22 | bool CastIsConnected(struct sCastCtx *Ctx); 23 | bool CastIsMediaSession(struct sCastCtx *Ctx); 24 | struct in_addr CastGetAddr(struct sCastCtx *Ctx); 25 | 26 | -------------------------------------------------------------------------------- /aircast/src/castmessage.pb.c: -------------------------------------------------------------------------------- 1 | /* Automatically generated nanopb constant definitions */ 2 | /* Generated by nanopb-0.4.6-dev */ 3 | 4 | #include "castmessage.pb.h" 5 | #if PB_PROTO_HEADER_VERSION != 40 6 | #error Regenerate this file with the current version of nanopb generator. 7 | #endif 8 | 9 | PB_BIND(CastMessage, CastMessage, 2) 10 | 11 | 12 | PB_BIND(AuthChallenge, AuthChallenge, AUTO) 13 | 14 | 15 | PB_BIND(AuthResponse, AuthResponse, AUTO) 16 | 17 | 18 | PB_BIND(AuthError, AuthError, AUTO) 19 | 20 | 21 | PB_BIND(DeviceAuthMessage, DeviceAuthMessage, AUTO) 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /aircast/src/castmessage.pb.h: -------------------------------------------------------------------------------- 1 | /* Automatically generated nanopb header */ 2 | /* Generated by nanopb-0.4.6-dev */ 3 | 4 | #ifndef PB_CASTMESSAGE_PB_H_INCLUDED 5 | #define PB_CASTMESSAGE_PB_H_INCLUDED 6 | #include 7 | 8 | #if PB_PROTO_HEADER_VERSION != 40 9 | #error Regenerate this file with the current version of nanopb generator. 10 | #endif 11 | 12 | /* Enum definitions */ 13 | typedef enum _CastMessage_ProtocolVersion { 14 | CastMessage_ProtocolVersion_CASTV2_1_0 = 0 15 | } CastMessage_ProtocolVersion; 16 | 17 | typedef enum _CastMessage_PayloadType { 18 | CastMessage_PayloadType_STRING = 0, 19 | CastMessage_PayloadType_BINARY = 1 20 | } CastMessage_PayloadType; 21 | 22 | typedef enum _AuthError_ErrorType { 23 | AuthError_ErrorType_INTERNAL_ERROR = 0, 24 | AuthError_ErrorType_NO_TLS = 1 25 | } AuthError_ErrorType; 26 | 27 | /* Struct definitions */ 28 | /* Messages for authentication protocol between a sender and a receiver. */ 29 | typedef struct _AuthChallenge { 30 | char dummy_field; 31 | } AuthChallenge; 32 | 33 | typedef struct _AuthResponse { 34 | pb_callback_t signature; 35 | pb_callback_t client_auth_certificate; 36 | pb_callback_t client_ca; 37 | } AuthResponse; 38 | 39 | typedef struct _AuthError { 40 | AuthError_ErrorType error_type; 41 | } AuthError; 42 | 43 | typedef struct _CastMessage { 44 | CastMessage_ProtocolVersion protocol_version; 45 | /* source and destination ids identify the origin and destination of the 46 | message. They are used to route messages between endpoints that share a 47 | device-to-device channel. 48 | 49 | For messages between applications: 50 | - The sender application id is a unique identifier generated on behalf of 51 | the sender application. 52 | - The receiver id is always the the session id for the application. 53 | 54 | For messages to or from the sender or receiver platform, the special ids 55 | 'sender-0' and 'receiver-0' can be used. 56 | 57 | For messages intended for all endpoints using a given channel, the 58 | wildcard destination_id '*' can be used. */ 59 | char source_id[128]; 60 | char destination_id[128]; 61 | /* This is the core multiplexing key. All messages are sent on a namespace 62 | and endpoints sharing a channel listen on one or more namespaces. The 63 | namespace defines the protocol and semantics of the message. */ 64 | char namespace[128]; 65 | CastMessage_PayloadType payload_type; 66 | /* Depending on payload_type, exactly one of the following optional fields 67 | will always be set. */ 68 | bool has_payload_utf8; 69 | char payload_utf8[2048]; 70 | pb_callback_t payload_binary; 71 | } CastMessage; 72 | 73 | typedef struct _DeviceAuthMessage { 74 | /* Request fields */ 75 | bool has_challenge; 76 | AuthChallenge challenge; 77 | /* Response fields */ 78 | bool has_response; 79 | AuthResponse response; 80 | bool has_error; 81 | AuthError error; 82 | } DeviceAuthMessage; 83 | 84 | 85 | /* Helper constants for enums */ 86 | #define _CastMessage_ProtocolVersion_MIN CastMessage_ProtocolVersion_CASTV2_1_0 87 | #define _CastMessage_ProtocolVersion_MAX CastMessage_ProtocolVersion_CASTV2_1_0 88 | #define _CastMessage_ProtocolVersion_ARRAYSIZE ((CastMessage_ProtocolVersion)(CastMessage_ProtocolVersion_CASTV2_1_0+1)) 89 | 90 | #define _CastMessage_PayloadType_MIN CastMessage_PayloadType_STRING 91 | #define _CastMessage_PayloadType_MAX CastMessage_PayloadType_BINARY 92 | #define _CastMessage_PayloadType_ARRAYSIZE ((CastMessage_PayloadType)(CastMessage_PayloadType_BINARY+1)) 93 | 94 | #define _AuthError_ErrorType_MIN AuthError_ErrorType_INTERNAL_ERROR 95 | #define _AuthError_ErrorType_MAX AuthError_ErrorType_NO_TLS 96 | #define _AuthError_ErrorType_ARRAYSIZE ((AuthError_ErrorType)(AuthError_ErrorType_NO_TLS+1)) 97 | 98 | 99 | #ifdef __cplusplus 100 | extern "C" { 101 | #endif 102 | 103 | /* Initializer values for message structs */ 104 | #define CastMessage_init_default {CastMessage_ProtocolVersion_CASTV2_1_0, "sender-0", "receiver-0", "", CastMessage_PayloadType_STRING, false, "", {{NULL}, NULL}} 105 | #define AuthChallenge_init_default {0} 106 | #define AuthResponse_init_default {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} 107 | #define AuthError_init_default {_AuthError_ErrorType_MIN} 108 | #define DeviceAuthMessage_init_default {false, AuthChallenge_init_default, false, AuthResponse_init_default, false, AuthError_init_default} 109 | #define CastMessage_init_zero {_CastMessage_ProtocolVersion_MIN, "", "", "", _CastMessage_PayloadType_MIN, false, "", {{NULL}, NULL}} 110 | #define AuthChallenge_init_zero {0} 111 | #define AuthResponse_init_zero {{{NULL}, NULL}, {{NULL}, NULL}, {{NULL}, NULL}} 112 | #define AuthError_init_zero {_AuthError_ErrorType_MIN} 113 | #define DeviceAuthMessage_init_zero {false, AuthChallenge_init_zero, false, AuthResponse_init_zero, false, AuthError_init_zero} 114 | 115 | /* Field tags (for use in manual encoding/decoding) */ 116 | #define AuthResponse_signature_tag 1 117 | #define AuthResponse_client_auth_certificate_tag 2 118 | #define AuthResponse_client_ca_tag 3 119 | #define AuthError_error_type_tag 1 120 | #define CastMessage_protocol_version_tag 1 121 | #define CastMessage_source_id_tag 2 122 | #define CastMessage_destination_id_tag 3 123 | #define CastMessage_namespace_tag 4 124 | #define CastMessage_payload_type_tag 5 125 | #define CastMessage_payload_utf8_tag 6 126 | #define CastMessage_payload_binary_tag 7 127 | #define DeviceAuthMessage_challenge_tag 1 128 | #define DeviceAuthMessage_response_tag 2 129 | #define DeviceAuthMessage_error_tag 3 130 | 131 | /* Struct field encoding specification for nanopb */ 132 | #define CastMessage_FIELDLIST(X, a) \ 133 | X(a, STATIC, REQUIRED, UENUM, protocol_version, 1) \ 134 | X(a, STATIC, REQUIRED, STRING, source_id, 2) \ 135 | X(a, STATIC, REQUIRED, STRING, destination_id, 3) \ 136 | X(a, STATIC, REQUIRED, STRING, namespace, 4) \ 137 | X(a, STATIC, REQUIRED, UENUM, payload_type, 5) \ 138 | X(a, STATIC, OPTIONAL, STRING, payload_utf8, 6) \ 139 | X(a, CALLBACK, OPTIONAL, BYTES, payload_binary, 7) 140 | #define CastMessage_CALLBACK pb_default_field_callback 141 | #define CastMessage_DEFAULT (const pb_byte_t*)"\x12\x08\x73\x65\x6e\x64\x65\x72\x2d\x30\x1a\x0a\x72\x65\x63\x65\x69\x76\x65\x72\x2d\x30\x00" 142 | 143 | #define AuthChallenge_FIELDLIST(X, a) \ 144 | 145 | #define AuthChallenge_CALLBACK NULL 146 | #define AuthChallenge_DEFAULT NULL 147 | 148 | #define AuthResponse_FIELDLIST(X, a) \ 149 | X(a, CALLBACK, REQUIRED, BYTES, signature, 1) \ 150 | X(a, CALLBACK, REQUIRED, BYTES, client_auth_certificate, 2) \ 151 | X(a, CALLBACK, REPEATED, BYTES, client_ca, 3) 152 | #define AuthResponse_CALLBACK pb_default_field_callback 153 | #define AuthResponse_DEFAULT NULL 154 | 155 | #define AuthError_FIELDLIST(X, a) \ 156 | X(a, STATIC, REQUIRED, UENUM, error_type, 1) 157 | #define AuthError_CALLBACK NULL 158 | #define AuthError_DEFAULT NULL 159 | 160 | #define DeviceAuthMessage_FIELDLIST(X, a) \ 161 | X(a, STATIC, OPTIONAL, MESSAGE, challenge, 1) \ 162 | X(a, STATIC, OPTIONAL, MESSAGE, response, 2) \ 163 | X(a, STATIC, OPTIONAL, MESSAGE, error, 3) 164 | #define DeviceAuthMessage_CALLBACK NULL 165 | #define DeviceAuthMessage_DEFAULT NULL 166 | #define DeviceAuthMessage_challenge_MSGTYPE AuthChallenge 167 | #define DeviceAuthMessage_response_MSGTYPE AuthResponse 168 | #define DeviceAuthMessage_error_MSGTYPE AuthError 169 | 170 | extern const pb_msgdesc_t CastMessage_msg; 171 | extern const pb_msgdesc_t AuthChallenge_msg; 172 | extern const pb_msgdesc_t AuthResponse_msg; 173 | extern const pb_msgdesc_t AuthError_msg; 174 | extern const pb_msgdesc_t DeviceAuthMessage_msg; 175 | 176 | /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ 177 | #define CastMessage_fields &CastMessage_msg 178 | #define AuthChallenge_fields &AuthChallenge_msg 179 | #define AuthResponse_fields &AuthResponse_msg 180 | #define AuthError_fields &AuthError_msg 181 | #define DeviceAuthMessage_fields &DeviceAuthMessage_msg 182 | 183 | /* Maximum encoded size of messages (where known) */ 184 | /* CastMessage_size depends on runtime parameters */ 185 | /* AuthResponse_size depends on runtime parameters */ 186 | /* DeviceAuthMessage_size depends on runtime parameters */ 187 | #define AuthChallenge_size 0 188 | #define AuthError_size 2 189 | 190 | #ifdef __cplusplus 191 | } /* extern "C" */ 192 | #endif 193 | 194 | #endif 195 | -------------------------------------------------------------------------------- /aircast/src/config_cast.c: -------------------------------------------------------------------------------- 1 | /* 2 | * AirCast: config management 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "platform.h" 15 | #include "cross_log.h" 16 | #include "ixmlextra.h" 17 | #include "aircast.h" 18 | #include "config_cast.h" 19 | 20 | /*----------------------------------------------------------------------------*/ 21 | /* locals */ 22 | /*----------------------------------------------------------------------------*/ 23 | 24 | extern log_level main_loglevel; 25 | extern log_level util_loglevel; 26 | extern log_level cast_loglevel; 27 | 28 | /*----------------------------------------------------------------------------*/ 29 | void SaveConfig(char *name, void *ref, bool full) { 30 | struct sMR *p; 31 | IXML_Document *doc = ixmlDocument_createDocument(); 32 | IXML_Document *old_doc = ref; 33 | IXML_Node *root, *common; 34 | 35 | IXML_Element* old_root = ixmlDocument_getElementById(old_doc, "aircast"); 36 | 37 | if (!full && old_doc) { 38 | ixmlDocument_importNode(doc, (IXML_Node*) old_root, true, &root); 39 | ixmlNode_appendChild((IXML_Node*) doc, root); 40 | 41 | IXML_NodeList* list = ixmlDocument_getElementsByTagName((IXML_Document*) root, "device"); 42 | for (int i = 0; i < (int) ixmlNodeList_length(list); i++) { 43 | IXML_Node *device = ixmlNodeList_item(list, i); 44 | ixmlNode_removeChild(root, device, &device); 45 | ixmlNode_free(device); 46 | } 47 | if (list) ixmlNodeList_free(list); 48 | common = (IXML_Node*) ixmlDocument_getElementById((IXML_Document*) root, "common"); 49 | } 50 | else { 51 | root = XMLAddNode(doc, NULL, "aircast", NULL); 52 | common = (IXML_Node*) XMLAddNode(doc, root, "common", NULL); 53 | } 54 | 55 | XMLUpdateNode(doc, root, false, "main_log", level2debug(main_loglevel)); 56 | XMLUpdateNode(doc, root, false, "cast_log", level2debug(cast_loglevel)); 57 | XMLUpdateNode(doc, root, false, "util_log", level2debug(util_loglevel)); 58 | XMLUpdateNode(doc, root, false, "log_limit", "%d", (int32_t) glLogLimit); 59 | XMLUpdateNode(doc, root, false, "max_players", "%d", (int) glMaxDevices); 60 | XMLUpdateNode(doc, root, false, "ports", "%hu:%hu", glPortBase, glPortRange); 61 | XMLUpdateNode(doc, root, false, "binding", glBinding); 62 | 63 | XMLUpdateNode(doc, common, false, "enabled", "%d", (int) glMRConfig.Enabled); 64 | XMLUpdateNode(doc, common, false, "stop_receiver", "%d", (int) glMRConfig.StopReceiver); 65 | XMLUpdateNode(doc, common, false, "media_volume", "%0.4lf", glMRConfig.MediaVolume); 66 | XMLUpdateNode(doc, common, false, "latency", glMRConfig.Latency); 67 | XMLUpdateNode(doc, common, false, "drift", "%d", glMRConfig.Drift); 68 | XMLUpdateNode(doc, common, false, "codec", glMRConfig.Codec); 69 | XMLUpdateNode(doc, common, false, "metadata", "%d", glMRConfig.Metadata); 70 | XMLUpdateNode(doc, common, false, "flush", "%d", glMRConfig.Flush); 71 | XMLUpdateNode(doc, common, false, "artwork", "%s", glMRConfig.ArtWork); 72 | 73 | for (int i = 0; i < glMaxDevices; i++) { 74 | IXML_Node *dev_node; 75 | 76 | if (!glMRDevices[i].Running) continue; 77 | else p = &glMRDevices[i]; 78 | 79 | // new device, add nodes 80 | if (!old_doc || !FindMRConfig(old_doc, p->UDN)) { 81 | dev_node = XMLAddNode(doc, root, "device", NULL); 82 | XMLAddNode(doc, dev_node, "udn", p->UDN); 83 | XMLAddNode(doc, dev_node, "name", p->Config.Name); 84 | XMLAddNode(doc, dev_node, "mac", "%02x:%02x:%02x:%02x:%02x:%02x", p->Config.mac[0], 85 | p->Config.mac[1], p->Config.mac[2], p->Config.mac[3], p->Config.mac[4], p->Config.mac[5]); 86 | XMLAddNode(doc, dev_node, "enabled", "%d", (int) p->Config.Enabled); 87 | } 88 | } 89 | 90 | // add devices in old XML file that has not been discovered 91 | IXML_NodeList* list = ixmlDocument_getElementsByTagName((IXML_Document*) old_root, "device"); 92 | for (int i = 0; i < (int) ixmlNodeList_length(list); i++) { 93 | char *udn; 94 | 95 | IXML_Node* device = ixmlNodeList_item(list, i); 96 | IXML_Node* node = (IXML_Node*) ixmlDocument_getElementById((IXML_Document*) device, "udn"); 97 | node = ixmlNode_getFirstChild(node); 98 | udn = (char*) ixmlNode_getNodeValue(node); 99 | if (!FindMRConfig(doc, udn)) { 100 | ixmlDocument_importNode(doc, device, true, &device); 101 | ixmlNode_appendChild((IXML_Node*) root, device); 102 | } 103 | } 104 | if (list) ixmlNodeList_free(list); 105 | 106 | FILE* file = fopen(name, "wb"); 107 | char* s = ixmlDocumenttoString(doc); 108 | fwrite(s, 1, strlen(s), file); 109 | fclose(file); 110 | free(s); 111 | 112 | ixmlDocument_free(doc); 113 | } 114 | 115 | 116 | /*----------------------------------------------------------------------------*/ 117 | static void LoadConfigItem(tMRConfig *Conf, char *name, char *val) { 118 | if (!val) return; 119 | 120 | if (!strcmp(name, "enabled")) Conf->Enabled = atol(val); 121 | if (!strcmp(name, "stop_receiver")) Conf->StopReceiver = atol(val); 122 | if (!strcmp(name, "media_volume")) Conf->MediaVolume = atof(val); 123 | if (!strcmp(name, "codec")) strcpy(Conf->Codec, val); 124 | if (!strcmp(name, "metadata")) Conf->Metadata = atoi(val); 125 | if (!strcmp(name, "flush")) Conf->Flush = atoi(val); 126 | if (!strcmp(name, "artwork")) strcpy(Conf->ArtWork, val); 127 | if (!strcmp(name, "latency")) strcpy(Conf->Latency, val); 128 | if (!strcmp(name, "drift")) Conf->Drift = atoi(val); 129 | if (!strcmp(name, "name")) strcpy(Conf->Name, val); 130 | if (!strcmp(name, "mac")) { 131 | unsigned mac[6]; 132 | // seems to be a Windows scanf buf, cannot support %hhx 133 | sscanf(val,"%2x:%2x:%2x:%2x:%2x:%2x", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]); 134 | for (int i = 0; i < 6; i++) Conf->mac[i] = mac[i]; 135 | } 136 | } 137 | 138 | /*----------------------------------------------------------------------------*/ 139 | static void LoadGlobalItem(char *name, char *val) { 140 | if (!val) return; 141 | 142 | if (!strcmp(name, "main_log")) main_loglevel = debug2level(val); 143 | if (!strcmp(name, "cast_log")) cast_loglevel = debug2level(val); 144 | if (!strcmp(name, "util_log")) util_loglevel = debug2level(val); 145 | if (!strcmp(name, "log_limit")) glLogLimit = atol(val); 146 | if (!strcmp(name, "max_players")) glMaxDevices = atol(val); 147 | if (!strcmp(name, "ports")) sscanf(val, "%hu:%hu", &glPortBase, &glPortRange); 148 | if (!strcmp(name, "binding")) strcpy(glBinding, val); 149 | } 150 | 151 | 152 | /*----------------------------------------------------------------------------*/ 153 | void *FindMRConfig(void *ref, char *UDN) { 154 | IXML_Node* device = NULL; 155 | IXML_Document* doc = (IXML_Document*) ref; 156 | 157 | IXML_Element* elm = ixmlDocument_getElementById(doc, "aircast"); 158 | IXML_NodeList* l1_node_list = ixmlDocument_getElementsByTagName((IXML_Document*) elm, "udn"); 159 | for (int i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 160 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 161 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 162 | char* v = (char*) ixmlNode_getNodeValue(l1_1_node); 163 | if (v && !strcmp(v, UDN)) { 164 | device = ixmlNode_getParentNode(l1_node); 165 | break; 166 | } 167 | } 168 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 169 | return device; 170 | } 171 | 172 | /*----------------------------------------------------------------------------*/ 173 | void *LoadMRConfig(void *ref, char *UDN, tMRConfig *Conf) { 174 | IXML_Document *doc = (IXML_Document*) ref; 175 | IXML_Node* node = (IXML_Node*) FindMRConfig(doc, UDN); 176 | 177 | if (node) { 178 | IXML_NodeList* node_list = ixmlNode_getChildNodes(node); 179 | for (unsigned i = 0; i < ixmlNodeList_length(node_list); i++) { 180 | IXML_Node* l1_node = ixmlNodeList_item(node_list, i); 181 | char* n = (char*) ixmlNode_getNodeName(l1_node); 182 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 183 | char *v = (char*) ixmlNode_getNodeValue(l1_1_node); 184 | LoadConfigItem(Conf, n, v); 185 | } 186 | if (node_list) ixmlNodeList_free(node_list); 187 | } 188 | 189 | return node; 190 | } 191 | 192 | /*----------------------------------------------------------------------------*/ 193 | void *LoadConfig(char *name, tMRConfig *Conf) { 194 | IXML_Document* doc = ixmlLoadDocument(name); 195 | if (!doc) return NULL; 196 | 197 | IXML_Element* elm = ixmlDocument_getElementById(doc, "aircast"); 198 | if (elm) { 199 | IXML_NodeList* l1_node_list = ixmlNode_getChildNodes((IXML_Node*) elm); 200 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 201 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 202 | char* n = (char*) ixmlNode_getNodeName(l1_node); 203 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 204 | char* v = (char*) ixmlNode_getNodeValue(l1_1_node); 205 | LoadGlobalItem(n, v); 206 | } 207 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 208 | } 209 | 210 | elm = ixmlDocument_getElementById((IXML_Document *)elm, "common"); 211 | if (elm) { 212 | IXML_NodeList* l1_node_list = ixmlNode_getChildNodes((IXML_Node*) elm); 213 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 214 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 215 | char* n = (char*) ixmlNode_getNodeName(l1_node); 216 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 217 | char* v = (char*) ixmlNode_getNodeValue(l1_1_node); 218 | LoadConfigItem(&glMRConfig, n, v); 219 | } 220 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 221 | } 222 | 223 | return doc; 224 | } 225 | 226 | 227 | -------------------------------------------------------------------------------- /aircast/src/config_cast.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AirCast: config management 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | void SaveConfig(char *name, void *ref, bool full); 13 | void* LoadConfig(char *name, struct sMRConfig *Conf); 14 | void* FindMRConfig(void *ref, char *UDN); 15 | void* LoadMRConfig(void *ref, char *UDN, struct sMRConfig *Conf); 16 | -------------------------------------------------------------------------------- /airupnp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AirUPnP bridge 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | ExecStart=/var/lib/airconnect/airupnp-linux-arm -l 1000:2000 -Z -x /var/lib/airconnect/airupnp.xml 8 | Restart=on-failure 9 | RestartSec=30 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /airupnp/AirUPnP.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Static 14 | Win32 15 | 16 | 17 | 18 | 19 | 20 | 21 | stdc11 22 | stdc11 23 | stdc11 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 16.0 43 | Win32Proj 44 | {0e8d87a4-0262-46bb-b355-4dbe44491af0} 45 | 10.0 46 | .. 47 | $(BaseDir)\common 48 | 49 | 50 | 51 | Application 52 | v143 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $(SolutionDir)\$(ProjectName)\build\$(Platform)\$(Configuration)\ 66 | $(SolutionDir)\bin\ 67 | 68 | 69 | $(ProjectName)-static 70 | 71 | 72 | 73 | Level3 74 | 4129;4018;4244;4101;4267;4102;4068;4142 75 | true 76 | UPNP_STATIC_LIB;_CRT_NONSTDC_NO_DEPRECATE;_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 77 | true 78 | $(BaseCommon);$(BaseCommon)\libraop\targets\include;$(BaseCommon)\crosstools\src;$(BaseCommon)\libopenssl\targets\win32\$(PlatformTarget)\include;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\addons;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\upnp;$(BaseCommon)\libpupnp\targets\win32\$(PlatformTarget)\include\ixml;$(BaseCommon)\libmdns\targets\include\mdnssvc;$(BaseCommon)\libmdns\targets\include\mdnssd;$(BaseCommon)\libpthreads4w\targets\win32\$(PlatformTarget)\include;$(BaseDir)\tools;$(BaseCommon)\dmap-parser\;$(BaseCommon)\libcodecs\targets\include\flac;$(BaseCommon)\libcodecs\targets\include\shine;$(BaseCommon)\libcodecs\targets\include\addons;%(AdditionalIncludeDirectories) 79 | stdcpp20 80 | $(TEMP)vc$(PlatformToolsetVersion)$(ProjectName).pdb 81 | Default 82 | OldStyle 83 | OldStyle 84 | 85 | 86 | Console 87 | true 88 | 89 | 90 | ws2_32.lib;wsock32.lib;%(AdditionalDependencies) 91 | libcmt;libcmtd 92 | 93 | 94 | ..\common\manifest.xml 95 | 96 | 97 | ..\common\manifest.xml 98 | 99 | 100 | 101 | 102 | SSL_LIB_STATIC;%(PreprocessorDefinitions) 103 | $(TEMP)vc$(PlatformToolsetVersion)$(ProjectName).pd 104 | OldStyle 105 | 106 | 107 | $(BaseCommon)\libopenssl\targets\win32\$(PlatformTarget);%(AdditionalLibraryDirectories) 108 | libopenssl_static.lib;%(AdditionalDependencies) 109 | 110 | 111 | ..\common\manifest.xml 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /airupnp/Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(CC),cc) 2 | CC=$(lastword $(subst /, ,$(shell readlink -f `which cc`))) 3 | endif 4 | 5 | ifeq ($(findstring gcc,$(CC)),gcc) 6 | CFLAGS += -Wno-deprecated-declarations -Wno-format-truncation -Wno-stringop-truncation 7 | LDFLAGS += -s 8 | else 9 | CFLAGS += -fno-temp-file 10 | endif 11 | 12 | PLATFORM ?= $(firstword $(subst -, ,$(CC))) 13 | HOST ?= $(word 2, $(subst -, ,$(CC))) 14 | 15 | ifneq ($(HOST),macos) 16 | ifneq ($(HOST),solaris) 17 | LINKSTATIC = -static 18 | else 19 | LDFLAGS += -lssp 20 | endif 21 | endif 22 | 23 | BASE = .. 24 | CORE = $(BASE)/bin/airupnp-$(HOST) 25 | BUILDDIR = $(dir $(CORE))$(HOST)/$(PLATFORM) 26 | EXECUTABLE = $(CORE)-$(PLATFORM) 27 | EXECUTABLE_STATIC = $(EXECUTABLE)-static 28 | 29 | SRC = src 30 | COMMON = $(BASE)/common 31 | TOOLS = $(COMMON)/crosstools/src 32 | RAOP = $(COMMON)/libraop/targets 33 | MDNS = $(COMMON)/libmdns/targets 34 | #VALGRIND = $(BASE)/valgrind 35 | PUPNP = $(COMMON)/libpupnp/targets/$(HOST)/$(PLATFORM) 36 | CODECS = $(COMMON)/libcodecs/targets 37 | OPENSSL = $(COMMON)/libopenssl/targets/$(HOST)/$(PLATFORM) 38 | 39 | DEFINES += -DNDEBUG -D_GNU_SOURCE -DUPNP_STATIC_LIB 40 | CFLAGS += -Wall -fPIC -ggdb -O2 $(DEFINES) -fdata-sections -ffunction-sections 41 | LDFLAGS += -lpthread -ldl -lm -L. 42 | 43 | vpath %.c $(TOOLS):$(SRC):$(COMMON) 44 | 45 | INCLUDE = -I$(OPENSSL)/include \ 46 | -I$(COMMON) \ 47 | -I$(CODECS)/include/flac -I$(CODECS)/include/shine \ 48 | -I$(TOOLS) \ 49 | -I$(SRC)/inc \ 50 | -I$(RAOP)/include \ 51 | -I$(PUPNP)/include/upnp -I$(PUPNP)/include/ixml -I$(PUPNP)/include/addons \ 52 | -I$(MDNS)/include/mdnssvc -I$(MDNS)/include/mdnssd 53 | 54 | DEPS = $(SRC)/airupnp.h $(LIBRARY) $(LIBRARY_STATIC) 55 | 56 | SOURCES = avt_util.c airupnp.c mr_util.c config_upnp.c \ 57 | cross_util.c cross_log.c cross_net.c cross_thread.c platform.c 58 | 59 | SOURCES_LIBS = cross_ssl.c 60 | 61 | OBJECTS = $(patsubst %.c,$(BUILDDIR)/%.o,$(SOURCES) $(SOURCES_LIBS)) 62 | OBJECTS_STATIC = $(patsubst %.c,$(BUILDDIR)/%.o,$(SOURCES)) $(patsubst %.c,$(BUILDDIR)/%-static.o,$(SOURCES_LIBS)) 63 | 64 | LIBRARY = $(RAOP)/$(HOST)/$(PLATFORM)/libraop.a \ 65 | $(PUPNP)/libpupnp.a \ 66 | $(CODECS)/$(HOST)/$(PLATFORM)/libcodecs.a \ 67 | $(MDNS)/$(HOST)/$(PLATFORM)/libmdns.a 68 | 69 | LIBRARY_STATIC = $(LIBRARY) $(OPENSSL)/libopenssl.a 70 | 71 | all: directory $(EXECUTABLE) $(EXECUTABLE_STATIC) 72 | 73 | $(EXECUTABLE): $(OBJECTS) 74 | $(CC) $(OBJECTS) $(LIBRARY) $(CFLAGS) $(LDFLAGS) -o $@ 75 | ifeq ($(HOST),macos) 76 | rm -f $(CORE) 77 | lipo -create -output $(CORE) $$(ls $(CORE)* | grep -v '\-static') 78 | endif 79 | 80 | $(EXECUTABLE_STATIC): $(OBJECTS_STATIC) 81 | $(CC) $(OBJECTS_STATIC) $(LIBRARY_STATIC) $(CFLAGS) $(LDFLAGS) $(LINKSTATIC) -o $@ 82 | ifeq ($(HOST),macos) 83 | rm -f $(CORE)-static 84 | lipo -create -output $(CORE)-static $(CORE)-*-static 85 | endif 86 | 87 | $(OBJECTS) $(OBJECTS_STATIC): $(DEPS) 88 | 89 | directory: 90 | @mkdir -p $(BUILDDIR) 91 | 92 | $(BUILDDIR)/%.o : %.c 93 | $(CC) $(CFLAGS) $(CPPFLAGS) $(INCLUDE) $< -c -o $@ 94 | 95 | $(BUILDDIR)/%-static.o : %.c 96 | $(CC) $(CFLAGS) $(CPPFLAGS) -DSSL_STATIC_LIB $(INCLUDE) $< -c -o $(BUILDDIR)/$*-static.o 97 | 98 | clean: 99 | rm -f $(OBJECTS) $(EXECUTABLE) $(OBJECTS_STATIC) $(EXECUTABLE_STATIC) $(CORE) $(CORE)-static 100 | -------------------------------------------------------------------------------- /airupnp/airupnp.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | crosstools 10 | 11 | 12 | crosstools 13 | 14 | 15 | crosstools 16 | 17 | 18 | crosstools 19 | 20 | 21 | crosstools 22 | 23 | 24 | crosstools 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {327936e6-f488-4665-bacd-e765b0e0d3b1} 37 | 38 | 39 | -------------------------------------------------------------------------------- /airupnp/src/airupnp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AirUPnP - AirPlay to uPNP gateway 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "pthread.h" 17 | #include "upnp.h" 18 | 19 | #include "platform.h" 20 | #include "raop_server.h" 21 | #include "cross_util.h" 22 | #include "metadata.h" 23 | 24 | #define VERSION "v1.8.3"" ("__DATE__" @ "__TIME__")" 25 | 26 | /*----------------------------------------------------------------------------*/ 27 | /* typedefs */ 28 | /*----------------------------------------------------------------------------*/ 29 | 30 | #define STR_LEN 256 31 | 32 | #define MAX_PROTO 128 33 | #define MAX_RENDERERS 32 34 | #define MAGIC 0xAABBCCDD 35 | #define RESOURCE_LENGTH 250 36 | 37 | enum eMRstate { UNKNOWN, STOPPED, PLAYING, PAUSED, TRANSITIONING }; 38 | enum { AVT_SRV_IDX = 0, REND_SRV_IDX, CNX_MGR_IDX, TOPOLOGY_IDX, GRP_REND_SRV_IDX, NB_SRV }; 39 | 40 | struct sService { 41 | char Id [RESOURCE_LENGTH]; 42 | char Type [RESOURCE_LENGTH]; 43 | char EventURL [RESOURCE_LENGTH]; 44 | char ControlURL [RESOURCE_LENGTH]; 45 | Upnp_SID SID; 46 | int32_t TimeOut; 47 | uint32_t Failed; 48 | }; 49 | 50 | typedef struct sMRConfig 51 | { 52 | int HTTPLength; 53 | bool Enabled; 54 | char Name[STR_LEN]; 55 | int UPnPMax; 56 | bool SendMetaData; 57 | bool SendCoverArt; 58 | bool Flush; 59 | int MaxVolume; 60 | char Codec[STR_LEN]; 61 | bool Metadata; 62 | char Latency[STR_LEN]; 63 | bool Drift; 64 | uint8_t mac[6]; 65 | char ArtWork[4*STR_LEN]; 66 | } tMRConfig; 67 | 68 | struct sMR { 69 | uint32_t Magic; 70 | bool Running; 71 | tMRConfig Config; 72 | char UDN [RESOURCE_LENGTH]; 73 | char DescDocURL [RESOURCE_LENGTH]; 74 | char friendlyName [STR_LEN]; 75 | enum eMRstate State; 76 | bool ExpectStop; 77 | struct raopsr_s *Raop; 78 | metadata_t MetaData; 79 | raopsr_event_t RaopState; 80 | uint32_t Elapsed; 81 | uint32_t LastSeen; 82 | uint8_t *seqN; 83 | void *WaitCookie, *StartCookie; 84 | cross_queue_t ActionQueue; 85 | unsigned TrackPoll, StatePoll; 86 | struct sService Service[NB_SRV]; 87 | struct sAction *Actions; 88 | struct sMR *Master; 89 | pthread_mutex_t Mutex; 90 | pthread_t Thread; 91 | double Volume; // to avoid int volume being stuck at 0 92 | uint32_t VolumeStampRx, VolumeStampTx; 93 | int ErrorCount; 94 | bool TimeOut; 95 | char *ProtocolInfo; 96 | }; 97 | 98 | extern UpnpClient_Handle glControlPointHandle; 99 | extern int32_t glLogLimit; 100 | extern tMRConfig glMRConfig; 101 | extern struct sMR *glMRDevices; 102 | extern int glMaxDevices; 103 | extern char glBinding[128]; 104 | extern unsigned short glPortBase, glPortRange; 105 | 106 | int MasterHandler(Upnp_EventType EventType, const void *Event, void *Cookie); 107 | int ActionHandler(Upnp_EventType EventType, const void *Event, void *Cookie); 108 | -------------------------------------------------------------------------------- /airupnp/src/avt_util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * UPnP control utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * see LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | 12 | #include "platform.h" 13 | #include "ixmlextra.h" 14 | #include "upnptools.h" 15 | #include "cross_log.h" 16 | #include "avt_util.h" 17 | 18 | /* 19 | WARNING 20 | - ALL THESE FUNCTION MUST BE CALLED WITH MUTEX LOCKED 21 | */ 22 | 23 | extern log_level upnp_loglevel; 24 | static log_level *loglevel = &upnp_loglevel; 25 | 26 | static char *CreateDIDL(char *URI, char *ProtInfo, struct metadata_s *MetaData, struct sMRConfig *Config); 27 | 28 | /*----------------------------------------------------------------------------*/ 29 | bool SubmitTransportAction(struct sMR *Device, IXML_Document *ActionNode) { 30 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 31 | int rc = 0; 32 | 33 | if (!Device->WaitCookie) { 34 | Device->WaitCookie = Device->seqN++; 35 | rc = UpnpSendActionAsync(glControlPointHandle, Service->ControlURL, Service->Type, 36 | NULL, ActionNode, ActionHandler, Device->WaitCookie); 37 | 38 | if (rc != UPNP_E_SUCCESS) { 39 | LOG_ERROR("[%p]: Error in UpnpSendActionAsync -- %d", Device, rc); 40 | } 41 | 42 | ixmlDocument_free(ActionNode); 43 | } else { 44 | tAction *Action = malloc(sizeof(tAction)); 45 | Action->Device = Device; 46 | Action->ActionNode = ActionNode; 47 | queue_insert(&Device->ActionQueue, Action); 48 | } 49 | 50 | return (rc == 0); 51 | } 52 | 53 | /*----------------------------------------------------------------------------*/ 54 | void AVTActionFlush(cross_queue_t *Queue) { 55 | tAction *Action; 56 | 57 | while ((Action = queue_extract(Queue)) != NULL) { 58 | free(Action); 59 | } 60 | } 61 | 62 | /*----------------------------------------------------------------------------*/ 63 | 64 | bool AVTSetURI(struct sMR *Device, char *URI, struct metadata_s *MetaData, char *ProtoInfo) { 65 | IXML_Document *ActionNode = NULL; 66 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 67 | 68 | char *DIDLData = CreateDIDL(URI, ProtoInfo, MetaData, &Device->Config); 69 | LOG_INFO("[%p]: uPNP setURI %s (cookie %p)", Device, URI, Device->seqN); 70 | LOG_DEBUG("[%p]: DIDL header: %s", Device, DIDLData); 71 | 72 | if ((ActionNode = UpnpMakeAction("SetAVTransportURI", Service->Type, 0, NULL)) == NULL) return false; 73 | UpnpAddToAction(&ActionNode, "SetAVTransportURI", Service->Type, "InstanceID", "0"); 74 | UpnpAddToAction(&ActionNode, "SetAVTransportURI", Service->Type, "CurrentURI", URI); 75 | UpnpAddToAction(&ActionNode, "SetAVTransportURI", Service->Type, "CurrentURIMetaData", DIDLData); 76 | free(DIDLData); 77 | 78 | return SubmitTransportAction(Device, ActionNode); 79 | } 80 | 81 | /*----------------------------------------------------------------------------*/ 82 | bool AVTSetNextURI(struct sMR *Device, char *URI, struct metadata_s *MetaData, char *ProtoInfo) { 83 | IXML_Document *ActionNode = NULL; 84 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 85 | 86 | char *DIDLData = CreateDIDL(URI, ProtoInfo, MetaData, &Device->Config); 87 | LOG_INFO("[%p]: uPNP setNextURI %s (cookie %p)", Device, URI, Device->seqN); 88 | LOG_DEBUG("[%p]: DIDL header: %s", Device, DIDLData); 89 | 90 | if ((ActionNode = UpnpMakeAction("SetNextAVTransportURI", Service->Type, 0, NULL)) == NULL) return false; 91 | UpnpAddToAction(&ActionNode, "SetNextAVTransportURI", Service->Type, "InstanceID", "0"); 92 | UpnpAddToAction(&ActionNode, "SetNextAVTransportURI", Service->Type, "NextURI", URI); 93 | UpnpAddToAction(&ActionNode, "SetNextAVTransportURI", Service->Type, "NextURIMetaData", DIDLData); 94 | free(DIDLData); 95 | 96 | return SubmitTransportAction(Device, ActionNode); 97 | } 98 | 99 | /*----------------------------------------------------------------------------*/ 100 | int AVTCallAction(struct sMR *Device, char *Action, void *Cookie) { 101 | IXML_Document *ActionNode = NULL; 102 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 103 | 104 | LOG_SDEBUG("[%p]: uPNP %s (cookie %p)", Device, Action, Cookie); 105 | 106 | if ((ActionNode = UpnpMakeAction(Action, Service->Type, 0, NULL)) == NULL) return false; 107 | UpnpAddToAction(&ActionNode, Action, Service->Type, "InstanceID", "0"); 108 | 109 | int rc = UpnpSendActionAsync(glControlPointHandle, Service->ControlURL, Service->Type, NULL, 110 | ActionNode, ActionHandler, Cookie); 111 | 112 | if (rc != UPNP_E_SUCCESS) LOG_ERROR("[%p]: Error in UpnpSendActionAsync -- %d", Device, rc); 113 | ixmlDocument_free(ActionNode); 114 | 115 | return rc; 116 | } 117 | 118 | /*----------------------------------------------------------------------------*/ 119 | bool AVTPlay(struct sMR *Device) { 120 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 121 | IXML_Document *ActionNode = NULL; 122 | 123 | LOG_INFO("[%p]: uPNP play (cookie %p)", Device, Device->seqN); 124 | 125 | if ((ActionNode = UpnpMakeAction("Play", Service->Type, 0, NULL)) == NULL) return false; 126 | UpnpAddToAction(&ActionNode, "Play", Service->Type, "InstanceID", "0"); 127 | UpnpAddToAction(&ActionNode, "Play", Service->Type, "Speed", "1"); 128 | 129 | return SubmitTransportAction(Device, ActionNode); 130 | } 131 | 132 | /*----------------------------------------------------------------------------*/ 133 | bool AVTSetPlayMode(struct sMR *Device) { 134 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 135 | IXML_Document *ActionNode = NULL; 136 | 137 | LOG_INFO("[%p]: uPNP set play mode (cookie %p)", Device, Device->seqN); 138 | if ((ActionNode = UpnpMakeAction("SetPlayMode", Service->Type, 0, NULL)) == NULL) return false;; 139 | UpnpAddToAction(&ActionNode, "SetPlayMode", Service->Type, "InstanceID", "0"); 140 | UpnpAddToAction(&ActionNode, "SetPlayMode", Service->Type, "NewPlayMode", "NORMAL"); 141 | 142 | return SubmitTransportAction(Device, ActionNode); 143 | } 144 | 145 | /*----------------------------------------------------------------------------*/ 146 | bool AVTSeek(struct sMR *Device, unsigned Interval) { 147 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 148 | IXML_Document *ActionNode = NULL; 149 | char params[128]; 150 | 151 | LOG_INFO("[%p]: uPNP seek (%.2lf sec) (cookie %p)", Device, Interval / 1000.0, Device->seqN); 152 | 153 | if ((ActionNode = UpnpMakeAction("Seek", Service->Type, 0, NULL)) == NULL) return false; 154 | UpnpAddToAction(&ActionNode, "Seek", Service->Type, "InstanceID", "0"); 155 | sprintf(params, "%d", (int) (Interval / 1000 + 0.5)); 156 | UpnpAddToAction(&ActionNode, "Seek", Service->Type, "Unit", params); 157 | UpnpAddToAction(&ActionNode, "Seek", Service->Type, "Target", "REL_TIME"); 158 | 159 | return SubmitTransportAction(Device, ActionNode); 160 | } 161 | 162 | /*----------------------------------------------------------------------------*/ 163 | bool AVTBasic(struct sMR *Device, char *Action) { 164 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 165 | IXML_Document *ActionNode = NULL; 166 | 167 | LOG_INFO("[%p]: uPNP %s (cookie %p)", Device, Action, Device->seqN); 168 | 169 | if ((ActionNode = UpnpMakeAction(Action, Service->Type, 0, NULL)) == NULL) return false; 170 | UpnpAddToAction(&ActionNode, Action, Service->Type, "InstanceID", "0"); 171 | 172 | return SubmitTransportAction(Device, ActionNode); 173 | } 174 | 175 | /*----------------------------------------------------------------------------*/ 176 | bool AVTStop(struct sMR *Device) { 177 | struct sService *Service = &Device->Service[AVT_SRV_IDX]; 178 | IXML_Document *ActionNode = NULL; 179 | 180 | LOG_INFO("[%p]: uPNP stop (cookie %p)", Device, Device->seqN); 181 | 182 | if ((ActionNode = UpnpMakeAction("Stop", Service->Type, 0, NULL)) == NULL) return false; 183 | UpnpAddToAction(&ActionNode, "Stop", Service->Type, "InstanceID", "0"); 184 | AVTActionFlush(&Device->ActionQueue); 185 | 186 | Device->WaitCookie = Device->seqN++; 187 | int rc = UpnpSendActionAsync(glControlPointHandle, Service->ControlURL, Service->Type, 188 | NULL, ActionNode, ActionHandler, Device->WaitCookie); 189 | 190 | ixmlDocument_free(ActionNode); 191 | 192 | if (rc != UPNP_E_SUCCESS) { 193 | LOG_ERROR("[%p]: Error in UpnpSendActionAsync -- %d", Device, rc); 194 | } 195 | 196 | return (rc == 0); 197 | } 198 | 199 | /*----------------------------------------------------------------------------*/ 200 | int CtrlSetVolume(struct sMR *Device, uint8_t Volume, void *Cookie) { 201 | IXML_Document *ActionNode = NULL; 202 | struct sService *Service = &Device->Service[REND_SRV_IDX]; 203 | char params[8]; 204 | 205 | LOG_INFO("[%p]: uPNP volume %d (cookie %p)", Device, Volume, Cookie); 206 | 207 | ActionNode = UpnpMakeAction("SetVolume", Service->Type, 0, NULL); 208 | UpnpAddToAction(&ActionNode, "SetVolume", Service->Type, "InstanceID", "0"); 209 | UpnpAddToAction(&ActionNode, "SetVolume", Service->Type, "Channel", "Master"); 210 | sprintf(params, "%d", (int) Volume); 211 | UpnpAddToAction(&ActionNode, "SetVolume", Service->Type, "DesiredVolume", params); 212 | 213 | int rc = UpnpSendActionAsync(glControlPointHandle, Service->ControlURL, Service->Type, NULL, 214 | ActionNode, ActionHandler, Cookie); 215 | if (rc != UPNP_E_SUCCESS) { 216 | LOG_ERROR("[%p]: Error in UpnpSendActionAsync -- %d", Device, rc); 217 | } 218 | 219 | if (ActionNode) ixmlDocument_free(ActionNode); 220 | 221 | return rc; 222 | } 223 | 224 | /*----------------------------------------------------------------------------*/ 225 | int CtrlSetMute(struct sMR *Device, bool Mute, void *Cookie) { 226 | IXML_Document *ActionNode = NULL; 227 | struct sService *Service = &Device->Service[REND_SRV_IDX]; 228 | 229 | LOG_INFO("[%p]: uPNP mute %d (cookie %p)", Device, Mute, Cookie); 230 | ActionNode = UpnpMakeAction("SetMute", Service->Type, 0, NULL); 231 | UpnpAddToAction(&ActionNode, "SetMute", Service->Type, "InstanceID", "0"); 232 | UpnpAddToAction(&ActionNode, "SetMute", Service->Type, "Channel", "Master"); 233 | UpnpAddToAction(&ActionNode, "SetMute", Service->Type, "DesiredMute", Mute ? "1" : "0"); 234 | 235 | int rc = UpnpSendActionAsync(glControlPointHandle, Service->ControlURL, Service->Type, NULL, 236 | ActionNode, ActionHandler, Cookie); 237 | 238 | if (ActionNode) ixmlDocument_free(ActionNode); 239 | 240 | if (rc != UPNP_E_SUCCESS) { 241 | LOG_ERROR("[%p]: Error in UpnpSendActionAsync -- %d", Device, rc); 242 | } 243 | 244 | return rc; 245 | } 246 | 247 | /*----------------------------------------------------------------------------*/ 248 | int CtrlGetGroupVolume(struct sMR *Device) { 249 | IXML_Document *ActionNode, *Response = NULL; 250 | struct sService *Service = &Device->Service[GRP_REND_SRV_IDX]; 251 | int Volume = -1; 252 | 253 | if (*Service->ControlURL) return Volume; 254 | 255 | ActionNode = UpnpMakeAction("GetGroupVolume", Service->Type, 0, NULL); 256 | UpnpAddToAction(&ActionNode, "GetGroupVolume", Service->Type, "InstanceID", "0"); 257 | UpnpSendAction(glControlPointHandle, Service->ControlURL, Service->Type, 258 | NULL, ActionNode, &Response); 259 | 260 | if (ActionNode) ixmlDocument_free(ActionNode); 261 | 262 | char *Item = XMLGetFirstDocumentItem(Response, "CurrentVolume", true); 263 | if (Response) ixmlDocument_free(Response); 264 | 265 | // master / slave relation might not be set yet, so GetGroupVolume will fail 266 | if (Item) { 267 | Volume = atoi(Item); 268 | free(Item); 269 | } 270 | 271 | return Volume; 272 | } 273 | 274 | /*----------------------------------------------------------------------------*/ 275 | int CtrlGetVolume(struct sMR *Device) { 276 | IXML_Document *ActionNode, *Response = NULL; 277 | struct sService *Service = &Device->Service[REND_SRV_IDX]; 278 | int Volume = -1; 279 | 280 | if (*Service->ControlURL) return Volume; 281 | 282 | ActionNode = UpnpMakeAction("GetVolume", Service->Type, 0, NULL); 283 | UpnpAddToAction(&ActionNode, "GetVolume", Service->Type, "InstanceID", "0"); 284 | UpnpAddToAction(&ActionNode, "GetVolume", Service->Type, "Channel", "Master"); 285 | UpnpSendAction(glControlPointHandle, Service->ControlURL, Service->Type, 286 | NULL, ActionNode, &Response); 287 | 288 | if (ActionNode) ixmlDocument_free(ActionNode); 289 | 290 | if (Response) { 291 | char *Item = XMLGetFirstDocumentItem(Response, "CurrentVolume", true); 292 | if (Item) { 293 | Volume = atoi(Item); 294 | free(Item); 295 | } 296 | ixmlDocument_free(Response); 297 | } 298 | 299 | return Volume; 300 | } 301 | 302 | /*----------------------------------------------------------------------------*/ 303 | char *GetProtocolInfo(struct sMR *Device) { 304 | IXML_Document *ActionNode, *Response = NULL; 305 | struct sService *Service = &Device->Service[CNX_MGR_IDX]; 306 | char *ProtocolInfo = NULL; 307 | 308 | LOG_DEBUG("[%p]: uPNP GetProtocolInfo", Device); 309 | ActionNode = UpnpMakeAction("GetProtocolInfo", Service->Type, 0, NULL); 310 | 311 | UpnpSendAction(glControlPointHandle, Service->ControlURL, Service->Type, NULL, 312 | ActionNode, &Response); 313 | 314 | if (ActionNode) ixmlDocument_free(ActionNode); 315 | 316 | if (Response) { 317 | ProtocolInfo = XMLGetFirstDocumentItem(Response, "Sink", false); 318 | ixmlDocument_free(Response); 319 | LOG_DEBUG("[%p]: ProtocolInfo %s", Device, ProtocolInfo); 320 | } 321 | 322 | return ProtocolInfo; 323 | } 324 | 325 | /*----------------------------------------------------------------------------*/ 326 | char *CreateDIDL(char *URI, char *ProtoInfo, struct metadata_s *MetaData, struct sMRConfig *Config) { 327 | IXML_Document *doc = ixmlDocument_createDocument(); 328 | IXML_Node *node, *root; 329 | 330 | root = XMLAddNode(doc, NULL, "DIDL-Lite", NULL); 331 | XMLAddAttribute(doc, root, "xmlns:dc", "http://purl.org/dc/elements/1.1/"); 332 | XMLAddAttribute(doc, root, "xmlns:upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/"); 333 | XMLAddAttribute(doc, root, "xmlns", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"); 334 | XMLAddAttribute(doc, root, "xmlns:dlna", "urn:schemas-dlna-org:metadata-1-0/"); 335 | 336 | node = XMLAddNode(doc, root, "item", NULL); 337 | XMLAddAttribute(doc, node, "id", "1"); 338 | XMLAddAttribute(doc, node, "parentID", "0"); 339 | XMLAddAttribute(doc, node, "restricted", "1"); 340 | 341 | if (MetaData->duration) { 342 | div_t duration = div(MetaData->duration, 1000); 343 | 344 | if (Config->SendMetaData) { 345 | XMLAddNode(doc, node, "dc:title", MetaData->title); 346 | XMLAddNode(doc, node, "dc:creator", MetaData->artist); 347 | XMLAddNode(doc, node, "upnp:genre", MetaData->genre); 348 | XMLAddNode(doc, node, "upnp:artist", MetaData->artist); 349 | XMLAddNode(doc, node, "upnp:album", MetaData->album); 350 | if (MetaData->track) XMLAddNode(doc, node, "upnp:originalTrackNumber", "%d", MetaData->track); 351 | if (MetaData->disc) XMLAddNode(doc, node, "upnp:originalDiscNumber", "%d", MetaData->disc); 352 | if (MetaData->artwork) XMLAddNode(doc, node, "upnp:albumArtURI", "%s", MetaData->artwork); 353 | } 354 | 355 | XMLAddNode(doc, node, "upnp:class", "object.item.audioItem.musicTrack"); 356 | node = XMLAddNode(doc, node, "res", URI); 357 | XMLAddAttribute(doc, node, "duration", "%1d:%02d:%02d.%03d", 358 | duration.quot/3600, (duration.quot % 3600) / 60, 359 | duration.quot % 60, duration.rem); 360 | } else { 361 | if (Config->SendMetaData) { 362 | XMLAddNode(doc, node, "dc:title", MetaData->remote_title); 363 | XMLAddNode(doc, node, "dc:creator", ""); 364 | XMLAddNode(doc, node, "upnp:album", ""); 365 | XMLAddNode(doc, node, "upnp:channelName", MetaData->remote_title); 366 | XMLAddNode(doc, node, "upnp:channelNr", "%d", MetaData->track); 367 | if (MetaData->artwork) XMLAddNode(doc, node, "upnp:albumArtURI", "%s", MetaData->artwork); 368 | } 369 | 370 | XMLAddNode(doc, node, "upnp:class", "object.item.audioItem.audioBroadcast"); 371 | node = XMLAddNode(doc, node, "res", URI); 372 | } 373 | 374 | XMLAddAttribute(doc, node, "protocolInfo", ProtoInfo); 375 | 376 | // set optional parameters if we have them all (only happens with pcm) 377 | if (MetaData->sample_rate && MetaData->sample_size && MetaData->channels) { 378 | XMLAddAttribute(doc, node, "sampleFrequency", "%u", MetaData->sample_rate); 379 | XMLAddAttribute(doc, node, "bitsPerSample", "%hhu", MetaData->sample_size); 380 | XMLAddAttribute(doc, node, "nrAudioChannels", "%hhu", MetaData->channels); 381 | if (MetaData->duration) 382 | XMLAddAttribute(doc, node, "size", "%u", (uint32_t) ((MetaData->sample_rate * 383 | MetaData->sample_size / 8 * MetaData->channels * 384 | (uint64_t) MetaData->duration) / 1000)); 385 | } 386 | 387 | char *s = ixmlNodetoString((IXML_Node*) doc); 388 | ixmlDocument_free(doc); 389 | 390 | return s; 391 | } 392 | 393 | 394 | 395 | /* typical DIDL header 396 | 397 | "" 398 | "" 399 | "Make You Feel My Love" 400 | "Adele" 401 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/0_ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40.mp3" 402 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40?formatID=20" 403 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40?formatID=18" 404 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40.mp3?formatID=24" 405 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40.wma?formatID=42" 406 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40.wma?formatID=50" 407 | "http://192.168.2.10:10243/WMPNSSv4/4053149364/ezIxNDhGMUQ1LTFCRTYtNDdDMy04MUFGLTYxNUE5NjBFMzcwNH0uMC40.wma?formatID=54" 408 | "object.item.audioItem.musicTrack" 409 | "[Unknown Genre]" 410 | "Adele" 411 | "Adele" 412 | "[Unknown Composer]" 413 | "19" 414 | "9" 415 | "2008-01-02" 416 | "Adele" 417 | "" 418 | "Adele" 419 | "Adele" 420 | "" 421 | "" 422 | "[Unknown Composer]" 423 | "" 424 | "" 425 | "2008" 426 | "" 427 | "" 428 | "3" 429 | "50" 430 | "" 431 | "" 432 | "" 433 | */ 434 | 435 | 436 | -------------------------------------------------------------------------------- /airupnp/src/avt_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * UPnP Control util 3 | * 4 | * (c) Philippe, philippe_44@outlook.comom 5 | * 6 | * see LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include "ixml.h" 13 | #include "airupnp.h" 14 | 15 | struct sMRConfig; 16 | struct sMR; 17 | 18 | typedef struct sAction { 19 | struct sMR *Device; 20 | void *ActionNode; 21 | union { 22 | uint8_t Volume; 23 | } Param; 24 | } tAction; 25 | 26 | bool AVTSetURI(struct sMR *Device, char *URI, struct metadata_s *MetaData, char *ProtoInfo); 27 | bool AVTSetNextURI(struct sMR *Device, char *URI, struct metadata_s *MetaData, char *ProtoInfo); 28 | int AVTCallAction(struct sMR *Device, char *Var, void *Cookie); 29 | bool AVTPlay(struct sMR *Device); 30 | bool AVTSetPlayMode(struct sMR *Device); 31 | bool AVTSeek(struct sMR *Device, unsigned Interval); 32 | bool AVTBasic(struct sMR *Device, char *Action); 33 | bool AVTStop(struct sMR *Device); 34 | void AVTActionFlush(cross_queue_t *Queue); 35 | int CtrlSetVolume(struct sMR *Device, uint8_t Volume, void *Cookie); 36 | int CtrlSetMute(struct sMR *Device, bool Mute, void *Cookie); 37 | int CtrlGetVolume(struct sMR *Device); 38 | int CtrlGetGroupVolume(struct sMR *Device); 39 | char* GetProtocolInfo(struct sMR *Device); 40 | 41 | 42 | -------------------------------------------------------------------------------- /airupnp/src/config_upnp.c: -------------------------------------------------------------------------------- 1 | /* 2 | * AirUPnP - Config utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * see LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "platform.h" 15 | #include "ixmlextra.h" 16 | #include "cross_log.h" 17 | #include "airupnp.h" 18 | #include "config_upnp.h" 19 | 20 | /*----------------------------------------------------------------------------*/ 21 | /* locals */ 22 | /*----------------------------------------------------------------------------*/ 23 | extern log_level main_loglevel; 24 | extern log_level util_loglevel; 25 | extern log_level raop_loglevel; 26 | extern log_level upnp_loglevel; 27 | 28 | /*----------------------------------------------------------------------------*/ 29 | void SaveConfig(char *name, void *ref, bool full) { 30 | struct sMR *p; 31 | IXML_Document *doc = ixmlDocument_createDocument(); 32 | IXML_Document *old_doc = ref; 33 | IXML_Node *root, *common; 34 | IXML_Element* old_root = ixmlDocument_getElementById(old_doc, "airupnp"); 35 | 36 | if (!full && old_doc) { 37 | ixmlDocument_importNode(doc, (IXML_Node*) old_root, true, &root); 38 | ixmlNode_appendChild((IXML_Node*) doc, root); 39 | 40 | IXML_NodeList* list = ixmlDocument_getElementsByTagName((IXML_Document*) root, "device"); 41 | for (int i = 0; i < (int) ixmlNodeList_length(list); i++) { 42 | IXML_Node *device = ixmlNodeList_item(list, i); 43 | ixmlNode_removeChild(root, device, &device); 44 | ixmlNode_free(device); 45 | } 46 | if (list) ixmlNodeList_free(list); 47 | common = (IXML_Node*) ixmlDocument_getElementById((IXML_Document*) root, "common"); 48 | } else { 49 | root = XMLAddNode(doc, NULL, "airupnp", NULL); 50 | common = (IXML_Node*) XMLAddNode(doc, root, "common", NULL); 51 | } 52 | 53 | XMLUpdateNode(doc, root, false, "main_log",level2debug(main_loglevel)); 54 | XMLUpdateNode(doc, root, false, "upnp_log",level2debug(upnp_loglevel)); 55 | XMLUpdateNode(doc, root, false, "util_log",level2debug(util_loglevel)); 56 | XMLUpdateNode(doc, root, false, "raop_log",level2debug(raop_loglevel)); 57 | XMLUpdateNode(doc, root, false, "log_limit", "%d", (int32_t) glLogLimit); 58 | XMLUpdateNode(doc, root, false, "max_players", "%d", (int) glMaxDevices); 59 | XMLUpdateNode(doc, root, false, "binding", glBinding); 60 | XMLUpdateNode(doc, root, false, "ports", "%hu:%hu", glPortBase, glPortRange); 61 | 62 | XMLUpdateNode(doc, common, false, "enabled", "%d", (int) glMRConfig.Enabled); 63 | XMLUpdateNode(doc, common, false, "max_volume", "%d", glMRConfig.MaxVolume); 64 | XMLUpdateNode(doc, common, false, "http_length", "%d", glMRConfig.HTTPLength); 65 | XMLUpdateNode(doc, common, false, "upnp_max", "%d", glMRConfig.UPnPMax); 66 | XMLUpdateNode(doc, common, false, "codec", glMRConfig.Codec); 67 | XMLUpdateNode(doc, common, false, "metadata", "%d", glMRConfig.Metadata); 68 | XMLUpdateNode(doc, common, false, "flush", "%d", glMRConfig.Flush); 69 | XMLUpdateNode(doc, common, false, "artwork", "%s", glMRConfig.ArtWork); 70 | XMLUpdateNode(doc, common, false, "latency", glMRConfig.Latency); 71 | XMLUpdateNode(doc, common, false, "drift", "%d", glMRConfig.Drift); 72 | 73 | // mutex is locked here so no risk of a player being destroyed in our back 74 | for (int i = 0; i < glMaxDevices; i++) { 75 | IXML_Node *dev_node; 76 | 77 | if (!glMRDevices[i].Running) continue; 78 | else p = &glMRDevices[i]; 79 | 80 | // new device, add nodes 81 | if (!old_doc || !FindMRConfig(old_doc, p->UDN)) { 82 | dev_node = XMLAddNode(doc, root, "device", NULL); 83 | XMLAddNode(doc, dev_node, "udn", p->UDN); 84 | XMLAddNode(doc, dev_node, "name", p->Config.Name); 85 | XMLAddNode(doc, dev_node, "mac", "%02x:%02x:%02x:%02x:%02x:%02x", p->Config.mac[0], 86 | p->Config.mac[1], p->Config.mac[2], p->Config.mac[3], p->Config.mac[4], p->Config.mac[5]); 87 | XMLAddNode(doc, dev_node, "enabled", "%d", (int) p->Config.Enabled); 88 | } 89 | } 90 | 91 | // add devices in old XML file that has not been discovered 92 | IXML_NodeList* list = ixmlDocument_getElementsByTagName((IXML_Document*) old_root, "device"); 93 | for (int i = 0; i < (int) ixmlNodeList_length(list); i++) { 94 | char *udn; 95 | IXML_Node *device, *node; 96 | 97 | device = ixmlNodeList_item(list, i); 98 | node = (IXML_Node*) ixmlDocument_getElementById((IXML_Document*) device, "udn"); 99 | node = ixmlNode_getFirstChild(node); 100 | udn = (char*) ixmlNode_getNodeValue(node); 101 | if (!FindMRConfig(doc, udn)) { 102 | ixmlDocument_importNode(doc, device, true, &device); 103 | ixmlNode_appendChild((IXML_Node*) root, device); 104 | } 105 | } 106 | if (list) ixmlNodeList_free(list); 107 | 108 | FILE* file = fopen(name, "wb"); 109 | char *s = ixmlDocumenttoString(doc); 110 | fwrite(s, 1, strlen(s), file); 111 | fclose(file); 112 | free(s); 113 | 114 | ixmlDocument_free(doc); 115 | } 116 | 117 | /*----------------------------------------------------------------------------*/ 118 | static void LoadConfigItem(tMRConfig *Conf, char *name, char *val) { 119 | if (!val) return; 120 | 121 | if (!strcmp(name, "enabled")) Conf->Enabled = atoi(val); 122 | if (!strcmp(name, "max_volume")) Conf->MaxVolume = atoi(val); 123 | if (!strcmp(name, "http_length")) Conf->HTTPLength = atoi(val); 124 | if (!strcmp(name, "upnp_max")) Conf->UPnPMax = atoi(val); 125 | if (!strcmp(name, "use_flac")) strcpy(Conf->Codec, "flac"); // temporary 126 | if (!strcmp(name, "codec")) strcpy(Conf->Codec, val); 127 | if (!strcmp(name, "metadata")) Conf->Metadata = atoi(val); 128 | if (!strcmp(name, "flush")) Conf->Flush = atoi(val); 129 | if (!strcmp(name, "artwork")) strcpy(Conf->ArtWork, val); 130 | if (!strcmp(name, "latency")) strcpy(Conf->Latency, val); 131 | if (!strcmp(name, "drift")) Conf->Drift = atoi(val); 132 | if (!strcmp(name, "name")) strcpy(Conf->Name, val); 133 | if (!strcmp(name, "mac")) { 134 | unsigned mac[6]; 135 | int i; 136 | // seems to be a Windows scanf buf, cannot support %hhx 137 | sscanf(val,"%2x:%2x:%2x:%2x:%2x:%2x", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]); 138 | for (i = 0; i < 6; i++) Conf->mac[i] = mac[i]; 139 | } 140 | } 141 | 142 | /*----------------------------------------------------------------------------*/ 143 | static void LoadGlobalItem(char *name, char *val) { 144 | if (!val) return; 145 | 146 | if (!strcmp(name, "main_log")) main_loglevel = debug2level(val); 147 | if (!strcmp(name, "upnp_log")) upnp_loglevel = debug2level(val); 148 | if (!strcmp(name, "util_log")) util_loglevel = debug2level(val); 149 | if (!strcmp(name, "raop_log")) raop_loglevel = debug2level(val); 150 | if (!strcmp(name, "log_limit")) glLogLimit = atol(val); 151 | if (!strcmp(name, "max_players")) glMaxDevices = atol(val); 152 | if (!strcmp(name, "binding")) strcpy(glBinding, val); 153 | if (!strcmp(name, "ports")) sscanf(val, "%hu:%hu", &glPortBase, &glPortRange); 154 | } 155 | 156 | /*----------------------------------------------------------------------------*/ 157 | void *FindMRConfig(void *ref, char *UDN) { 158 | IXML_Node *device = NULL; 159 | IXML_Document *doc = (IXML_Document*) ref; 160 | IXML_Element* elm = ixmlDocument_getElementById(doc, "airupnp"); 161 | IXML_NodeList* l1_node_list = ixmlDocument_getElementsByTagName((IXML_Document*) elm, "udn"); 162 | 163 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 164 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 165 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 166 | char* v = (char*) ixmlNode_getNodeValue(l1_1_node); 167 | if (v && !strcmp(v, UDN)) { 168 | device = ixmlNode_getParentNode(l1_node); 169 | break; 170 | } 171 | } 172 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 173 | return device; 174 | } 175 | 176 | /*----------------------------------------------------------------------------*/ 177 | void *LoadMRConfig(void *ref, char *UDN, tMRConfig *Conf) { 178 | IXML_Document *doc = (IXML_Document*) ref; 179 | IXML_Node* node = (IXML_Node*) FindMRConfig(doc, UDN); 180 | 181 | if (node) { 182 | IXML_NodeList* node_list = ixmlNode_getChildNodes(node); 183 | for (unsigned i = 0; i < ixmlNodeList_length(node_list); i++) { 184 | IXML_Node* l1_node = ixmlNodeList_item(node_list, i); 185 | char* n = (char*) ixmlNode_getNodeName(l1_node); 186 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 187 | char *v = (char*) ixmlNode_getNodeValue(l1_1_node); 188 | LoadConfigItem(Conf, n, v); 189 | } 190 | if (node_list) ixmlNodeList_free(node_list); 191 | } 192 | 193 | return node; 194 | } 195 | 196 | /*----------------------------------------------------------------------------*/ 197 | void *LoadConfig(char *name, tMRConfig *Conf) { 198 | IXML_Document* doc = ixmlLoadDocument(name); 199 | if (!doc) return NULL; 200 | 201 | IXML_Element* elm = ixmlDocument_getElementById(doc, "airupnp"); 202 | if (elm) { 203 | IXML_NodeList* l1_node_list = ixmlNode_getChildNodes((IXML_Node*) elm); 204 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 205 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 206 | char* n = (char*) ixmlNode_getNodeName(l1_node); 207 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 208 | char *v = (char*) ixmlNode_getNodeValue(l1_1_node); 209 | LoadGlobalItem(n, v); 210 | } 211 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 212 | } 213 | 214 | elm = ixmlDocument_getElementById((IXML_Document *)elm, "common"); 215 | if (elm) { 216 | IXML_NodeList* l1_node_list = ixmlNode_getChildNodes((IXML_Node*) elm); 217 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 218 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 219 | char* n = (char*) ixmlNode_getNodeName(l1_node); 220 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 221 | char *v = (char*) ixmlNode_getNodeValue(l1_1_node); 222 | LoadConfigItem(&glMRConfig, n, v); 223 | } 224 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 225 | } 226 | 227 | elm = ixmlDocument_getElementById((IXML_Document*)elm, "protocolInfo"); 228 | if (elm) { 229 | IXML_NodeList* l1_node_list = ixmlNode_getChildNodes((IXML_Node*) elm); 230 | for (unsigned i = 0; i < ixmlNodeList_length(l1_node_list); i++) { 231 | IXML_Node* l1_node = ixmlNodeList_item(l1_node_list, i); 232 | char* n = (char*) ixmlNode_getNodeName(l1_node); 233 | IXML_Node* l1_1_node = ixmlNode_getFirstChild(l1_node); 234 | char* v = (char*) ixmlNode_getNodeValue(l1_1_node); 235 | LoadConfigItem(&glMRConfig, n, v); 236 | } 237 | if (l1_node_list) ixmlNodeList_free(l1_node_list); 238 | } 239 | 240 | 241 | return doc; 242 | } 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /airupnp/src/config_upnp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * AirUPnP : AirPlay to UPnP bridge 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "ixml.h" /* for IXML_Document, IXML_Element */ 17 | 18 | void SaveConfig(char *name, void *ref, bool full); 19 | void* LoadConfig(char *name, struct sMRConfig *Conf); 20 | void* FindMRConfig(void *ref, char *UDN); 21 | void* LoadMRConfig(void *ref, char *UDN, struct sMRConfig *Conf); 22 | -------------------------------------------------------------------------------- /airupnp/src/mr_util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * UPnP renderer utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #include 11 | 12 | #include "platform.h" 13 | #include "ixml.h" 14 | #include "ixmlextra.h" 15 | #include "upnptools.h" 16 | #include "cross_thread.h" 17 | #include "cross_log.h" 18 | #include "avt_util.h" 19 | #include "mr_util.h" 20 | 21 | extern log_level util_loglevel; 22 | static log_level *loglevel = &util_loglevel; 23 | 24 | static IXML_Node* _getAttributeNode(IXML_Node *node, char *SearchAttr); 25 | int _voidHandler(Upnp_EventType EventType, const void *_Event, void *Cookie) { return 0; } 26 | 27 | /*----------------------------------------------------------------------------*/ 28 | int CalcGroupVolume(struct sMR *Device) { 29 | int i, n = 0; 30 | double GroupVolume = 0; 31 | 32 | if (!*Device->Service[GRP_REND_SRV_IDX].ControlURL) return -1; 33 | 34 | for (i = 0; i < glMaxDevices; i++) { 35 | struct sMR *p = glMRDevices + i; 36 | if (p->Running && (p == Device || p->Master == Device)) { 37 | if (p->Volume == -1) p->Volume = CtrlGetVolume(p); 38 | GroupVolume += p->Volume; 39 | n++; 40 | } 41 | } 42 | 43 | return GroupVolume / n; 44 | } 45 | 46 | /*----------------------------------------------------------------------------*/ 47 | struct sMR *GetMaster(struct sMR *Device, char **Name) 48 | { 49 | IXML_Document *ActionNode = NULL, *Response; 50 | char *Body; 51 | struct sMR *Master = NULL; 52 | struct sService *Service = &Device->Service[TOPOLOGY_IDX]; 53 | bool done = false; 54 | 55 | if (!*Service->ControlURL) return NULL; 56 | 57 | ActionNode = UpnpMakeAction("GetZoneGroupState", Service->Type, 0, NULL); 58 | 59 | UpnpSendAction(glControlPointHandle, Service->ControlURL, Service->Type, 60 | NULL, ActionNode, &Response); 61 | 62 | if (ActionNode) ixmlDocument_free(ActionNode); 63 | 64 | Body = XMLGetFirstDocumentItem(Response, "ZoneGroupState", true); 65 | if (Response) ixmlDocument_free(Response); 66 | 67 | Response = ixmlParseBuffer(Body); 68 | NFREE(Body); 69 | 70 | if (Response) { 71 | char myUUID[RESOURCE_LENGTH] = ""; 72 | IXML_NodeList *GroupList = ixmlDocument_getElementsByTagName(Response, "ZoneGroup"); 73 | int i; 74 | 75 | sscanf(Device->UDN, "uuid:%s", myUUID); 76 | 77 | // list all ZoneGroups 78 | for (i = 0; !done && GroupList && i < (int) ixmlNodeList_length(GroupList); i++) { 79 | IXML_Node *Group = ixmlNodeList_item(GroupList, i); 80 | const char *Coordinator = ixmlElement_getAttribute((IXML_Element*) Group, "Coordinator"); 81 | IXML_NodeList *MemberList = ixmlDocument_getElementsByTagName((IXML_Document*) Group, "ZoneGroupMember"); 82 | int j; 83 | 84 | // list all ZoneMembers 85 | for (j = 0; !done && j < (int) ixmlNodeList_length(MemberList); j++) { 86 | IXML_Node *Member = ixmlNodeList_item(MemberList, j); 87 | const char *UUID = ixmlElement_getAttribute((IXML_Element*) Member, "UUID"); 88 | int k; 89 | 90 | // get ZoneName 91 | if (!strcasecmp(myUUID, UUID)) { 92 | NFREE(*Name); 93 | *Name = strdup(ixmlElement_getAttribute((IXML_Element*) Member, "ZoneName")); 94 | if (!strcasecmp(myUUID, Coordinator)) done = true; 95 | } 96 | 97 | // look for our master (if we are not) 98 | for (k = 0; !done && k < glMaxDevices; k++) { 99 | if (glMRDevices[k].Running && strcasestr(glMRDevices[k].UDN, (char*) Coordinator)) { 100 | Master = glMRDevices + k; 101 | LOG_DEBUG("Found Master %s %s", myUUID, Master->UDN); 102 | done = true; 103 | } 104 | } 105 | } 106 | 107 | ixmlNodeList_free(MemberList); 108 | } 109 | 110 | // our master is not yet discovered, refer to self then 111 | if (!done) { 112 | Master = Device; 113 | LOG_INFO("[%p]: Master not discovered yet, assigning to self", Device); 114 | } 115 | 116 | ixmlNodeList_free(GroupList); 117 | ixmlDocument_free(Response); 118 | } 119 | 120 | return Master; 121 | } 122 | 123 | /*----------------------------------------------------------------------------*/ 124 | void FlushMRDevices(void) { 125 | for (int i = 0; i < glMaxDevices; i++) { 126 | struct sMR *p = &glMRDevices[i]; 127 | pthread_mutex_lock(&p->Mutex); 128 | if (p->Running) { 129 | // critical to stop the device otherwise libupnp might wait forever 130 | if (p->RaopState == RAOP_PLAY) AVTStop(p); 131 | raopsr_delete(p->Raop); 132 | // device's mutex returns unlocked 133 | DelMRDevice(p); 134 | } else pthread_mutex_unlock(&p->Mutex); 135 | } 136 | } 137 | 138 | /*----------------------------------------------------------------------------*/ 139 | void DelMRDevice(struct sMR *p) { 140 | // try to unsubscribe but missing players will not succeed and as a result 141 | // terminating the libupnp takes a while ... 142 | for (int i = 0; i < NB_SRV; i++) { 143 | if (p->Service[i].TimeOut) { 144 | UpnpUnSubscribeAsync(glControlPointHandle, p->Service[i].SID, _voidHandler, NULL); 145 | } 146 | } 147 | 148 | p->Running = false; 149 | 150 | // kick-up all sleepers and join player's thread 151 | crossthreads_wake(); 152 | 153 | pthread_mutex_unlock(&p->Mutex); 154 | pthread_join(p->Thread, NULL); 155 | } 156 | 157 | /*----------------------------------------------------------------------------*/ 158 | struct sMR* CURL2Device(const UpnpString *CtrlURL) { 159 | for (int i = 0; i < glMaxDevices; i++) { 160 | if (!glMRDevices[i].Running) continue; 161 | for (int j = 0; j < NB_SRV; j++) { 162 | if (!strcmp(glMRDevices[i].Service[j].ControlURL, UpnpString_get_String(CtrlURL))) { 163 | return &glMRDevices[i]; 164 | } 165 | } 166 | } 167 | 168 | return NULL; 169 | } 170 | 171 | /*----------------------------------------------------------------------------*/ 172 | struct sMR* SID2Device(const UpnpString *SID) { 173 | for (int i = 0; i < glMaxDevices; i++) { 174 | if (!glMRDevices[i].Running) continue; 175 | for (int j = 0; j < NB_SRV; j++) { 176 | if (!strcmp(glMRDevices[i].Service[j].SID, UpnpString_get_String(SID))) { 177 | return &glMRDevices[i]; 178 | } 179 | } 180 | } 181 | 182 | return NULL; 183 | } 184 | 185 | /*----------------------------------------------------------------------------*/ 186 | struct sService *EventURL2Service(const UpnpString *URL, struct sService *s) { 187 | for (int i = 0; i < NB_SRV; s++, i++) { 188 | if (!strcmp(s->EventURL, UpnpString_get_String(URL))) return s; 189 | } 190 | 191 | return NULL; 192 | } 193 | 194 | /*----------------------------------------------------------------------------*/ 195 | struct sMR* UDN2Device(const char *UDN) { 196 | for (int i = 0; i < glMaxDevices; i++) { 197 | if (glMRDevices[i].Running && !strcmp(glMRDevices[i].UDN, UDN)) return &glMRDevices[i]; 198 | } 199 | 200 | return NULL; 201 | } 202 | 203 | /*----------------------------------------------------------------------------*/ 204 | bool CheckAndLock(struct sMR *Device) { 205 | if (!Device) { 206 | LOG_INFO("device is NULL"); 207 | return false; 208 | } 209 | 210 | pthread_mutex_lock(&Device->Mutex); 211 | 212 | if (Device->Running) return true; 213 | 214 | LOG_INFO("[%p]: device has been removed", Device); 215 | pthread_mutex_unlock(&Device->Mutex); 216 | 217 | return false; 218 | } 219 | 220 | 221 | /*----------------------------------------------------------------------------*/ 222 | /* */ 223 | /* XML utils */ 224 | /* */ 225 | /*----------------------------------------------------------------------------*/ 226 | 227 | /*----------------------------------------------------------------------------*/ 228 | static IXML_NodeList *XMLGetNthServiceList(IXML_Document *doc, unsigned int n, bool *contd) { 229 | IXML_NodeList *ServiceList = NULL; 230 | *contd = false; 231 | 232 | /* ixmlDocument_getElementsByTagName() 233 | * Returns a NodeList of all Elements that match the given 234 | * tag name in the order in which they were encountered in a preorder 235 | * traversal of the Document tree. 236 | * 237 | * return (NodeList*) A pointer to a NodeList containing the 238 | * matching items or NULL on an error. */ 239 | LOG_SDEBUG("GetNthServiceList called : n = %d", n); 240 | IXML_NodeList* servlistnodelist = ixmlDocument_getElementsByTagName(doc, "serviceList"); 241 | if (servlistnodelist && 242 | ixmlNodeList_length(servlistnodelist) && 243 | n < ixmlNodeList_length(servlistnodelist)) { 244 | /* Retrieves a Node from a NodeList} specified by a 245 | * numerical index. 246 | * 247 | * return (Node*) A pointer to a Node or NULL if there was an 248 | * error. */ 249 | IXML_Node* servlistnode = ixmlNodeList_item(servlistnodelist, n); 250 | if (servlistnode) { 251 | /* create as list of DOM nodes */ 252 | ServiceList = ixmlElement_getElementsByTagName( 253 | (IXML_Element *)servlistnode, "service"); 254 | *contd = true; 255 | } else 256 | LOG_WARN("ixmlNodeList_item(nodeList, n) returned NULL", NULL); 257 | } 258 | if (servlistnodelist) 259 | ixmlNodeList_free(servlistnodelist); 260 | 261 | return ServiceList; 262 | } 263 | 264 | /*----------------------------------------------------------------------------*/ 265 | int XMLFindAndParseService(IXML_Document* DescDoc, const char* location, 266 | const char* serviceTypeBase, char** serviceType, char** serviceId, 267 | char** eventURL, char** controlURL, char** serviceURL) { 268 | int found = 0; 269 | int ret; 270 | const char* base = NULL; 271 | bool contd = true; 272 | 273 | char* baseURL = XMLGetFirstDocumentItem(DescDoc, "URLBase", true); 274 | if (baseURL) base = baseURL; 275 | else base = location; 276 | 277 | for (unsigned int sindex = 0; contd; sindex++) { 278 | char* tempServiceType = NULL; 279 | char* relcontrolURL = NULL; 280 | char* releventURL = NULL; 281 | IXML_Element* service = NULL; 282 | IXML_NodeList* serviceList = NULL; 283 | 284 | if ((serviceList = XMLGetNthServiceList(DescDoc, sindex, &contd)) == NULL) continue; 285 | unsigned long length = ixmlNodeList_length(serviceList); 286 | for (int i = 0; i < length; i++) { 287 | service = (IXML_Element*)ixmlNodeList_item(serviceList, i); 288 | tempServiceType = XMLGetFirstElementItem((IXML_Element*)service, "serviceType"); 289 | LOG_SDEBUG("serviceType %s", tempServiceType); 290 | 291 | // remove version from service type 292 | *strrchr(tempServiceType, ':') = '\0'; 293 | if (tempServiceType && strcmp(tempServiceType, serviceTypeBase) == 0) { 294 | NFREE(*serviceURL); 295 | *serviceURL = XMLGetFirstElementItem((IXML_Element*)service, "SCPDURL"); 296 | NFREE(*serviceType); 297 | *serviceType = XMLGetFirstElementItem((IXML_Element*)service, "serviceType"); 298 | NFREE(*serviceId); 299 | *serviceId = XMLGetFirstElementItem(service, "serviceId"); 300 | LOG_SDEBUG("Service %s, serviceId: %s", serviceType, *serviceId); 301 | relcontrolURL = XMLGetFirstElementItem(service, "controlURL"); 302 | releventURL = XMLGetFirstElementItem(service, "eventSubURL"); 303 | NFREE(*controlURL); 304 | *controlURL = (char*)malloc(strlen(base) + strlen(relcontrolURL) + 1 + 10); 305 | if (*controlURL) { 306 | ret = UpnpResolveURL(base, relcontrolURL, *controlURL); 307 | if (ret != UPNP_E_SUCCESS) LOG_ERROR("Error generating controlURL from %s + %s", base, relcontrolURL); 308 | } 309 | NFREE(*eventURL); 310 | *eventURL = (char*)malloc(strlen(base) + strlen(releventURL) + 1 + 10); 311 | if (*eventURL) { 312 | ret = UpnpResolveURL(base, releventURL, *eventURL); 313 | if (ret != UPNP_E_SUCCESS) LOG_ERROR("Error generating eventURL from %s + %s", base, releventURL); 314 | } 315 | free(relcontrolURL); 316 | free(releventURL); 317 | found = 1; 318 | break; 319 | } 320 | free(tempServiceType); 321 | tempServiceType = NULL; 322 | } 323 | free(tempServiceType); 324 | if (serviceList) ixmlNodeList_free(serviceList); 325 | } 326 | 327 | free(baseURL); 328 | return found; 329 | } 330 | 331 | /*----------------------------------------------------------------------------*/ 332 | bool XMLFindAction(const char* base, char* service, char* action) { 333 | char* url = malloc(strlen(base) + strlen(service) + 1 + 10); 334 | IXML_Document* AVTDoc = NULL; 335 | bool res = false; 336 | 337 | UpnpResolveURL(base, service, url); 338 | 339 | if (UpnpDownloadXmlDoc(url, &AVTDoc) == UPNP_E_SUCCESS) { 340 | IXML_Element* actions = ixmlDocument_getElementById(AVTDoc, "actionList"); 341 | IXML_NodeList* actionList = ixmlDocument_getElementsByTagName((IXML_Document*)actions, "action"); 342 | 343 | for (int i = 0; actionList && i < (int)ixmlNodeList_length(actionList); i++) { 344 | IXML_Node* node = ixmlNodeList_item(actionList, i); 345 | node = (IXML_Node*)ixmlDocument_getElementById((IXML_Document*)node, "name"); 346 | node = ixmlNode_getFirstChild(node); 347 | const char* name = ixmlNode_getNodeValue(node); 348 | if (name && !strcasecmp(name, action)) { 349 | res = true; 350 | break; 351 | } 352 | } 353 | ixmlNodeList_free(actionList); 354 | } 355 | 356 | free(url); 357 | ixmlDocument_free(AVTDoc); 358 | 359 | return res; 360 | } 361 | 362 | /*----------------------------------------------------------------------------*/ 363 | char *XMLGetChangeItem(IXML_Document *doc, char *Tag, char *SearchAttr, char *SearchVal, char *RetAttr) { 364 | char *ret = NULL; 365 | 366 | IXML_Element* LastChange = ixmlDocument_getElementById(doc, "LastChange"); 367 | if (!LastChange) return NULL; 368 | 369 | IXML_Node* node = ixmlNode_getFirstChild((IXML_Node*) LastChange); 370 | if (!node) return NULL; 371 | 372 | char* buf = (char*) ixmlNode_getNodeValue(node); 373 | if (!buf) return NULL; 374 | 375 | IXML_Document* ItemDoc = ixmlParseBuffer(buf); 376 | if (!ItemDoc) return NULL; 377 | 378 | IXML_NodeList* List = ixmlDocument_getElementsByTagName(ItemDoc, Tag); 379 | if (!List) { 380 | ixmlDocument_free(ItemDoc); 381 | return NULL; 382 | } 383 | 384 | for (unsigned i = 0; i < ixmlNodeList_length(List); i++) { 385 | IXML_Node *node = ixmlNodeList_item(List, i); 386 | IXML_Node *attr = _getAttributeNode(node, SearchAttr); 387 | 388 | if (!attr) continue; 389 | 390 | if (!strcasecmp(ixmlNode_getNodeValue(attr), SearchVal)) { 391 | if ((node = ixmlNode_getNextSibling(attr)) == NULL) 392 | if ((node = ixmlNode_getPreviousSibling(attr)) == NULL) continue; 393 | if (!strcasecmp(ixmlNode_getNodeName(node), "val")) { 394 | ret = strdup(ixmlNode_getNodeValue(node)); 395 | break; 396 | } 397 | } 398 | } 399 | 400 | ixmlNodeList_free(List); 401 | ixmlDocument_free(ItemDoc); 402 | 403 | return ret; 404 | } 405 | 406 | /*----------------------------------------------------------------------------*/ 407 | static IXML_Node *_getAttributeNode(IXML_Node *node, char *SearchAttr) { 408 | IXML_Node *ret = NULL; 409 | IXML_NamedNodeMap *map = ixmlNode_getAttributes(node); 410 | 411 | /* 412 | supposed to act like but case insensitive 413 | ixmlElement_getAttributeNode((IXML_Element*) node, SearchAttr); 414 | */ 415 | 416 | for (int i = 0; i < ixmlNamedNodeMap_getLength(map); i++) { 417 | ret = ixmlNamedNodeMap_item(map, i); 418 | if (strcasecmp(ixmlNode_getNodeName(ret), SearchAttr)) ret = NULL; 419 | else break; 420 | } 421 | 422 | ixmlNamedNodeMap_free(map); 423 | 424 | return ret; 425 | } 426 | 427 | /*----------------------------------------------------------------------------*/ 428 | char *uPNPEvent2String(Upnp_EventType S) { 429 | switch (S) { 430 | /* Discovery */ 431 | case UPNP_DISCOVERY_ADVERTISEMENT_ALIVE: 432 | return "UPNP_DISCOVERY_ADVERTISEMENT_ALIVE"; 433 | case UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE: 434 | return "UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE"; 435 | case UPNP_DISCOVERY_SEARCH_RESULT: 436 | return "UPNP_DISCOVERY_SEARCH_RESULT"; 437 | case UPNP_DISCOVERY_SEARCH_TIMEOUT: 438 | return "UPNP_DISCOVERY_SEARCH_TIMEOUT"; 439 | /* SOAP */ 440 | case UPNP_CONTROL_ACTION_REQUEST: 441 | return "UPNP_CONTROL_ACTION_REQUEST"; 442 | case UPNP_CONTROL_ACTION_COMPLETE: 443 | return "UPNP_CONTROL_ACTION_COMPLETE"; 444 | case UPNP_CONTROL_GET_VAR_REQUEST: 445 | return "UPNP_CONTROL_GET_VAR_REQUEST"; 446 | case UPNP_CONTROL_GET_VAR_COMPLETE: 447 | return "UPNP_CONTROL_GET_VAR_COMPLETE"; 448 | case UPNP_EVENT_SUBSCRIPTION_REQUEST: 449 | return "UPNP_EVENT_SUBSCRIPTION_REQUEST"; 450 | case UPNP_EVENT_RECEIVED: 451 | return "UPNP_EVENT_RECEIVED"; 452 | case UPNP_EVENT_RENEWAL_COMPLETE: 453 | return "UPNP_EVENT_RENEWAL_COMPLETE"; 454 | case UPNP_EVENT_SUBSCRIBE_COMPLETE: 455 | return "UPNP_EVENT_SUBSCRIBE_COMPLETE"; 456 | case UPNP_EVENT_UNSUBSCRIBE_COMPLETE: 457 | return "UPNP_EVENT_UNSUBSCRIBE_COMPLETE"; 458 | case UPNP_EVENT_AUTORENEWAL_FAILED: 459 | return "UPNP_EVENT_AUTORENEWAL_FAILED"; 460 | case UPNP_EVENT_SUBSCRIPTION_EXPIRED: 461 | return "UPNP_EVENT_SUBSCRIPTION_EXPIRED"; 462 | } 463 | 464 | return ""; 465 | } 466 | -------------------------------------------------------------------------------- /airupnp/src/mr_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * UPnP renderer utils 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | */ 9 | 10 | #pragma once 11 | 12 | #include "airupnp.h" 13 | 14 | void FlushMRDevices(void); 15 | void DelMRDevice(struct sMR *p); 16 | struct sMR *GetMaster(struct sMR *Device, char **Name); 17 | int CalcGroupVolume(struct sMR *Master); 18 | bool CheckAndLock(struct sMR *Device); 19 | double GetLocalGroupVolume(struct sMR *Member, int *count); 20 | 21 | struct sMR* SID2Device(const UpnpString *SID); 22 | struct sMR* CURL2Device(const UpnpString *CtrlURL); 23 | struct sMR* PURL2Device(const UpnpString *URL); 24 | struct sMR* UDN2Device(const char *SID); 25 | 26 | struct sService* EventURL2Service(const UpnpString *URL, struct sService *s); 27 | 28 | int XMLFindAndParseService(IXML_Document* DescDoc, const char* location, const char* serviceTypeBase, char** serviceType, 29 | char** serviceId, char** eventURL, char** controlURL, char** serviceURL); 30 | bool XMLFindAction(const char* base, char* service, char* action); 31 | char* XMLGetChangeItem(IXML_Document *doc, char *Tag, char *SearchAttr, char *SearchVal, char *RetAttr); 32 | 33 | char* uPNPEvent2String(Upnp_EventType S); 34 | 35 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | setlocal 2 | 3 | call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat" 4 | 5 | 6 | if /I [%1] == [rebuild] ( 7 | set option="-t:Rebuild" 8 | ) 9 | 10 | msbuild AirConnect.sln /property:Configuration=Release %option% 11 | msbuild AirConnect.sln /property:Configuration=Static %option% 12 | 13 | endlocal -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | list="x86_64-linux-gnu-gcc x86-linux-gnu-gcc arm-linux-gnueabi-gcc aarch64-linux-gnu-gcc \ 4 | sparc64-linux-gnu-gcc mips-linux-gnu-gcc mipsel-linux-gnu-gcc powerpc-linux-gnu-gcc x86_64-macos-darwin-gcc \ 5 | arm64-macos-darwin-cc x86_64-freebsd-gnu-gcc x86_64-solaris-gnu-gcc armv6-linux-gnueabi-gcc \ 6 | armv5-linux-gnueabi-gcc" 7 | 8 | declare -A alias=( [x86-linux-gnu-gcc]=i686-stretch-linux-gnu-gcc \ 9 | [x86_64-linux-gnu-gcc]=x86_64-stretch-linux-gnu-gcc \ 10 | [arm-linux-gnueabi-gcc]=armv7-stretch-linux-gnueabi-gcc \ 11 | [armv5-linux-gnueabi-gcc]=armv6-stretch-linux-gnueabi-gcc \ 12 | [armv6-linux-gnueabi-gcc]=armv6-stretch-linux-gnueabi-gcc \ 13 | [aarch64-linux-gnu-gcc]=aarch64-stretch-linux-gnu-gcc \ 14 | [sparc64-linux-gnu-gcc]=sparc64-stretch-linux-gnu-gcc \ 15 | [mips-linux-gnu-gcc]=mips64-stretch-linux-gnu-gcc \ 16 | [mipsel-linux-gnu-gcc]=mips64el-stretch-linux-gnu-gcc \ 17 | [powerpc-linux-gnu-gcc]=powerpc64-stretch-linux-gnu-gcc \ 18 | [x86_64-macos-darwin-gcc]=x86_64-apple-darwin19-gcc \ 19 | [arm64-macos-darwin-cc]=arm64-apple-darwin20.4-cc \ 20 | [x86_64-freebsd-gnu-gcc]=x86_64-cross-freebsd12.3-gcc \ 21 | [x86_64-solaris-gnu-gcc]=x86_64-cross-solaris2.x-gcc ) 22 | 23 | declare -A cflags=( [sparc64-linux-gnu-gcc]="-mcpu=v7" \ 24 | [mips-linux-gnu-gcc]="-march=mips32" \ 25 | [mipsel-linux-gnu-gcc]="-march=mips32" \ 26 | [armv5-linux-gnueabi-gcc]="-march=armv5t -mfloat-abi=soft" \ 27 | [powerpc-linux-gnu-gcc]="-m32" ) 28 | 29 | declare -a compilers 30 | 31 | IFS= read -ra candidates <<< "$list" 32 | 33 | # do we have "clean" somewhere in parameters (assuming no compiler has "clean" in it... 34 | if [[ $@[*]} =~ clean ]]; then 35 | clean="clean" 36 | fi 37 | 38 | # first select platforms/compilers 39 | for cc in ${candidates[@]}; do 40 | # check compiler first 41 | if ! command -v ${alias[$cc]:-$cc} &> /dev/null; then 42 | if command -v $cc &> /dev/null; then 43 | unset alias[$cc] 44 | else 45 | continue 46 | fi 47 | fi 48 | 49 | if [[ $# == 0 || ($# == 1 && -n $clean) ]]; then 50 | compilers+=($cc) 51 | continue 52 | fi 53 | 54 | for arg in $@ 55 | do 56 | if [[ $cc =~ $arg ]]; then 57 | compilers+=($cc) 58 | fi 59 | done 60 | done 61 | 62 | # then do the work 63 | for cc in ${compilers[@]} 64 | do 65 | IFS=- read -r platform host dummy <<< $cc 66 | 67 | export CFLAGS=${cflags[$cc]} 68 | export CC=${alias[$cc]:-$cc} 69 | 70 | # don't let clang create temp files 71 | if [[ $CC =~ -cc ]]; then 72 | CFLAGS+="-fno-temp-file" 73 | fi 74 | 75 | make CC=$CC HOST=$host PLATFORM=$platform $clean -j8 76 | 77 | if [[ -n $clean ]]; then 78 | continue 79 | fi 80 | done 81 | -------------------------------------------------------------------------------- /buildall.sh: -------------------------------------------------------------------------------- 1 | cd airupnp && ../build.sh $1 $2 && cd .. 2 | cd aircast && ../build.sh $1 $2 && cd .. 3 | -------------------------------------------------------------------------------- /common/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /common/metadata.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Metadata instance 3 | * 4 | * (c) Philippe, philippe_44@outlook.com 5 | * 6 | * See LICENSE 7 | * 8 | * 9 | */ 10 | 11 | #pragma once 12 | 13 | #include 14 | 15 | typedef struct metadata_s { 16 | char* artist; 17 | char* album; 18 | char* title; 19 | char* remote_title; 20 | char* artwork; 21 | char *genre; 22 | uint32_t track, disc; 23 | uint32_t duration, live_duration; 24 | uint32_t sample_rate; 25 | uint8_t sample_size; 26 | uint8_t channels; 27 | } metadata_t; 28 | -------------------------------------------------------------------------------- /updater: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #AirConnect Updater - Pull latest Update from https://github.com/philippe44/AirConnect 3 | #AirConnect by philippe44 - updater script by FaserF https://github.com/FaserF 4 | 5 | #Variables 6 | newversion=0000 7 | currentversion=0000 8 | state=NoLocalInstallationFound 9 | currentdirectory=$PWD 10 | tmpfolder=/tmp/AirConnect 11 | backupfolder=$tmpfolder/backup 12 | downloadfolder=$tmpfolder/download 13 | 14 | 15 | function checkLocalInstallation() { 16 | if [ -f "CHANGELOG" ] && [ -f "airupnp.service" ] 17 | then 18 | echo "Current installation detected" 19 | 20 | #Remeber current install directory 21 | currentdirectory=$PWD 22 | 23 | #Set state 24 | state=LocalInstallationFound 25 | fi 26 | if [ -f "$currentdirectory/AirConnect/CHANGELOG" ] && [ -f "$currentdirectory/AirConnect/airupnp.service" ] 27 | then 28 | echo "Current installation detected in subfolder AirConnect" 29 | 30 | #Remeber current install directory 31 | currentdirectory=$PWD/AirConnect 32 | 33 | #Set state 34 | state=LocalInstallationFound 35 | fi 36 | } 37 | 38 | 39 | function cleanInstall() { 40 | echo "First installation..." 41 | 42 | #Ask for target installation directory 43 | read -p "Enter full path where to install: " targetdirectory 44 | 45 | if test -z "targetdirectory" 46 | then 47 | # install to current path if nothing is entered 48 | targetdirectory=$PWD 49 | fi 50 | 51 | if [ "targetdirectory" = "/" ] 52 | then 53 | echo "Error: Can't install at root" 54 | exit 1 55 | fi 56 | 57 | #Create target directory 58 | if [ ! -d "$targetdirectory" ] 59 | then 60 | mkdir "$targetdirectory" 61 | fi 62 | 63 | #Install newest version 64 | echo "----" 65 | echo "Installing newest version..." 66 | echo "----" 67 | git clone --depth=1 https://github.com/philippe44/AirConnect.git $targetdirectory 2>&1 >/dev/null 68 | } 69 | 70 | 71 | function prepareTmpFolder() { 72 | #Delete previous folder if still exist 73 | if [ -d "$tmpfolder" ] 74 | then 75 | rm -rf $tmpfolder 76 | fi 77 | 78 | #Create new empty tmp folders 79 | mkdir $tmpfolder 80 | mkdir $tmpfolder/backup 81 | mkdir $tmpfolder/download 82 | } 83 | 84 | 85 | function searchNewVersion() { 86 | #Checking for new Updates 87 | echo "----" 88 | echo "Checking for new updates" 89 | echo "----" 90 | 91 | #Download online changelog 92 | download https://raw.githubusercontent.com/philippe44/AirConnect/master/CHANGELOG $tmpfolder/CHANGELOG 93 | 94 | #Calculate online version 95 | newversion=$(head -n 1 $tmpfolder/CHANGELOG) 96 | newversion=${newversion//[-._]} 97 | 98 | #Delete online changelog 99 | rm $tmpfolder/CHANGELOG 100 | 101 | #Grep current version number 102 | currentversion=$(head -n 1 $currentdirectory/CHANGELOG) 103 | currentversion=${currentversion//[-._]} 104 | 105 | if [ "$newversion" -gt "$currentversion" ] 106 | then 107 | echo "Found new version: $newversion" 108 | state=NewVersionFoundOnGithub 109 | else 110 | echo "No new update found" 111 | fi 112 | } 113 | 114 | 115 | function backupCurrentVersion() { 116 | echo "----" 117 | echo "Backing up old version..." 118 | mv $currentdirectory/* $tmpfolder/backup 119 | } 120 | 121 | 122 | function updateCurrentVersion() { 123 | #Remove previous version files 124 | rm -rf $currentdirectory/* 125 | 126 | #Download newest Version 127 | echo "----" 128 | echo "Update version..." 129 | echo "----" 130 | git clone --depth=1 https://github.com/philippe44/AirConnect.git $downloadfolder 2>&1 >/dev/null 131 | 132 | #Install 133 | mv $downloadfolder/* $currentdirectory 134 | } 135 | 136 | 137 | function validateUpdatedVersion() { 138 | versioncheck=$(head -n 1 $currentdirectory/CHANGELOG) 139 | versioncheck=${versioncheck//[-._]} 140 | 141 | if [ "$versioncheck" -eq "$newversion" ] 142 | then 143 | echo "----" 144 | echo "Update done" 145 | echo "----" 146 | echo "New Version: $versioncheck" 147 | else 148 | echo "----" 149 | echo "Update failed" 150 | echo "----" 151 | echo "Current Version: $currentversion" 152 | fi 153 | 154 | echo "-----" 155 | echo "Old version backup in: $backupfolder" 156 | } 157 | 158 | 159 | function download() { 160 | url=$1 161 | filename=$2 162 | 163 | if [ -x "$(which wget)" ] ; then 164 | wget -q $url -O $2 165 | elif [ -x "$(which curl)" ]; then 166 | curl -o $2 -sfL $url 167 | else 168 | echo "Could not find curl or wget, please install one." >&2 169 | fi 170 | } 171 | 172 | 173 | # 174 | # Main 175 | ######################################### 176 | # 177 | # 178 | 179 | #Check if AirConnect is already installed 180 | checkLocalInstallation 181 | 182 | if [ $state = "NoLocalInstallationFound" ] 183 | then 184 | #Install new version 185 | cleanInstall 186 | else 187 | #Create needed folder 188 | prepareTmpFolder 189 | 190 | #Check if a new version is available 191 | searchNewVersion 192 | 193 | if [ $state = "NewVersionFoundOnGithub" ] 194 | then 195 | #Backup Current Installation 196 | backupCurrentVersion 197 | 198 | #Update Current Installation 199 | updateCurrentVersion 200 | 201 | #Validate Version 202 | validateUpdatedVersion 203 | fi 204 | fi 205 | --------------------------------------------------------------------------------