IP = $Sonos_IP; $this->PORT = $Sonos_Port; } protected function Upnp($url,$SOAP_service,$SOAP_action,$SOAP_arguments = '',$XML_filter = '') { $POST_xml = ''; $POST_xml .= ''; $POST_xml .= ''; $POST_xml .= $SOAP_arguments; $POST_xml .= ''; $POST_xml .= ''; $POST_xml .= ''; $POST_url = $this->IP.":".$this->PORT.$url; $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_URL, $POST_url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: text/xml", "SOAPAction: ".$SOAP_service."#".$SOAP_action)); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $POST_xml); $r = curl_exec($ch); curl_close($ch); if ($XML_filter != '') return $this->Filter($r,$XML_filter); else return $r; } protected function Filter($subject,$pattern) { preg_match('/\<'.$pattern.'\>(.+)\<\/'.$pattern.'\>/',$subject,$matches); ///'/\<'.$pattern.'\>(.+)\<\/'.$pattern.'\>/' return $matches[1]; } /** * Play */ public function Play() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Play'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '01'; return $this->Upnp($url,$service,$action,$args); } /** * Pause */ public function Pause() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Pause'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; return $this->Upnp($url,$service,$action,$args); } /** * Stop */ public function Stop() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Stop'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; return $this->Upnp($url,$service,$action,$args); } /** * Next */ public function Next() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Next'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; return $this->Upnp($url,$service,$action,$args); } /** * Previous */ public function Previous() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Previous'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; return $this->Upnp($url,$service,$action,$args); } /** * Seek to position xx:xx:xx or track number x * @param string 'REL_TIME' for time position (xx:xx:xx) or 'TRACK_NR' for track in actual queue * @param string */ public function Seek($type,$position) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'Seek'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'.$type.''.$position.''; return $this->Upnp($url,$service,$action,$args); } /** * Seek to time xx:xx:xx */ public function SeekTime($time) { return $this->Seek("REL_TIME",$time); } /** * Change to track number */ public function ChangeTrack($number) { return $this->Seek("TRACK_NR",$number); } /** * Restart actual track */ public function RestartTrack() { return $this->Seek("REL_TIME","00:00:00"); } /** * Restart actual queue */ public function RestartQueue() { return $this->Seek("TRACK_NR","1"); } /** * Get volume value (0-100) */ public function GetVolume() { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'GetVolume'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '0Master'; $filter = 'CurrentVolume'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Set volume value (0-100) */ public function SetVolume($volume) { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'SetVolume'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '0Master'.$volume.''; return $this->Upnp($url,$service,$action,$args); } /** * Get mute status */ public function GetMute() { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'GetMute'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '0Master'; $filter = 'CurrentMute'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Set mute * @param integer mute active=1 */ public function SetMute($mute = 0) { $url = '/MediaRenderer/RenderingControl/Control'; $action = 'SetMute'; $service = 'urn:schemas-upnp-org:service:RenderingControl:1'; $args = '0Master'.$mute.''; return $this->Upnp($url,$service,$action,$args); } /** * Get Transport Info : get status about player */ public function GetTransportInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetTransportInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; $filter = 'CurrentTransportState'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Get Media Info : get informations about media */ public function GetMediaInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetMediaInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; $filter = 'CurrentURI'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Get Position Info : get some informations about track */ public function GetPositionInfo() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'GetPositionInfo'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; $xml = $this->Upnp($url,$service,$action,$args); $data["TrackNumberInQueue"] = $this->Filter($xml,"Track"); $data["TrackURI"] = $this->Filter($xml,"TrackURI"); $data["TrackDuration"] = $this->Filter($xml,"TrackDuration"); $data["RelTime"] = $this->Filter($xml,"RelTime"); $TrackMetaData = $this->Filter($xml,"TrackMetaData"); $xml = substr($xml, stripos($TrackMetaData, '<')); $xml = substr($xml, 0, strrpos($xml, '>') + 4); $xml = str_replace(array("<", ">", """, "&", "%3a", "%2f", "%25"), array("<", ">", "\"", "&", ":", "/", "%"), $xml); $data["Title"] = $this->Filter($xml,"dc:title"); // Track Title $data["AlbumArtist"] = $this->Filter($xml,"r:albumArtist"); // Album Artist $data["Album"] = $this->Filter($xml,"upnp:album"); // Album Title $data["TitleArtist"] = $this->Filter($xml,"dc:creator"); // Track Artist return $data; } /** * Add URI to Queue * @param string track/radio URI * @param bool added next (=1) or end queue (=0) */ public function AddURIToQueue($URI,$next=0) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'AddURIToQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $next = (int)$next; $args = '0'.$URI.'0'.$next.''; $filter = 'FirstTrackNumberEnqueued'; return $this->Upnp($url,$service,$action,$args,$filter); } /** * Remove a track from Queue * */ public function RemoveTrackFromQueue($tracknumber) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'RemoveTrackFromQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0Q:0/'.$tracknumber.''; return $this->Upnp($url,$service,$action,$args); } /** * Clear Queue * */ public function RemoveAllTracksFromQueue() { $url = '/MediaRenderer/AVTransport/Control'; $action = 'RemoveAllTracksFromQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'; return $this->Upnp($url,$service,$action,$args); } /** * Set Queue * @param string URI of new track */ public function SetQueue($URI) { $url = '/MediaRenderer/AVTransport/Control'; $action = 'SetAVTransportURI'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $args = '0'.$URI.''; return $this->Upnp($url,$service,$action,$args); } /** * Refresh music library * */ public function RefreshShareIndex() { $url = '/MediaServer/ContentDirectory/Control'; $action = 'RefreshShareIndex'; $service = 'urn:schemas-upnp-org:service:ContentDirectory:1'; return $this->Upnp($url,$service,$action,$args); } /** * Split string in several strings * */ protected function CutString($string,$intmax) { $i = 0; while (strlen($string) > $intmax) { $string_cut = substr($string, 0, $intmax); $last_space = strrpos($string_cut, "+"); $strings[$i] = substr($string, 0, $last_space); $string = substr($string, $last_space, strlen($string)); $i++; } $strings[$i] = $string; return $strings; } /** * Convert Words (text) to Speech (MP3) * */ protected function TTSToMp3($words,$lang) { // Directory $folder = "audio/".$lang; // Replace the non-alphanumeric characters // The spaces in the sentence are replaced with the Plus symbol $words = urlencode($words); // Name of the MP3 file generated using the MD5 hash $file = md5($words); // If folder doesn't exists, create it if (!file_exists($folder)) mkdir($folder, 0755, true); // Save the MP3 file in this folder with the .mp3 extension $file = $folder."/TTS-".$file.".mp3"; // If the MP3 file exists, do not create a new request if (!file_exists($file)) { // Google Translate API cannot handle strings > 100 characters $words = $this->CutString($words,100); ini_set('user_agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0'); $mp3 = ""; for ($i = 0; $i < count($words); $i++) $mp3[$i] = file_get_contents('http://translate.google.com/translate_tts?q='.$words[$i].'&tl='.$lang); file_put_contents($file, $mp3); } return $file; } /** * Say song name via TTS message * @param string message * @param string radio name display on sonos controller * @param int volume * @param string language */ public function SongNameTTS($directory,$volume=0,$unmute=0,$lang='fr') { $ThisSong = "Cette chanson s'appelle "; $By = " de "; $actual['track'] = $this->GetPositionInfo(); $SongName = $actual['track']['Title']; $Artist = $actual['track']['TitleArtist']; $message = $ThisSong . $SongName . $By . $Artist ; $this->PlayTTS($message,$directory,$volume,$unmute,$lang); return true; } /** * Play a TTS message * @param string message * @param string radio name display on sonos controller * @param int volume * @param string language */ public function PlayTTS($message,$directory,$volume=0,$unmute=0,$lang='fr') { $actual['track'] = $this->GetPositionInfo(); $actual['volume'] = $this->GetVolume(); $actual['mute'] = $this->GetMute(); $actual['status'] = $this->GetTransportInfo(); $this->Pause(); if ($unmute == 1) $this->SetMute(0); if ($volume != 0) $this->SetVolume($volume); $file = 'x-file-cifs://'.$directory.'/'.$this->TTSToMp3($message,$lang); if (((stripos($actual['track']["TrackURI"],"x-file-cifs://")) != false) or ((stripos($actual['track']["TrackURI"],".mp3")) != false)) { // It's a MP3 file $TrackNumber = $this->AddURIToQueue($file); $this->ChangeTrack($TrackNumber); $this->Play(); sleep(2); while ($this->GetTransportInfo() == "PLAYING") {} $this->Pause(); $this->SetVolume($actual['volume']); $this->SetMute($actual['mute']); $this->ChangeTrack($actual['track']["TrackNumberInQueue"]); $this->SeekTime($actual['track']["RelTime"]); $this->RemoveTrackFromQueue($TrackNumber); } else { //It's a radio / or TV (playbar) / or nothing $this->SetQueue($file); $this->Play(); sleep(2); while ($this->GetTransportInfo() == "PLAYING") {} $this->Pause(); $this->SetVolume($actual['volume']); $this->SetMute($actual['mute']); $this->SetQueue($actual['track']["TrackURI"]); } if (strcmp($actual['status'],"PLAYING") == 0) $this->Play(); return true; } public function AddSpotifyToQueue($spotify_id, $next = false) { $rand = mt_rand(10000000, 99999999); $meta = ' object.item.audioItem.musicTrack SA_RINCON2311_X_#Svc2311-0-Token '; $meta = htmlentities($meta); $url = '/MediaRenderer/AVTransport/Control'; $action = 'AddURIToQueue'; $service = 'urn:schemas-upnp-org:service:AVTransport:1'; $next = (int)$next; $args = " 0 x-sonos-spotify:spotify%3atrack%3a{$spotify_id} {$meta} 0 {$next} "; $filter = 'FirstTrackNumberEnqueued'; return $this->Upnp($url, $service, $action, $args, $filter); } public function device_info() { $xml = $this->_device_info_raw('/xml/device_description.xml'); $out = array( 'friendlyName' => (string)$xml->device->friendlyName, 'modelNumber' => (string)$xml->device->modelNumber, 'modelName' => (string)$xml->device->modelName, 'softwareVersion' => (string)$xml->device->softwareVersion, 'hardwareVersion' => (string)$xml->device->hardwareVersion, 'roomName' => (string)$xml->device->roomName, ); return $out; } public function get_coordinator() { $topology = $this->_device_info_raw('/status/topology'); $myself = null; $coordinators = array(); // Loop players, build map of coordinators and find myself foreach ($topology->ZonePlayers->ZonePlayer as $player) { $player_data = $player->attributes(); $url = parse_url((string)$player_data->location); $ip = $url['host']; if ($ip == $this->IP) { $myself = $player_data; } if ((string)$player_data->coordinator == 'true') { $coordinators[(string)$player_data->group] = $ip; } } $coordinator = $coordinators[(string)$myself->group]; return new static($coordinator); } protected function _device_info_raw($url) { $url = "http://{$this->IP}:{$this->PORT}{$url}"; if (!isset($this->_raw[$url])) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch); curl_close($ch); $this->_raw[$url] = simplexml_load_string($data); } return $this->_raw[$url]; } public static function detect($ip = '239.255.255.250', $port = 1900) { $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); socket_set_option($sock, getprotobyname('ip'), IP_MULTICAST_TTL, 2); $data = <<device_info(); if ($dev['roomName'] == $room_name) { return $device->get_coordinator(); } } return false; } }