98_DLNARenderer.pm 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725
  1. ############################################################################
  2. # Author: dominik.karall@gmail.com
  3. # $Id: 98_DLNARenderer.pm 12169 2016-09-18 16:03:37Z dominikkarall $
  4. #
  5. # v2.0.3 - 20160918
  6. # - BUGFIX: fixed SyncPlay for CaskeId players
  7. #
  8. # v2.0.2 - 20160913
  9. # - BUGFIX: fixed pauseToggle (thx@MattG)
  10. # - BUGFIX: fixed next/previous (thx@MattG)
  11. #
  12. # v2.0.1 - 20160725
  13. # - FEATURE: support DIDL-Lite in channel_X attribute (thx@Weissbrotgrill)
  14. # - FEATURE: automatically generate DIDL-Lite based on URI (thx@Weissbrotgrill)
  15. # - CHANGE: update CommandRef perl library requirements
  16. # - BUGFIX: fix ignoreUDNs crash when device gets removed
  17. #
  18. # v2.0.0 - 20160718
  19. # - CHANGE: first official release within fhem repository
  20. # - BUGFIX: support device events without / at the end of the xmlns (thx@MichaelT)
  21. # - FEATURE: support defaultRoom attribute, defines the room to which new devices are assigned
  22. #
  23. # v2.0.0 RC5 - 20160614
  24. # - BUGFIX: support events from devices with wrong serviceId
  25. # - BUGFIX: fix perl warning on startup
  26. # - BUGFIX: fix error if LastChange event is empty
  27. #
  28. # v2.0.0 RC4 - 20160613
  29. # - FEATURE: support devices with wrong serviceId
  30. # - BUGFIX: fix crash during stereo mode update for caskeid players
  31. # - FEATURE: add stereoPairName reading
  32. # - CHANGE: add version string to main device internals
  33. # - BUGFIX: fix error when UPnP method is not implemented
  34. # - FEATURE: identify stereo support (reading: stereoSupport)
  35. #
  36. # v2.0.0 RC3 - 20160609
  37. # - BUGFIX: check correct number of params for all commands
  38. # - BUGFIX: fix addUnitToSession/removeUnitFromSession for MUNET/Caskeid devices
  39. # - BUGFIX: support devices with non-standard UUIDs
  40. # - CHANGE: use BlockingCall for subscription renewal
  41. # - CHANGE: remove ignoreUDNs attribute from play devices
  42. # - CHANGE: remove multiRoomGroups attribute from main device
  43. # - CHANGE: split stereoDevices reading into stereoLeft/stereoRight
  44. # - FEATURE: support multiRoomVolume to change volume of all group speakers e.g.
  45. # set <name> multiRoomVolume +10
  46. # set <name> multiRoomVolume 25
  47. # - FEATURE: support channel_01-10 attribute
  48. # attr <name> channel_01 http://... (save URI to channel_01)
  49. # set <name> channel 1 (play channel_01)
  50. # - FEATURE: support speak functionality via Google Translate
  51. # set <name> speak "This is a test."
  52. # attr <name> ttsLanguage de
  53. # set <name> speak "Das ist ein Test."
  54. # - FEATURE: automatically retrieve stereo mode from speakers and update stereoId/Left/Right readings
  55. # - FEATURE: support mute
  56. # set <name> mute on/off
  57. #
  58. # v2.0.0 RC2 - 20160510
  59. # - BUGFIX: fix multiroom for MUNET/Caskeid devices
  60. #
  61. # v2.0.0 RC1 - 20160509
  62. # - CHANGE: change state to offline/playing/stopped/paused/online
  63. # - CHANGE: removed on/off devstateicon on creation due to changed state values
  64. # - CHANGE: play is NOT setting AVTransport any more
  65. # - CHANGE: code cleanup
  66. # - CHANGE: handle socket via fhem main loop instead of InternalTimer
  67. # - BUGFIX: do not create new search objects every 30 minutes
  68. # - FEATURE: support pauseToggle
  69. # - FEATURE: support SetExtensions (on-for-timer, off-for-timer, ...)
  70. # - FEATURE: support relative volume changes (e.g. set <device> volume +10)
  71. #
  72. # v2.0.0 BETA3 - 20160504
  73. # - BUGFIX: XML parsing error "NOT_IMPLEMENTED"
  74. # - CHANGE: change readings to lowcaseUppercase format
  75. # - FEATURE: support pause
  76. # - FEATURE: support seek REL_TIME
  77. # - FEATURE: support next/prev
  78. #
  79. # v2.0.0 BETA2 - 20160403
  80. # - FEATURE: support events from DLNA devices
  81. # - FEATURE: support caskeid group definitions
  82. # set <name> saveGroupAs Bad
  83. # set <name> loadGroup Bad
  84. # - FEATURE: support caskeid stereo mode
  85. # set <name> stereo MUNET1 MUNET2 MunetStereoPaar
  86. # set <name> standalone
  87. # - CHANGE: use UPnP::ControlPoint from FHEM library
  88. # - BUGFIX: fix presence status
  89. #
  90. # v2.0.0 BETA1 - 20160321
  91. # - FEATURE: autodiscover and autocreate DLNA devices
  92. # just use "define dlnadevices DLNARenderer" and wait 2 minutes
  93. # - FEATURE: support Caskeid (e.g. MUNET devices) with following commands
  94. # set <name> playEverywhere
  95. # set <name> stopPlayEverywhere
  96. # set <name> addUnit <UNIT>
  97. # set <name> removeUnit <UNIT>
  98. # set <name> enableBTCaskeid
  99. # set <name> disableBTCaskeid
  100. # - FEATURE: display multiroom speakers in multiRoomUnits reading
  101. # - FEATURE: automatically set alias for friendlyname
  102. # - FEATURE: automatically set webCmd volume
  103. # - FEATURE: automatically set devStateIcon audio icons
  104. # - FEATURE: ignoreUDNs attribute in main
  105. # - FEATURE: scanInterval attribute in main
  106. #
  107. # DLNA Module to play given URLs on a DLNA Renderer
  108. # and control their volume. Just define
  109. # define dlnadevices DLNARenderer
  110. # and look for devices in Unsorted section after 2 minutes.
  111. #
  112. #TODO
  113. # - speak: support continue stream after speak finished
  114. # - redesign multiroom functionality (virtual devices: represent the readings of master device
  115. # and send the commands only to the master device (except volume?)
  116. # automatically create group before playing
  117. # - use bulk update for readings
  118. #
  119. ############################################################################
  120. package main;
  121. use strict;
  122. use warnings;
  123. use Blocking;
  124. use SetExtensions;
  125. use HTML::Entities;
  126. use XML::Simple;
  127. use Data::Dumper;
  128. use Data::UUID;
  129. use LWP::UserAgent;
  130. #get UPnP::ControlPoint loaded properly
  131. my $gPath = '';
  132. BEGIN {
  133. $gPath = substr($0, 0, rindex($0, '/'));
  134. }
  135. if (lc(substr($0, -7)) eq 'fhem.pl') {
  136. $gPath = $attr{global}{modpath}.'/FHEM';
  137. }
  138. use lib ($gPath.'/lib', $gPath.'/FHEM/lib', './FHEM/lib', './lib', './FHEM', './', '/usr/local/FHEM/share/fhem/FHEM/lib');
  139. use UPnP::ControlPoint;
  140. sub DLNARenderer_Initialize($) {
  141. my ($hash) = @_;
  142. $hash->{SetFn} = "DLNARenderer_Set";
  143. $hash->{DefFn} = "DLNARenderer_Define";
  144. $hash->{ReadFn} = "DLNARenderer_Read";
  145. $hash->{UndefFn} = "DLNARenderer_Undef";
  146. $hash->{AttrFn} = "DLNARenderer_Attribute";
  147. }
  148. sub DLNARenderer_Attribute {
  149. my ($mode, $devName, $attrName, $attrValue) = @_;
  150. #ignoreUDNs, multiRoomGroups, channel_01-10
  151. if($mode eq "set") {
  152. } elsif($mode eq "del") {
  153. }
  154. return undef;
  155. }
  156. sub DLNARenderer_Define($$) {
  157. my ($hash, $def) = @_;
  158. my @param = split("[ \t][ \t]*", $def);
  159. #init caskeid clients for multiroom
  160. $hash->{helper}{caskeidClients} = "";
  161. $hash->{helper}{caskeid} = 0;
  162. if(@param < 3) {
  163. #main
  164. $hash->{UDN} = 0;
  165. my $VERSION = "v2.0.3";
  166. $hash->{VERSION} = $VERSION;
  167. Log3 $hash, 3, "DLNARenderer: DLNA Renderer $VERSION";
  168. DLNARenderer_setupControlpoint($hash);
  169. DLNARenderer_startDlnaRendererSearch($hash);
  170. readingsSingleUpdate($hash,"state","initialized",1);
  171. addToDevAttrList($hash->{NAME}, "ignoreUDNs");
  172. addToDevAttrList($hash->{NAME}, "defaultRoom");
  173. return undef;
  174. }
  175. #device specific
  176. my $name = shift @param;
  177. my $type = shift @param;
  178. my $udn = shift @param;
  179. $hash->{UDN} = $udn;
  180. readingsSingleUpdate($hash,"presence","offline",1);
  181. readingsSingleUpdate($hash,"state","offline",1);
  182. addToDevAttrList($hash->{NAME}, "multiRoomGroups");
  183. addToDevAttrList($hash->{NAME}, "ttsLanguage");
  184. addToDevAttrList($hash->{NAME}, "channel_01");
  185. addToDevAttrList($hash->{NAME}, "channel_02");
  186. addToDevAttrList($hash->{NAME}, "channel_03");
  187. addToDevAttrList($hash->{NAME}, "channel_04");
  188. addToDevAttrList($hash->{NAME}, "channel_05");
  189. addToDevAttrList($hash->{NAME}, "channel_06");
  190. addToDevAttrList($hash->{NAME}, "channel_07");
  191. addToDevAttrList($hash->{NAME}, "channel_08");
  192. addToDevAttrList($hash->{NAME}, "channel_09");
  193. addToDevAttrList($hash->{NAME}, "channel_10");
  194. InternalTimer(gettimeofday() + 200, 'DLNARenderer_renewSubscriptions', $hash, 0);
  195. InternalTimer(gettimeofday() + 60, 'DLNARenderer_updateStereoMode', $hash, 0);
  196. return undef;
  197. }
  198. sub DLNARenderer_Undef($) {
  199. my ($hash) = @_;
  200. RemoveInternalTimer($hash);
  201. return undef;
  202. }
  203. sub DLNARenderer_Read($) {
  204. my ($hash) = @_;
  205. my $name = $hash->{NAME};
  206. my $phash = $hash->{phash};
  207. my $cp = $phash->{helper}{controlpoint};
  208. eval {
  209. $cp->handleOnce($hash->{CD});
  210. };
  211. if($@) {
  212. Log3 $hash, 3, "DLNARenderer: handleOnce failed, $@";
  213. }
  214. return undef;
  215. }
  216. sub DLNARenderer_Set($@) {
  217. my ($hash, $name, @params) = @_;
  218. my $dev = $hash->{helper}{device};
  219. # check parameters
  220. return "no set value specified" if(int(@params) < 1);
  221. my $ctrlParam = shift(@params);
  222. # check device presence
  223. if ($ctrlParam ne "?" and (!defined($dev) or ReadingsVal($hash->{NAME}, "presence", "") eq "offline")) {
  224. return "DLNARenderer: Currently searching for device...";
  225. }
  226. #get quoted text from params
  227. my $blankParams = join(" ", @params);
  228. my @params2;
  229. while($blankParams =~ /"?((?<!")\S+(?<!")|[^"]+)"?\s*/g) {
  230. push(@params2, $1);
  231. }
  232. @params = @params2;
  233. my $set_method_mapping = {
  234. volume => {method => \&DLNARenderer_volume, args => 1, argdef => "slider,0,1,100"},
  235. mute => {method => \&DLNARenderer_mute, args => 1, argdef => "on,off"},
  236. pause => {method => \&DLNARenderer_upnpPause, args => 0},
  237. pauseToggle => {method => \&DLNARenderer_pauseToggle, args => 0},
  238. play => {method => \&DLNARenderer_play, args => 0},
  239. next => {method => \&DLNARenderer_upnpNext, args => 0},
  240. previous => {method => \&DLNARenderer_upnpPrevious, args => 0},
  241. seek => {method => \&DLNARenderer_seek, args => 1},
  242. multiRoomVolume => {method => \&DLNARenderer_setMultiRoomVolume, args => 1, argdef => "slider,0,1,100", caskeid => 1},
  243. stereo => {method => \&DLNARenderer_setStereoMode, args => 3, caskeid => 1},
  244. standalone => {method => \&DLNARenderer_setStandaloneMode, args => 0, caskeid => 1},
  245. playEverywhere => {method => \&DLNARenderer_playEverywhere, args => 0, caskeid => 1},
  246. stopPlayEverywhere => {method => \&DLNARenderer_stopPlayEverywhere, args => 0, caskeid => 1},
  247. addUnit => {method => \&DLNARenderer_addUnit, args => 1, argdef => $hash->{helper}{caskeidClients}, caskeid => 1},
  248. removeUnit => {method => \&DLNARenderer_removeUnit, args => 1, argdef => ReadingsVal($hash->{NAME}, "multiRoomUnits", ""), caskeid => 1},
  249. saveGroupAs => {method => \&DLNARenderer_saveGroupAs, args => 1, caskeid => 1},
  250. enableBTCaskeid => {method => \&DLNARenderer_enableBTCaskeid, args => 0, caskeid => 1},
  251. disableBTCaskeid => {method => \&DLNARenderer_disableBTCaskeid, args => 0, caskeid => 1},
  252. off => {method => \&DLNARenderer_upnpStop, args => 0},
  253. stop => {method => \&DLNARenderer_upnpStop, args => 0},
  254. loadGroup => {method => \&DLNARenderer_loadGroup, args => 1, caskeid => 1},
  255. on => {method => \&DLNARenderer_on, args => 0},
  256. stream => {method => \&DLNARenderer_stream, args => 1},
  257. channel => {method => \&DLNARenderer_channel, args => 1, argdef => "1,2,3,4,5,6,7,8,9,10"},
  258. speak => {method => \&DLNARenderer_speak, args => 1}
  259. };
  260. if($set_method_mapping->{$ctrlParam}) {
  261. if($set_method_mapping->{$ctrlParam}{args} != int(@params)) {
  262. return "DLNARenderer: $ctrlParam requires $set_method_mapping->{$ctrlParam}{args} parameter.";
  263. }
  264. #params array till args number
  265. my @args = @params[0 .. $set_method_mapping->{$ctrlParam}{args}];
  266. $set_method_mapping->{$ctrlParam}{method}->($hash, @args);
  267. } else {
  268. my $cmdList;
  269. foreach my $cmd (keys %$set_method_mapping) {
  270. next if($hash->{helper}{caskeid} == 0 && $set_method_mapping->{$cmd}{caskeid} && $set_method_mapping->{$cmd}{caskeid} == 1);
  271. if($set_method_mapping->{$cmd}{args} == 0) {
  272. $cmdList .= $cmd.":noArg ";
  273. } else {
  274. if($set_method_mapping->{$cmd}{argdef}) {
  275. $cmdList .= $cmd.":".$set_method_mapping->{$cmd}{argdef}." ";
  276. } else {
  277. $cmdList .= $cmd." ";
  278. }
  279. }
  280. }
  281. return SetExtensions($hash, $cmdList, $name, $ctrlParam, @params);
  282. }
  283. return undef;
  284. }
  285. ##############################
  286. ##### SET FUNCTIONS ##########
  287. ##############################
  288. sub DLNARenderer_speak {
  289. my ($hash, $ttsText) = @_;
  290. my $ttsLang = AttrVal($hash->{NAME}, "ttsLanguage", "en");
  291. return "DLNARenderer: Maximum text length is 100 characters." if(length($ttsText) > 100);
  292. DLNARenderer_stream($hash, "http://translate.google.com/translate_tts?tl=$ttsLang&client=tw-ob&q=$ttsText", "");
  293. }
  294. sub DLNARenderer_channel {
  295. my ($hash, $channelNr) = @_;
  296. my $stream = AttrVal($hash->{NAME}, sprintf("channel_%02d", $channelNr), "");
  297. if($stream eq "") {
  298. return "DLNARenderer: Set channel_XX attribute first.";
  299. }
  300. my $meta = "";
  301. if (substr($stream,0,10) eq "<DIDL-Lite") {
  302. eval {
  303. my $xml = XMLin($stream);
  304. $meta = $stream;
  305. $stream = $xml->{"item"}{"res"}{"content"};
  306. };
  307. if($@) {
  308. Log3 $hash, 2, "DLNARenderer: Incorrect DIDL-Lite format, $@";
  309. }
  310. }
  311. DLNARenderer_stream($hash, $stream, $meta);
  312. readingsSingleUpdate($hash, "channel", $channelNr, 1);
  313. }
  314. sub DLNARenderer_stream {
  315. my ($hash, $stream, $meta) = @_;
  316. if (!defined($meta)) {
  317. DLNARenderer_generateDidlLiteAndPlay($hash, $stream);
  318. return undef;
  319. }
  320. DLNARenderer_upnpSetAVTransportURI($hash, $stream, $meta);
  321. DLNARenderer_play($hash);
  322. readingsSingleUpdate($hash, "stream", $stream, 1);
  323. }
  324. sub DLNARenderer_on {
  325. my ($hash) = @_;
  326. if (defined($hash->{READINGS}{stream})) {
  327. my $lastStream = $hash->{READINGS}{stream}{VAL};
  328. if ($lastStream) {
  329. DLNARenderer_upnpSetAVTransportURI($hash, $lastStream);
  330. DLNARenderer_play($hash);
  331. }
  332. }
  333. }
  334. sub DLNARenderer_convertVolumeToAbsolute {
  335. my ($hash, $targetVolume) = @_;
  336. if(substr($targetVolume, 0, 1) eq "+" or
  337. substr($targetVolume, 0, 1) eq "-") {
  338. $targetVolume = ReadingsVal($hash->{NAME}, "volume", 0) + $targetVolume;
  339. }
  340. return $targetVolume;
  341. }
  342. sub DLNARenderer_volume {
  343. my ($hash, $targetVolume) = @_;
  344. $targetVolume = DLNARenderer_convertVolumeToAbsolute($hash, $targetVolume);
  345. DLNARenderer_upnpSetVolume($hash, $targetVolume);
  346. }
  347. sub DLNARenderer_mute {
  348. my ($hash, $muteState) = @_;
  349. if($muteState eq "on") {
  350. $muteState = 1;
  351. } else {
  352. $muteState = 0;
  353. }
  354. DLNARenderer_upnpSetMute($hash, $muteState);
  355. }
  356. sub DLNARenderer_removeUnit {
  357. my ($hash, $unitToRemove) = @_;
  358. DLNARenderer_removeUnitToPlay($hash, $unitToRemove);
  359. my $multiRoomUnitsReading = "";
  360. my @multiRoomUnits = split(",", ReadingsVal($hash->{NAME}, "multiRoomUnits", ""));
  361. foreach my $unit (@multiRoomUnits) {
  362. $multiRoomUnitsReading .= ",".$unit if($unit ne $unitToRemove);
  363. }
  364. $multiRoomUnitsReading = substr($multiRoomUnitsReading, 1) if($multiRoomUnitsReading ne "");
  365. readingsSingleUpdate($hash, "multiRoomUnits", $multiRoomUnitsReading, 1);
  366. return undef;
  367. }
  368. sub DLNARenderer_loadGroup {
  369. my ($hash, $groupName) = @_;
  370. my $groupMembers = DLNARenderer_getGroupDefinition($hash, $groupName);
  371. return "DLNARenderer: Group $groupName not defined." if(!defined($groupMembers));
  372. DLNARenderer_destroyCurrentSession($hash);
  373. my $leftSpeaker = "";
  374. my $rightSpeaker = "";
  375. my @groupMembersArray = split(",", $groupMembers);
  376. foreach my $member (@groupMembersArray) {
  377. if($member =~ /^R:([a-zA-Z0-9äöüßÄÜÖ_]+)/) {
  378. $rightSpeaker = $1;
  379. } elsif($member =~ /^L:([a-zA-Z0-9äöüßÄÜÖ_]+)/) {
  380. $leftSpeaker = $1;
  381. } else {
  382. DLNARenderer_addUnit($hash, $member);
  383. }
  384. }
  385. if($leftSpeaker ne "" && $rightSpeaker ne "") {
  386. DLNARenderer_setStereoMode($hash, $leftSpeaker, $rightSpeaker, $groupName);
  387. }
  388. }
  389. sub DLNARenderer_stopPlayEverywhere {
  390. my ($hash) = @_;
  391. DLNARenderer_destroyCurrentSession($hash);
  392. readingsSingleUpdate($hash, "multiRoomUnits", "", 1);
  393. return undef;
  394. }
  395. sub DLNARenderer_playEverywhere {
  396. my ($hash) = @_;
  397. my $multiRoomUnits = "";
  398. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  399. foreach my $client (@caskeidClients) {
  400. if($client->{UDN} ne $hash->{UDN}) {
  401. DLNARenderer_addUnitToPlay($hash, substr($client->{UDN},5));
  402. my $multiRoomUnits = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  403. $multiRoomUnits .= "," if($multiRoomUnits ne "");
  404. $multiRoomUnits .= ReadingsVal($client->{NAME}, "friendlyName", "");
  405. readingsSingleUpdate($hash, "multiRoomUnits", $multiRoomUnits, 1);
  406. }
  407. }
  408. return undef;
  409. }
  410. sub DLNARenderer_setMultiRoomVolume {
  411. my ($hash, $targetVolume) = @_;
  412. #change volume of this device
  413. DLNARenderer_volume($hash, $targetVolume);
  414. #handle volume for all devices in the current group
  415. #iterate through group and change volume relative to the current volume of this device
  416. my $mainVolumeDiff = DLNARenderer_convertVolumeToAbsolute($hash, $targetVolume) - ReadingsVal($hash->{NAME}, "volume", 0);
  417. my $multiRoomUnits = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  418. my @multiRoomUnitsArray = split(",", $multiRoomUnits);
  419. foreach my $unit (@multiRoomUnitsArray) {
  420. my $devHash = DLNARenderer_getHashByFriendlyName($hash, $unit);
  421. my $newVolume = ReadingsVal($devHash->{NAME}, "volume", 0) + $mainVolumeDiff;
  422. if($newVolume > 100) {
  423. $newVolume = 100;
  424. } elsif($newVolume < 0) {
  425. $newVolume = 0;
  426. }
  427. DLNARenderer_volume($devHash, $newVolume);
  428. }
  429. return undef;
  430. }
  431. sub DLNARenderer_pauseToggle {
  432. my ($hash) = @_;
  433. if($hash->{READINGS}{state}{VAL} eq "paused") {
  434. DLNARenderer_play($hash);
  435. } else {
  436. DLNARenderer_upnpPause($hash);
  437. }
  438. }
  439. sub DLNARenderer_play {
  440. my ($hash) = @_;
  441. #start play
  442. if($hash->{helper}{caskeid}) {
  443. if($hash->{READINGS}{sessionId}{VAL} eq "") {
  444. DLNARenderer_createSession($hash);
  445. }
  446. DLNARenderer_upnpSyncPlay($hash);
  447. } else {
  448. DLNARenderer_upnpPlay($hash);
  449. }
  450. return undef;
  451. }
  452. ###########################
  453. ##### CASKEID #############
  454. ###########################
  455. # BTCaskeid
  456. sub DLNARenderer_enableBTCaskeid {
  457. my ($hash) = @_;
  458. DLNARenderer_upnpAddToGroup($hash, "4DAA44C0-8291-11E3-BAA7-0800200C9A66", "Bluetooth");
  459. }
  460. sub DLNARenderer_disableBTCaskeid {
  461. my ($hash) = @_;
  462. DLNARenderer_upnpRemoveFromGroup($hash, "4DAA44C0-8291-11E3-BAA7-0800200C9A66");
  463. }
  464. # Stereo Mode
  465. sub DLNARenderer_setStereoMode {
  466. my ($hash, $leftSpeaker, $rightSpeaker, $name) = @_;
  467. DLNARenderer_destroyCurrentSession($hash);
  468. my @multiRoomDevices = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  469. my $uuid = DLNARenderer_createUuid($hash);
  470. foreach my $device (@multiRoomDevices) {
  471. if(ReadingsVal($device->{NAME}, "friendlyName", "") eq $leftSpeaker) {
  472. DLNARenderer_setMultiChannelSpeaker($device, "left", $uuid, $leftSpeaker);
  473. readingsSingleUpdate($hash, "stereoLeft", $leftSpeaker, 1);
  474. } elsif(ReadingsVal($device->{NAME}, "friendlyName", "") eq $rightSpeaker) {
  475. DLNARenderer_setMultiChannelSpeaker($device, "right", $uuid, $rightSpeaker);
  476. readingsSingleUpdate($hash, "stereoRight", $rightSpeaker, 1);
  477. }
  478. }
  479. }
  480. sub DLNARenderer_updateStereoMode {
  481. my ($hash) = @_;
  482. if(!defined($hash->{helper}{device})) {
  483. InternalTimer(gettimeofday() + 10, 'DLNARenderer_updateStereoMode', $hash, 0);
  484. return undef;
  485. }
  486. if($hash->{helper}{caskeid} == 0) {
  487. return undef;
  488. }
  489. my $result = DLNARenderer_upnpGetMultiChannelSpeaker($hash);
  490. if($result) {
  491. InternalTimer(gettimeofday() + 300, 'DLNARenderer_updateStereoMode', $hash, 0);
  492. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoSupport", 1, 1);
  493. } else {
  494. #speaker does not support multi channel
  495. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoSupport", 0, 1);
  496. return undef;
  497. }
  498. my $mcsType = $result->getValue("CurrentMCSType");
  499. my $mcsId = $result->getValue("CurrentMCSID");
  500. my $mcsFriendlyName = $result->getValue("CurrentMCSFriendlyName");
  501. my $mcsSpeakerChannel = $result->getValue("CurrentSpeakerChannel");
  502. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoPairName", $mcsFriendlyName, 1);
  503. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoId", $mcsId, 1);
  504. if($mcsId eq "") {
  505. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoLeft", "", 1);
  506. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoRight", "", 1);
  507. } else {
  508. #THIS speaker is the left or right speaker
  509. DLNARenderer_setStereoSpeakerReading($hash, $hash, $mcsType, $mcsId, $mcsFriendlyName, $mcsSpeakerChannel);
  510. #set left/right speaker for OTHER speaker if OTHER speaker has same mcsId
  511. my @allHashes = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  512. foreach my $hash2 (@allHashes) {
  513. my $result2 = DLNARenderer_upnpGetMultiChannelSpeaker($hash2);
  514. next if(!defined($result2));
  515. my $mcsType2 = $result2->getValue("CurrentMCSType");
  516. my $mcsId2 = $result2->getValue("CurrentMCSID");
  517. my $mcsFriendlyName2 = $result2->getValue("CurrentMCSFriendlyName");
  518. my $mcsSpeakerChannel2 = $result2->getValue("CurrentSpeakerChannel");
  519. if($mcsId2 eq $mcsId) {
  520. DLNARenderer_setStereoSpeakerReading($hash, $hash2, $mcsType2, $mcsId2, $mcsFriendlyName2, $mcsSpeakerChannel2);
  521. }
  522. }
  523. }
  524. }
  525. sub DLNARenderer_setStereoSpeakerReading {
  526. my ($hash, $speakerHash, $mcsType, $mcsId, $mcsFriendlyName, $mcsSpeakerChannel) = @_;
  527. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoId", $mcsId, 1);
  528. if($mcsSpeakerChannel eq "LEFT_FRONT") {
  529. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoLeft", ReadingsVal($speakerHash->{NAME}, "friendlyName", ""), 1);
  530. } elsif($mcsSpeakerChannel eq "RIGHT_FRONT") {
  531. DLNARenderer_readingsSingleUpdateIfChanged($hash, "stereoRight", ReadingsVal($speakerHash->{NAME}, "friendlyName", ""), 1);
  532. }
  533. }
  534. sub DLNARenderer_readingsSingleUpdateIfChanged {
  535. my ($hash, $reading, $value, $trigger) = @_;
  536. my $curVal = ReadingsVal($hash->{NAME}, $reading, "");
  537. if($curVal ne $value) {
  538. readingsSingleUpdate($hash, $reading, $value, $trigger);
  539. }
  540. }
  541. sub DLNARenderer_setMultiChannelSpeaker {
  542. my ($hash, $mode, $uuid, $name) = @_;
  543. my $uuidStr;
  544. if($mode eq "standalone") {
  545. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STANDALONE", "", "", "STANDALONE_SPEAKER");
  546. } elsif($mode eq "left") {
  547. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STEREO", $uuid, $name, "LEFT_FRONT");
  548. } elsif($mode eq "right") {
  549. DLNARenderer_upnpSetMultiChannelSpeaker($hash, "STEREO", $uuid, $name, "RIGHT_FRONT");
  550. }
  551. return undef;
  552. }
  553. sub DLNARenderer_setStandaloneMode {
  554. my ($hash) = @_;
  555. my @multiRoomDevices = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  556. my $rightSpeaker = ReadingsVal($hash->{NAME}, "stereoRight", "");
  557. my $leftSpeaker = ReadingsVal($hash->{NAME}, "stereoLeft", "");
  558. foreach my $device (@multiRoomDevices) {
  559. if(ReadingsVal($device->{NAME}, "friendlyName", "") eq $leftSpeaker or
  560. ReadingsVal($device->{NAME}, "friendlyName", "") eq $rightSpeaker) {
  561. DLNARenderer_setMultiChannelSpeaker($device, "standalone", "", "");
  562. }
  563. }
  564. readingsSingleUpdate($hash, "stereoLeft", "", 1);
  565. readingsSingleUpdate($hash, "stereoRight", "", 1);
  566. readingsSingleUpdate($hash, "stereoId", "", 1);
  567. return undef;
  568. }
  569. sub DLNARenderer_createUuid {
  570. my ($hash) = @_;
  571. my $ug = Data::UUID->new();
  572. my $uuid = $ug->create();
  573. my $uuidStr = $ug->to_string($uuid);
  574. return $uuidStr;
  575. }
  576. # SessionManagement
  577. sub DLNARenderer_createSession {
  578. my ($hash) = @_;
  579. return DLNARenderer_upnpCreateSession($hash, "FHEM_Session");
  580. }
  581. sub DLNARenderer_getSession {
  582. my ($hash) = @_;
  583. return DLNARenderer_upnpGetSession($hash);
  584. }
  585. sub DLNARenderer_destroySession {
  586. my ($hash, $session) = @_;
  587. return DLNARenderer_upnpDestroySession($hash, $session);
  588. }
  589. sub DLNARenderer_destroyCurrentSession {
  590. my ($hash) = @_;
  591. my $result = DLNARenderer_getSession($hash);
  592. if($result->getValue("SessionID") ne "") {
  593. DLNARenderer_destroySession($hash, $result->getValue("SessionID"));
  594. }
  595. }
  596. sub DLNARenderer_addUnitToPlay {
  597. my ($hash, $unit) = @_;
  598. my $session = DLNARenderer_getSession($hash)->getValue("SessionID");
  599. if($session eq "") {
  600. $session = DLNARenderer_createSession($hash)->getValue("SessionID");
  601. }
  602. DLNARenderer_addUnitToSession($hash, $unit, $session);
  603. }
  604. sub DLNARenderer_removeUnitToPlay {
  605. my ($hash, $unit) = @_;
  606. my $session = DLNARenderer_getSession($hash)->getValue("SessionID");
  607. if($session ne "") {
  608. DLNARenderer_removeUnitFromSession($hash, $unit, $session);
  609. }
  610. }
  611. sub DLNARenderer_addUnitToSession {
  612. my ($hash, $uuid, $session) = @_;
  613. return DLNARenderer_upnpAddUnitToSession($hash, $session, $uuid);
  614. }
  615. sub DLNARenderer_removeUnitFromSession {
  616. my ($hash, $uuid, $session) = @_;
  617. return DLNARenderer_upnpRemoveUnitFromSession($hash, $session, $uuid);
  618. }
  619. # Group Definitions
  620. sub DLNARenderer_getGroupDefinition {
  621. #used for ... play Bad ...
  622. my ($hash, $groupName) = @_;
  623. my $currentGroupSettings = AttrVal($hash->{NAME}, "multiRoomGroups", "");
  624. #regex Bad[MUNET1,MUNET2],WZ[L:MUNET2,R:MUNET3],...
  625. while ($currentGroupSettings =~ /([a-zA-Z0-9äöüßÄÜÖ_]+)\[([a-zA-Z,0-9:äöüßÄÜÖ_]+)/g) {
  626. my $group = $1;
  627. my $groupMembers = $2;
  628. Log3 $hash, 4, "DLNARenderer: Groupdefinition $group => $groupMembers";
  629. if($group eq $groupName) {
  630. return $groupMembers;
  631. }
  632. }
  633. return undef;
  634. }
  635. sub DLNARenderer_saveGroupAs {
  636. my ($hash, $groupName) = @_;
  637. my $currentGroupSettings = AttrVal($hash->{NAME}, "multiRoomGroups", "");
  638. $currentGroupSettings .= "," if($currentGroupSettings ne "");
  639. #session details
  640. my $currentSession = ReadingsVal($hash->{NAME}, "multiRoomUnits", "");
  641. #stereo mode
  642. my $stereoLeft = ReadingsVal($hash->{NAME}, "stereoLeft", "");
  643. my $stereoRight = ReadingsVal($hash->{NAME}, "stereoRight", "");
  644. my $stereoDevices = "L:$stereoLeft,R:$stereoRight" if($stereoLeft ne "" && $stereoRight ne "");
  645. return undef if($currentSession eq "" && $stereoLeft eq "" && $stereoRight eq "");
  646. $stereoDevices .= "," if($currentSession ne "" && $stereoDevices ne "");
  647. my $groupDefinition = $currentGroupSettings.$groupName."[".$stereoDevices.$currentSession."]";
  648. #save current session as group
  649. CommandAttr(undef, "$hash->{NAME} multiRoomGroups $groupDefinition");
  650. return undef;
  651. }
  652. sub DLNARenderer_addUnit {
  653. my ($hash, $unitName) = @_;
  654. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  655. foreach my $client (@caskeidClients) {
  656. if(ReadingsVal($client->{NAME}, "friendlyName", "") eq $unitName) {
  657. my @multiRoomUnits = split(",", ReadingsVal($hash->{NAME}, "multiRoomUnits", ""));
  658. foreach my $unit (@multiRoomUnits) {
  659. #skip if unit is already part of the session
  660. return undef if($unit eq $unitName);
  661. }
  662. #add unit to session
  663. DLNARenderer_addUnitToPlay($hash, substr($client->{UDN},5));
  664. return undef;
  665. }
  666. }
  667. return "DLNARenderer: No unit $unitName found.";
  668. }
  669. ##############################
  670. ####### UPNP FUNCTIONS #######
  671. ##############################
  672. sub DLNARenderer_upnpPause {
  673. my ($hash) = @_;
  674. return DLNARenderer_upnpCallAVTransport($hash, "Pause", 0);
  675. }
  676. sub DLNARenderer_upnpSetAVTransportURI {
  677. my ($hash, $stream, $meta) = @_;
  678. if (!defined($meta)) { $meta = ""; }
  679. return DLNARenderer_upnpCallAVTransport($hash, "SetAVTransportURI", 0, $stream, $meta);
  680. }
  681. sub DLNARenderer_upnpStop {
  682. my ($hash) = @_;
  683. return DLNARenderer_upnpCallAVTransport($hash, "Stop", 0);
  684. }
  685. sub DLNARenderer_upnpSeek {
  686. my ($hash, $seekTime) = @_;
  687. return DLNARenderer_upnpCallAVTransport($hash, "Seek", 0, "REL_TIME", $seekTime);
  688. }
  689. sub DLNARenderer_upnpNext {
  690. my ($hash) = @_;
  691. return DLNARenderer_upnpCallAVTransport($hash, "Next", 0);
  692. }
  693. sub DLNARenderer_upnpPrevious {
  694. my ($hash) = @_;
  695. return DLNARenderer_upnpCallAVTransport($hash, "Previous", 0);
  696. }
  697. sub DLNARenderer_upnpPlay {
  698. my ($hash) = @_;
  699. return DLNARenderer_upnpCallAVTransport($hash, "Play", 0, 1);
  700. }
  701. sub DLNARenderer_upnpSyncPlay {
  702. my ($hash) = @_;
  703. return DLNARenderer_upnpCallAVTransport($hash, "SyncPlay", 0, 1, "REL_TIME", "", "", "", "PUREDEVICECLOCK1");
  704. }
  705. sub DLNARenderer_upnpCallAVTransport {
  706. my ($hash, $method, @args) = @_;
  707. return DLNARenderer_upnpCall($hash, 'AVTransport', $method, @args);
  708. }
  709. sub DLNARenderer_upnpGetMultiChannelSpeaker {
  710. my ($hash) = @_;
  711. return DLNARenderer_upnpCallSpeakerManagement($hash, "GetMultiChannelSpeaker");
  712. }
  713. sub DLNARenderer_upnpSetMultiChannelSpeaker {
  714. my ($hash, @args) = @_;
  715. return DLNARenderer_upnpCallSpeakerManagement($hash, "SetMultiChannelSpeaker", @args);
  716. }
  717. sub DLNARenderer_upnpCallSpeakerManagement {
  718. my ($hash, $method, @args) = @_;
  719. return DLNARenderer_upnpCall($hash, 'SpeakerManagement', $method, @args);
  720. }
  721. sub DLNARenderer_upnpAddUnitToSession {
  722. my ($hash, $session, $uuid) = @_;
  723. return DLNARenderer_upnpCallSessionManagement($hash, "AddUnitToSession", $session, $uuid);
  724. }
  725. sub DLNARenderer_upnpRemoveUnitFromSession {
  726. my ($hash, $session, $uuid) = @_;
  727. return DLNARenderer_upnpCallSessionManagement($hash, "RemoveUnitFromSession", $session, $uuid);
  728. }
  729. sub DLNARenderer_upnpDestroySession {
  730. my ($hash, $session) = @_;
  731. return DLNARenderer_upnpCallSessionManagement($hash, "DestroySession", $session);
  732. }
  733. sub DLNARenderer_upnpCreateSession {
  734. my ($hash, $name) = @_;
  735. return DLNARenderer_upnpCallSessionManagement($hash, "CreateSession", $name);
  736. }
  737. sub DLNARenderer_upnpGetSession {
  738. my ($hash) = @_;
  739. return DLNARenderer_upnpCallSessionManagement($hash, "GetSession");
  740. }
  741. sub DLNARenderer_upnpAddToGroup {
  742. my ($hash, $unit, $name) = @_;
  743. return DLNARenderer_upnpCallSpeakerManagement($hash, "AddToGroup", $unit, $name, "");
  744. }
  745. sub DLNARenderer_upnpRemoveFromGroup {
  746. my ($hash, $unit) = @_;
  747. return DLNARenderer_upnpCallSpeakerManagement($hash, "RemoveFromGroup", $unit);
  748. }
  749. sub DLNARenderer_upnpCallSessionManagement {
  750. my ($hash, $method, @args) = @_;
  751. return DLNARenderer_upnpCall($hash, 'SessionManagement', $method, @args);
  752. }
  753. sub DLNARenderer_upnpSetVolume {
  754. my ($hash, $targetVolume) = @_;
  755. return DLNARenderer_upnpCallRenderingControl($hash, "SetVolume", 0, "Master", $targetVolume);
  756. }
  757. sub DLNARenderer_upnpSetMute {
  758. my ($hash, $muteState) = @_;
  759. return DLNARenderer_upnpCallRenderingControl($hash, "SetMute", 0, "Master", $muteState);
  760. }
  761. sub DLNARenderer_upnpCallRenderingControl {
  762. my ($hash, $method, @args) = @_;
  763. return DLNARenderer_upnpCall($hash, 'RenderingControl', $method, @args);
  764. }
  765. sub DLNARenderer_upnpCall {
  766. my ($hash, $service, $method, @args) = @_;
  767. my $upnpService = DLNARenderer_upnpGetService($hash, $service);
  768. my $ret = undef;
  769. eval {
  770. my $upnpServiceCtrlProxy = $upnpService->controlProxy();
  771. my $methodExists = $upnpService->getAction($method);
  772. if($methodExists) {
  773. $ret = $upnpServiceCtrlProxy->$method(@args);
  774. Log3 $hash, 5, "DLNARenderer: $service, $method(".join(",",@args).") succeed.";
  775. } else {
  776. Log3 $hash, 4, "DLNARenderer: $service, $method(".join(",",@args).") does not exist.";
  777. }
  778. };
  779. if($@) {
  780. Log3 $hash, 3, "DLNARenderer: $service, $method(".join(",",@args).") failed, $@";
  781. return undef;
  782. }
  783. return $ret;
  784. }
  785. sub DLNARenderer_upnpGetService {
  786. my ($hash, $service) = @_;
  787. my $upnpService;
  788. foreach my $srvc ($hash->{helper}{device}->services) {
  789. my @srvcParts = split(":", $srvc->serviceType);
  790. my $serviceName = $srvcParts[-2];
  791. if($serviceName eq $service) {
  792. Log3 $hash, 5, "DLNARenderer: $service: ".$srvc->serviceType." found. OK.";
  793. $upnpService = $srvc;
  794. }
  795. }
  796. if(!defined($upnpService)) {
  797. Log3 $hash, 4, "DLNARenderer: $service unknown for $hash->{NAME}.";
  798. return undef;
  799. }
  800. return $upnpService;
  801. }
  802. ##############################
  803. ####### EVENT HANDLING #######
  804. ##############################
  805. sub DLNARenderer_processEventXml {
  806. my ($hash, $property, $xml) = @_;
  807. Log3 $hash, 4, "DLNARenderer: ".Dumper($xml);
  808. if($property eq "LastChange") {
  809. return undef if($xml eq "");
  810. if($xml->{Event}) {
  811. if (index($xml->{Event}{xmlns},"urn:schemas-upnp-org:metadata-1-0/AVT")==0) {
  812. #process AV Transport
  813. my $e = $xml->{Event}{InstanceID};
  814. #DLNARenderer_updateReadingByEvent($hash, "NumberOfTracks", $e->{NumberOfTracks});
  815. DLNARenderer_updateReadingByEvent($hash, "transportState", $e->{TransportState});
  816. DLNARenderer_updateReadingByEvent($hash, "transportStatus", $e->{TransportStatus});
  817. #DLNARenderer_updateReadingByEvent($hash, "TransportPlaySpeed", $e->{TransportPlaySpeed});
  818. #DLNARenderer_updateReadingByEvent($hash, "PlaybackStorageMedium", $e->{PlaybackStorageMedium});
  819. #DLNARenderer_updateReadingByEvent($hash, "RecordStorageMedium", $e->{RecordStorageMedium});
  820. #DLNARenderer_updateReadingByEvent($hash, "RecordMediumWriteStatus", $e->{RecordMediumWriteStatus});
  821. #DLNARenderer_updateReadingByEvent($hash, "CurrentRecordQualityMode", $e->{CurrentRecordQualityMode});
  822. #DLNARenderer_updateReadingByEvent($hash, "PossibleRecordQualityMode", $e->{PossibleRecordQualityMode});
  823. DLNARenderer_updateReadingByEvent($hash, "currentTrackURI", $e->{CurrentTrackURI});
  824. #DLNARenderer_updateReadingByEvent($hash, "AVTransportURI", $e->{AVTransportURI});
  825. DLNARenderer_updateReadingByEvent($hash, "nextAVTransportURI", $e->{NextAVTransportURI});
  826. #DLNARenderer_updateReadingByEvent($hash, "RelativeTimePosition", $e->{RelativeTimePosition});
  827. #DLNARenderer_updateReadingByEvent($hash, "AbsoluteTimePosition", $e->{AbsoluteTimePosition});
  828. #DLNARenderer_updateReadingByEvent($hash, "RelativeCounterPosition", $e->{RelativeCounterPosition});
  829. #DLNARenderer_updateReadingByEvent($hash, "AbsoluteCounterPosition", $e->{AbsoluteCounterPosition});
  830. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrack", $e->{CurrentTrack});
  831. #DLNARenderer_updateReadingByEvent($hash, "CurrentMediaDuration", $e->{CurrentMediaDuration});
  832. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrackDuration", $e->{CurrentTrackDuration});
  833. #DLNARenderer_updateReadingByEvent($hash, "CurrentPlayMode", $e->{CurrentPlayMode});
  834. #handle metadata
  835. #DLNARenderer_updateReadingByEvent($hash, "AVTransportURIMetaData", $e->{AVTransportURIMetaData});
  836. #DLNARenderer_updateMetaData($hash, "current", $e->{AVTransportURIMetaData});
  837. #DLNARenderer_updateReadingByEvent($hash, "NextAVTransportURIMetaData", $e->{NextAVTransportURIMetaData});
  838. DLNARenderer_updateMetaData($hash, "next", $e->{NextAVTransportURIMetaData});
  839. #use only CurrentTrackMetaData instead of AVTransportURIMetaData
  840. #DLNARenderer_updateReadingByEvent($hash, "CurrentTrackMetaData", $e->{CurrentTrackMetaData});
  841. DLNARenderer_updateMetaData($hash, "current", $e->{CurrentTrackMetaData});
  842. #update state
  843. my $transportState = ReadingsVal($hash->{NAME}, "transportState", "");
  844. if(ReadingsVal($hash->{NAME}, "presence", "") ne "offline") {
  845. if($transportState eq "PAUSED_PLAYBACK") {
  846. readingsSingleUpdate($hash, "state", "paused", 1);
  847. } elsif($transportState eq "PLAYING") {
  848. readingsSingleUpdate($hash, "state", "playing", 1);
  849. } elsif($transportState eq "TRANSITIONING") {
  850. readingsSingleUpdate($hash, "state", "buffering", 1);
  851. } elsif($transportState eq "STOPPED") {
  852. readingsSingleUpdate($hash, "state", "stopped", 1);
  853. } elsif($transportState eq "NO_MEDIA_PRESENT") {
  854. readingsSingleUpdate($hash, "state", "online", 1);
  855. }
  856. }
  857. } elsif (index($xml->{Event}{xmlns},"urn:schemas-upnp-org:metadata-1-0/RCS")==0) {
  858. #process RenderingControl
  859. my $e = $xml->{Event}{InstanceID};
  860. DLNARenderer_updateVolumeByEvent($hash, "mute", $e->{Mute});
  861. DLNARenderer_updateVolumeByEvent($hash, "volume", $e->{Volume});
  862. readingsSingleUpdate($hash, "multiRoomVolume", ReadingsVal($hash->{NAME}, "volume", 0), 1);
  863. } elsif ($xml->{Event}{xmlns} eq "FIXME SpeakerManagement") {
  864. #process SpeakerManagement
  865. }
  866. }
  867. } elsif($property eq "Groups") {
  868. #handle BTCaskeid
  869. my $btCaskeidState = 0;
  870. foreach my $group (@{$xml->{groups}{group}}) {
  871. #"4DAA44C0-8291-11E3-BAA7-0800200C9A66", "Bluetooth"
  872. if($group->{id} eq "4DAA44C0-8291-11E3-BAA7-0800200C9A66") {
  873. $btCaskeidState = 1;
  874. }
  875. }
  876. #TODO update only if changed
  877. readingsSingleUpdate($hash, "btCaskeid", $btCaskeidState, 1);
  878. } elsif($property eq "SessionID") {
  879. #TODO search for other speakers with same sessionId and add them to multiRoomUnits
  880. readingsSingleUpdate($hash, "sessionId", $xml, 1);
  881. }
  882. return undef;
  883. }
  884. sub DLNARenderer_updateReadingByEvent {
  885. my ($hash, $readingName, $xmlEvent) = @_;
  886. my $currVal = ReadingsVal($hash->{NAME}, $readingName, "");
  887. if($xmlEvent) {
  888. Log3 $hash, 4, "DLNARenderer: Update reading $readingName with ".$xmlEvent->{val};
  889. my $val = $xmlEvent->{val};
  890. $val = "" if(ref $val eq ref {});
  891. if($val ne $currVal) {
  892. readingsSingleUpdate($hash, $readingName, $val, 1);
  893. }
  894. }
  895. return undef;
  896. }
  897. sub DLNARenderer_updateVolumeByEvent {
  898. my ($hash, $readingName, $volume) = @_;
  899. my $balance = 0;
  900. my $balanceSupport = 0;
  901. foreach my $vol (@{$volume}) {
  902. my $channel = $vol->{Channel} ? $vol->{Channel} : $vol->{channel};
  903. if($channel) {
  904. if($channel eq "Master") {
  905. DLNARenderer_updateReadingByEvent($hash, $readingName, $vol);
  906. } elsif($channel eq "LF") {
  907. $balance -= $vol->{val};
  908. $balanceSupport = 1;
  909. } elsif($channel eq "RF") {
  910. $balance += $vol->{val};
  911. $balanceSupport = 1;
  912. }
  913. } else {
  914. DLNARenderer_updateReadingByEvent($hash, $readingName, $vol);
  915. }
  916. }
  917. if($readingName eq "volume" && $balanceSupport == 1) {
  918. readingsSingleUpdate($hash, "balance", $balance, 1);
  919. }
  920. return undef;
  921. }
  922. sub DLNARenderer_updateMetaData {
  923. my ($hash, $prefix, $metaData) = @_;
  924. my $metaDataAvailable = 0;
  925. $metaDataAvailable = 1 if(defined($metaData) && $metaData->{val} && $metaData->{val} ne "");
  926. if($metaDataAvailable) {
  927. my $xml;
  928. if($metaData->{val} eq "NOT_IMPLEMENTED") {
  929. readingsSingleUpdate($hash, $prefix."Title", "", 1);
  930. readingsSingleUpdate($hash, $prefix."Artist", "", 1);
  931. readingsSingleUpdate($hash, $prefix."Album", "", 1);
  932. readingsSingleUpdate($hash, $prefix."AlbumArtist", "", 1);
  933. readingsSingleUpdate($hash, $prefix."AlbumArtURI", "", 1);
  934. readingsSingleUpdate($hash, $prefix."OriginalTrackNumber", "", 1);
  935. readingsSingleUpdate($hash, $prefix."Duration", "", 1);
  936. } else {
  937. eval {
  938. $xml = XMLin($metaData->{val}, KeepRoot => 1, ForceArray => [], KeyAttr => []);
  939. Log3 $hash, 4, "DLNARenderer: MetaData: ".Dumper($xml);
  940. };
  941. if(!$@) {
  942. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Title", $xml->{"DIDL-Lite"}{item}{"dc:title"});
  943. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Artist", $xml->{"DIDL-Lite"}{item}{"dc:creator"});
  944. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Album", $xml->{"DIDL-Lite"}{item}{"upnp:album"});
  945. DLNARenderer_updateMetaDataItemPart($hash, $prefix."AlbumArtist", $xml->{"DIDL-Lite"}{item}{"r:albumArtist"});
  946. if($xml->{"DIDL-Lite"}{item}{"upnp:albumArtURI"}) {
  947. DLNARenderer_updateMetaDataItemPart($hash, $prefix."AlbumArtURI", $xml->{"DIDL-Lite"}{item}{"upnp:albumArtURI"});
  948. } else {
  949. readingsSingleUpdate($hash, $prefix."AlbumArtURI", "", 1);
  950. }
  951. DLNARenderer_updateMetaDataItemPart($hash, $prefix."OriginalTrackNumber", $xml->{"DIDL-Lite"}{item}{"upnp:originalTrackNumber"});
  952. if($xml->{"DIDL-Lite"}{item}{res}) {
  953. DLNARenderer_updateMetaDataItemPart($hash, $prefix."Duration", $xml->{"DIDL-Lite"}{item}{res}{duration});
  954. } else {
  955. readingsSingleUpdate($hash, $prefix."Duration", "", 1);
  956. }
  957. } else {
  958. Log3 $hash, 1, "DLNARenderer: XML parsing error: ".$@;
  959. }
  960. }
  961. }
  962. return undef;
  963. }
  964. sub DLNARenderer_updateMetaDataItemPart {
  965. my ($hash, $readingName, $item) = @_;
  966. my $currVal = ReadingsVal($hash->{NAME}, $readingName, "");
  967. if($item) {
  968. $item = "" if(ref $item eq ref {});
  969. if($currVal ne $item) {
  970. readingsSingleUpdate($hash, $readingName, $item, 1);
  971. }
  972. }
  973. return undef;
  974. }
  975. ##############################
  976. ####### DISCOVERY ############
  977. ##############################
  978. sub DLNARenderer_setupControlpoint {
  979. my ($hash) = @_;
  980. my %empty = ();
  981. my $error;
  982. my $cp;
  983. do {
  984. eval {
  985. $cp = UPnP::ControlPoint->new(SearchPort => 0, SubscriptionPort => 0, MaxWait => 30, UsedOnlyIP => \%empty, IgnoreIP => \%empty);
  986. $hash->{helper}{controlpoint} = $cp;
  987. DLNARenderer_addSocketsToMainloop($hash);
  988. };
  989. $error = $@;
  990. } while($error);
  991. return undef;
  992. }
  993. sub DLNARenderer_startDlnaRendererSearch {
  994. my ($hash) = @_;
  995. eval {
  996. $hash->{helper}{controlpoint}->searchByType('urn:schemas-upnp-org:device:MediaRenderer:1', sub { DLNARenderer_discoverCallback($hash, @_); });
  997. };
  998. if($@) {
  999. Log3 $hash, 2, "DLNARenderer: Search failed with error $@";
  1000. }
  1001. return undef;
  1002. }
  1003. sub DLNARenderer_discoverCallback {
  1004. my ($hash, $search, $device, $action) = @_;
  1005. Log3 $hash, 4, "DLNARenderer: $action, ".$device->friendlyName();
  1006. if($action eq "deviceAdded") {
  1007. DLNARenderer_addedDevice($hash, $device);
  1008. } elsif($action eq "deviceRemoved") {
  1009. DLNARenderer_removedDevice($hash, $device);
  1010. }
  1011. return undef;
  1012. }
  1013. sub DLNARenderer_subscriptionCallback {
  1014. my ($hash, $service, %properties) = @_;
  1015. Log3 $hash, 4, "DLNARenderer: Received event: ".Dumper(%properties);
  1016. foreach my $property (keys %properties) {
  1017. $properties{$property} = decode_entities($properties{$property});
  1018. my $xml;
  1019. eval {
  1020. if($properties{$property} =~ /xml/) {
  1021. $xml = XMLin($properties{$property}, KeepRoot => 1, ForceArray => [qw(Volume Mute Loudness VolumeDB group)], KeyAttr => []);
  1022. } else {
  1023. $xml = $properties{$property};
  1024. }
  1025. };
  1026. if($@) {
  1027. Log3 $hash, 2, "DLNARenderer: XML formatting error: ".$@.", ".$properties{$property};
  1028. next;
  1029. }
  1030. DLNARenderer_processEventXml($hash, $property, $xml);
  1031. }
  1032. return undef;
  1033. }
  1034. sub DLNARenderer_renewSubscriptions {
  1035. my ($hash) = @_;
  1036. my $dev = $hash->{helper}{device};
  1037. InternalTimer(gettimeofday() + 200, 'DLNARenderer_renewSubscriptions', $hash, 0);
  1038. return undef if(!defined($dev));
  1039. BlockingCall('DLNARenderer_renewSubscriptionBlocking', $hash->{NAME});
  1040. return undef;
  1041. }
  1042. sub DLNARenderer_renewSubscriptionBlocking {
  1043. my ($string) = @_;
  1044. my ($name) = split("\\|", $string);
  1045. my $hash = $main::defs{$name};
  1046. #register callbacks
  1047. #urn:upnp-org:serviceId:AVTransport
  1048. eval {
  1049. if(defined($hash->{helper}{avTransportSubscription})) {
  1050. $hash->{helper}{avTransportSubscription}->renew();
  1051. }
  1052. };
  1053. #urn:upnp-org:serviceId:RenderingControl
  1054. eval {
  1055. if(defined($hash->{helper}{renderingControlSubscription})) {
  1056. $hash->{helper}{renderingControlSubscription}->renew();
  1057. }
  1058. };
  1059. #urn:pure-com:serviceId:SpeakerManagement
  1060. eval {
  1061. if(defined($hash->{helper}{speakerManagementSubscription})) {
  1062. $hash->{helper}{speakerManagementSubscription}->renew();
  1063. }
  1064. };
  1065. }
  1066. sub DLNARenderer_addedDevice {
  1067. my ($hash, $dev) = @_;
  1068. my $udn = $dev->UDN();
  1069. #TODO check for BOSE UDN
  1070. #ignoreUDNs
  1071. return undef if(AttrVal($hash->{NAME}, "ignoreUDNs", "") =~ /$udn/);
  1072. my $foundDevice = 0;
  1073. my @allDLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1074. foreach my $DLNARendererHash (@allDLNARenderers) {
  1075. if($DLNARendererHash->{UDN} eq $dev->UDN()) {
  1076. $foundDevice = 1;
  1077. }
  1078. }
  1079. if(!$foundDevice) {
  1080. my $uniqueDeviceName = "DLNA_".substr($dev->UDN(),29,12);
  1081. if(length($uniqueDeviceName) < 17) {
  1082. $uniqueDeviceName = "DLNA_".substr($dev->UDN(),5);
  1083. $uniqueDeviceName =~ tr/-/_/;
  1084. }
  1085. CommandDefine(undef, "$uniqueDeviceName DLNARenderer ".$dev->UDN());
  1086. CommandAttr(undef,"$uniqueDeviceName alias ".$dev->friendlyName());
  1087. CommandAttr(undef,"$uniqueDeviceName webCmd volume");
  1088. if(AttrVal($hash->{NAME}, "defaultRoom", "") ne "") {
  1089. CommandAttr(undef,"$uniqueDeviceName room ".AttrVal($hash->{NAME}, "defaultRoom", ""));
  1090. }
  1091. Log3 $hash, 3, "DLNARenderer: Created device $uniqueDeviceName for ".$dev->friendlyName();
  1092. #update list
  1093. @allDLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1094. }
  1095. foreach my $DLNARendererHash (@allDLNARenderers) {
  1096. if($DLNARendererHash->{UDN} eq $dev->UDN()) {
  1097. #device found, update data
  1098. $DLNARendererHash->{helper}{device} = $dev;
  1099. #update device information (FIXME only on change)
  1100. readingsSingleUpdate($DLNARendererHash, "friendlyName", $dev->friendlyName(), 1);
  1101. readingsSingleUpdate($DLNARendererHash, "manufacturer", $dev->manufacturer(), 1);
  1102. readingsSingleUpdate($DLNARendererHash, "modelDescription", $dev->modelDescription(), 1);
  1103. readingsSingleUpdate($DLNARendererHash, "modelName", $dev->modelName(), 1);
  1104. readingsSingleUpdate($DLNARendererHash, "modelNumber", $dev->modelNumber(), 1);
  1105. readingsSingleUpdate($DLNARendererHash, "modelURL", $dev->modelURL(), 1);
  1106. readingsSingleUpdate($DLNARendererHash, "manufacturerURL", $dev->manufacturerURL(), 1);
  1107. readingsSingleUpdate($DLNARendererHash, "presentationURL", $dev->presentationURL(), 1);
  1108. readingsSingleUpdate($DLNARendererHash, "manufacturer", $dev->manufacturer(), 1);
  1109. #register callbacks
  1110. #urn:upnp-org:serviceId:AVTransport
  1111. if(DLNARenderer_upnpGetService($DLNARendererHash, "AVTransport")) {
  1112. $DLNARendererHash->{helper}{avTransportSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "AVTransport")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); });
  1113. }
  1114. #urn:upnp-org:serviceId:RenderingControl
  1115. if(DLNARenderer_upnpGetService($DLNARendererHash, "RenderingControl")) {
  1116. $DLNARendererHash->{helper}{renderingControlSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "RenderingControl")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); });
  1117. }
  1118. #urn:pure-com:serviceId:SpeakerManagement
  1119. if(DLNARenderer_upnpGetService($DLNARendererHash, "SpeakerManagement")) {
  1120. $DLNARendererHash->{helper}{speakerManagementSubscription} = DLNARenderer_upnpGetService($DLNARendererHash, "SpeakerManagement")->subscribe(sub { DLNARenderer_subscriptionCallback($DLNARendererHash, @_); });
  1121. }
  1122. #set online
  1123. readingsSingleUpdate($DLNARendererHash,"presence","online",1);
  1124. if(ReadingsVal($DLNARendererHash->{NAME}, "state", "") eq "offline") {
  1125. readingsSingleUpdate($DLNARendererHash,"state","online",1);
  1126. }
  1127. #check caskeid
  1128. if(DLNARenderer_upnpGetService($DLNARendererHash, "SessionManagement")) {
  1129. $DLNARendererHash->{helper}{caskeid} = 1;
  1130. readingsSingleUpdate($DLNARendererHash,"multiRoomSupport","1",1);
  1131. } else {
  1132. readingsSingleUpdate($DLNARendererHash,"multiRoomSupport","0",1);
  1133. }
  1134. #update list of caskeid clients
  1135. my @caskeidClients = DLNARenderer_getAllDLNARenderersWithCaskeid($hash);
  1136. $DLNARendererHash->{helper}{caskeidClients} = "";
  1137. foreach my $client (@caskeidClients) {
  1138. #do not add myself
  1139. if($client->{UDN} ne $DLNARendererHash->{UDN}) {
  1140. $DLNARendererHash->{helper}{caskeidClients} .= ",".ReadingsVal($client->{NAME}, "friendlyName", "");
  1141. }
  1142. }
  1143. $DLNARendererHash->{helper}{caskeidClients} = substr($DLNARendererHash->{helper}{caskeidClients}, 1) if($DLNARendererHash->{helper}{caskeidClients} ne "");
  1144. }
  1145. }
  1146. return undef;
  1147. }
  1148. sub DLNARenderer_removedDevice($$) {
  1149. my ($hash, $device) = @_;
  1150. my $deviceHash = DLNARenderer_getHashByUDN($hash, $device->UDN());
  1151. return undef if(!defined($deviceHash));
  1152. readingsSingleUpdate($deviceHash, "presence", "offline", 1);
  1153. readingsSingleUpdate($deviceHash, "state", "offline", 1);
  1154. }
  1155. ###############################
  1156. ##### GET PLAYER FUNCTIONS ####
  1157. ###############################
  1158. sub DLNARenderer_getMainDLNARenderer($) {
  1159. my ($hash) = @_;
  1160. foreach my $fhem_dev (sort keys %main::defs) {
  1161. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} eq "0");
  1162. }
  1163. return undef;
  1164. }
  1165. sub DLNARenderer_getHashByUDN($$) {
  1166. my ($hash, $udn) = @_;
  1167. foreach my $fhem_dev (sort keys %main::defs) {
  1168. return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} eq $udn);
  1169. }
  1170. return undef;
  1171. }
  1172. sub DLNARenderer_getHashByFriendlyName {
  1173. my ($hash, $friendlyName) = @_;
  1174. foreach my $fhem_dev (sort keys %main::defs) {
  1175. my $devHash = $main::defs{$fhem_dev};
  1176. return $devHash if($devHash->{TYPE} eq 'DLNARenderer' && ReadingsVal($devHash->{NAME}, "friendlyName", "") eq $friendlyName);
  1177. }
  1178. return undef;
  1179. }
  1180. sub DLNARenderer_getAllDLNARenderers($) {
  1181. my ($hash) = @_;
  1182. my @DLNARenderers = ();
  1183. foreach my $fhem_dev (sort keys %main::defs) {
  1184. push @DLNARenderers, $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'DLNARenderer' && $main::defs{$fhem_dev}{UDN} ne "0" && $main::defs{$fhem_dev}{UDN} ne "-1");
  1185. }
  1186. return @DLNARenderers;
  1187. }
  1188. sub DLNARenderer_getAllDLNARenderersWithCaskeid($) {
  1189. my ($hash) = @_;
  1190. my @caskeidClients = ();
  1191. my @DLNARenderers = DLNARenderer_getAllDLNARenderers($hash);
  1192. foreach my $DLNARenderer (@DLNARenderers) {
  1193. push @caskeidClients, $DLNARenderer if($DLNARenderer->{helper}{caskeid});
  1194. }
  1195. return @caskeidClients;
  1196. }
  1197. ###############################
  1198. ###### UTILITY FUNCTIONS ######
  1199. ###############################
  1200. sub DLNARenderer_generateDidlLiteAndPlay {
  1201. my ($hash, $stream) = @_;
  1202. BlockingCall('DLNARenderer_generateDidlLiteBlocking', $hash->{NAME}."|".$stream, 'DLNARenderer_generateDidlLiteBlockingFinished');
  1203. return undef;
  1204. }
  1205. sub DLNARenderer_generateDidlLiteBlockingFinished {
  1206. my ($string) = @_;
  1207. return unless (defined($string));
  1208. my ($name, $stream, $meta) = split("\\|",$string);
  1209. my $hash = $defs{$name};
  1210. DLNARenderer_upnpSetAVTransportURI($hash, $stream, $meta);
  1211. DLNARenderer_play($hash);
  1212. readingsSingleUpdate($hash, "stream", $stream, 1);
  1213. }
  1214. sub DLNARenderer_generateDidlLiteBlocking {
  1215. my ($string) = @_;
  1216. my ($name, $stream) = split("\\|", $string);
  1217. my $hash = $main::defs{$name};
  1218. my $ret = $name."|".$stream;
  1219. if(index($stream, "http:") != 0) {
  1220. return $ret;
  1221. }
  1222. my $ua = new LWP::UserAgent(agent => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1');
  1223. $ua->max_size(0);
  1224. my $resp = $ua->get($stream);
  1225. my $didl_header = '<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:sec="http://www.sec.co.kr/"><item id="-1" parentID="parent" restricted="1">';
  1226. my $didl_footer = '</item></DIDL-Lite>';
  1227. $stream = encode_entities($stream);
  1228. my $size = "";
  1229. my $protocolInfo = "";
  1230. my $album = $stream;
  1231. my $title = $stream;
  1232. my $meta = "";
  1233. if (defined($resp->header('content-length'))) {
  1234. $size = ' size="'.$resp->header('content-length').'"';
  1235. }
  1236. my @header = split /;/, $resp->header('content-type');
  1237. my $contenttype = $header[0];
  1238. if (defined($resp->header('contentfeatures.dlna.org'))) {
  1239. $protocolInfo = "http-get:*:".$contenttype.":".$resp->header('contentfeatures.dlna.org');
  1240. } else {
  1241. $protocolInfo = "http-get:*:".$contenttype.":DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000";
  1242. }
  1243. if (defined($resp->header('icy-name'))) {
  1244. $album = encode_entities($resp->header('icy-name'));
  1245. }
  1246. if (defined($resp->header('icy-genre'))) {
  1247. $title = encode_entities($resp->header('icy-genre'));
  1248. }
  1249. if (substr($contenttype,0,5) eq "audio" or $contenttype eq "application/ogg") {
  1250. $meta = $didl_header.'<upnp:class>object.item.audioItem.musicTrack</upnp:class><dc:title>'.$title.'</dc:title><upnp:album>'.$album.'</upnp:album><res protocolInfo="'.$protocolInfo.'"'.$size.'>'.$stream.'</res>'.$didl_footer;
  1251. } elsif (substr($contenttype,0,5) eq "video") {
  1252. $meta = $didl_header.'<upnp:class>object.item.videoItem</upnp:class><dc:title>'.$title.'</dc:title><upnp:album>'.$album.'</upnp:album><res protocolInfo="'.$protocolInfo.'"'.$size.'>'.$stream.'</res>'.$didl_footer;
  1253. } else {
  1254. $meta = "";
  1255. }
  1256. $ret .= "|".$meta;
  1257. return $ret;
  1258. }
  1259. sub DLNARenderer_newChash($$$) {
  1260. my ($hash,$socket,$chash) = @_;
  1261. $chash->{TYPE} = $hash->{TYPE};
  1262. $chash->{UDN} = -1;
  1263. $chash->{NR} = $devcount++;
  1264. $chash->{phash} = $hash;
  1265. $chash->{PNAME} = $hash->{NAME};
  1266. $chash->{CD} = $socket;
  1267. $chash->{FD} = $socket->fileno();
  1268. $chash->{PORT} = $socket->sockport if( $socket->sockport );
  1269. $chash->{TEMPORARY} = 1;
  1270. $attr{$chash->{NAME}}{room} = 'hidden';
  1271. $defs{$chash->{NAME}} = $chash;
  1272. $selectlist{$chash->{NAME}} = $chash;
  1273. }
  1274. sub DLNARenderer_closeSocket($) {
  1275. my ($hash) = @_;
  1276. my $name = $hash->{NAME};
  1277. RemoveInternalTimer($hash);
  1278. close($hash->{CD});
  1279. delete($hash->{CD});
  1280. delete($selectlist{$name});
  1281. delete($hash->{FD});
  1282. }
  1283. sub DLNARenderer_addSocketsToMainloop {
  1284. my ($hash) = @_;
  1285. my @sockets = $hash->{helper}{controlpoint}->sockets();
  1286. #check if new sockets need to be added to mainloop
  1287. foreach my $s (@sockets) {
  1288. #create chash and add to selectlist
  1289. my $chash = DLNARenderer_newChash($hash, $s, {NAME => "DLNASocket-".$s->fileno()});
  1290. }
  1291. return undef;
  1292. }
  1293. 1;
  1294. =pod
  1295. =item device
  1296. =item summary Autodiscover and control your DLNA renderer devices easily
  1297. =item summary_DE Autodiscover und einfache Steuerung deiner DLNA Renderer Geräte
  1298. =begin html
  1299. <a name="DLNARenderer"></a>
  1300. <h3>DLNARenderer</h3>
  1301. <ul>
  1302. DLNARenderer automatically discovers all your MediaRenderer devices in your local network and allows you to fully control them.<br>
  1303. It also supports multiroom audio for Caskeid and Bluetooth Caskeid speakers (e.g. MUNET).<br><br>
  1304. <b>Note:</b> The followig libraries are required for this module:
  1305. <ul><li>SOAP::Lite</li> <li>LWP::Simple</li> <li>XML::Simple</li> <li>XML::Parser::Lite</li> <li>LWP::UserAgent</li><br>
  1306. </ul>
  1307. <a name="DLNARendererdefine"></a>
  1308. <b>Define</b>
  1309. <ul>
  1310. <code>define &lt;name&gt; DLNARenderer</code>
  1311. <br><br>
  1312. Example:
  1313. <ul>
  1314. <code>define dlnadevices DLNARenderer</code><br>
  1315. After about 2 minutes you can find all automatically created DLNA devices under "Unsorted".<br/>
  1316. </ul>
  1317. </ul>
  1318. <br>
  1319. <a name="DLNARendererset"></a>
  1320. <b>Set</b>
  1321. <ul>
  1322. <br><code>set &lt;name&gt; stream &lt;value&gt</code><br>
  1323. Set any URL to play.
  1324. </ul>
  1325. <ul>
  1326. <br><code>set &lt;name&gt; on</code><br>
  1327. Starts playing the last stream (reading stream).
  1328. </ul>
  1329. <ul>
  1330. <br><code>set &lt;name&gt; off</code><br>
  1331. Sends stop command to device.
  1332. </ul>
  1333. <ul>
  1334. <br><code>set &lt;name&gt; stop</code><br>
  1335. Stop playback.
  1336. </ul>
  1337. <ul>
  1338. <br><code>set &lt;name&gt; volume 0-100</code><br>
  1339. <code>set &lt;name&gt; volume +/-0-100</code><br>
  1340. Set volume of the device.
  1341. </ul>
  1342. <ul>
  1343. <br><code>set &lt;name&gt; channel 1-10</code><br>
  1344. Start playing channel X which must be configured as channel_X attribute first.<br>
  1345. You can specify your channel also in DIDL-Lite XML format if your player doesn't support plain URIs.
  1346. </ul>
  1347. <ul>
  1348. <br><code>set &lt;name&gt; mute on/off</code><br>
  1349. Mute the device.
  1350. </ul>
  1351. <ul>
  1352. <br><code>set &lt;name&gt; pause</code><br>
  1353. Pause playback of the device. No toggle.
  1354. </ul>
  1355. <ul>
  1356. <br><code>set &lt;name&gt; pauseToggle</code><br>
  1357. Toggle pause/play for the device.
  1358. </ul>
  1359. <ul>
  1360. <br><code>set &lt;name&gt; play</code><br>
  1361. Initiate play command. Only makes your player play if a stream was loaded (currentTrackURI is set).
  1362. </ul>
  1363. <ul>
  1364. <br><code>set &lt;name&gt; next</code><br>
  1365. Play next track.
  1366. </ul>
  1367. <ul>
  1368. <br><code>set &lt;name&gt; previous</code><br>
  1369. Play previous track.
  1370. </ul>
  1371. <ul>
  1372. <br><code>set &lt;name&gt; seek &lt;seconds&gt;</code><br>
  1373. Seek to position of track in seconds.
  1374. </ul>
  1375. <ul>
  1376. <br><code>set &lt;name&gt; speak "This is a test. 1 2 3."</code><br>
  1377. Speak the text followed after speak within quotes. Works with Google Translate.
  1378. </ul>
  1379. <ul>
  1380. <br><code>set &lt;name&gt; playEverywhere</code><br>
  1381. Only available for Caskeid players.<br>
  1382. Play current track on all available Caskeid players in sync.
  1383. </ul>
  1384. <ul>
  1385. <br><code>set &lt;name&gt; stopPlayEverywhere</code><br>
  1386. Only available for Caskeid players.<br>
  1387. Stops multiroom audio.
  1388. </ul>
  1389. <ul>
  1390. <br><code>set &lt;name&gt; addUnit &lt;unitName&gt;</code><br>
  1391. Only available for Caskeid players.<br>
  1392. Adds unit to multiroom audio session.
  1393. </ul>
  1394. <ul>
  1395. <br><code>set &lt;name&gt; removeUnit &lt;unitName&gt;</code><br>
  1396. Only available for Caskeid players.<br>
  1397. Removes unit from multiroom audio session.
  1398. </ul>
  1399. <ul>
  1400. <br><code>set &lt;name&gt; multiRoomVolume 0-100</code><br>
  1401. <code>set &lt;name&gt; multiRoomVolume +/-0-100</code><br>
  1402. Only available for Caskeid players.<br>
  1403. Set volume of all devices within this session.
  1404. </ul>
  1405. <ul>
  1406. <br><code>set &lt;name&gt; enableBTCaskeid</code><br>
  1407. Only available for Caskeid players.<br>
  1408. Activates Bluetooth Caskeid for this device.
  1409. </ul>
  1410. <ul>
  1411. <br><code>set &lt;name&gt; disableBTCaskeid</code><br>
  1412. Only available for Caskeid players.<br>
  1413. Deactivates Bluetooth Caskeid for this device.
  1414. </ul>
  1415. <ul>
  1416. <br><code>set &lt;name&gt; stereo &lt;left&gt; &lt;right&gt; &lt;pairName&gt;</code><br>
  1417. Only available for Caskeid players.<br>
  1418. Sets stereo mode for left/right speaker and defines the name of the stereo pair.
  1419. </ul>
  1420. <ul>
  1421. <br><code>set &lt;name&gt; standalone</code><br>
  1422. Only available for Caskeid players.<br>
  1423. Puts the speaker into standalone mode if it was member of a stereo pair before.
  1424. </ul>
  1425. <ul>
  1426. <br><code>set &lt;name&gt; saveGroupAs &lt;groupName&gt;</code><br>
  1427. Only available for Caskeid players.<br>
  1428. Saves the current group configuration (e.g. saveGroupAs LivingRoom).
  1429. </ul>
  1430. <ul>
  1431. <br><code>set &lt;name&gt; loadGroup &lt;groupName&gt;</code><br>
  1432. Only available for Caskeid players.<br>
  1433. Loads the configuration previously saved (e.g. loadGroup LivingRoom).
  1434. </ul>
  1435. <br>
  1436. <a name="DLNARendererattr"></a>
  1437. <b>Attributes</b>
  1438. <ul>
  1439. <br><code>ignoreUDNs</code><br>
  1440. Define list (comma or blank separated) of UDNs which should prevent automatic device creation.<br>
  1441. It is important that uuid: is also part of the UDN and must be included.
  1442. </ul>
  1443. </ul>
  1444. =end html
  1445. =cut