98_DLNARenderer.pm 58 KB

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