1
0
Fork 0
mirror of https://github.com/Yetangitu/ampache synced 2025-10-03 17:59:21 +02:00

UPnP localplay implementation

This commit is contained in:
SeregaPru 2015-03-21 01:43:30 +04:00
parent 5d09e7dcad
commit 7463e75204
11 changed files with 1578 additions and 65 deletions

View file

@ -923,7 +923,7 @@ abstract class Catalog extends database_object
* @param array|null $catalogs
* @return \Artist[]
*/
public static function get_artists($catalogs = null)
public static function get_artists($size = 0, $offset = 0, $catalogs = null)
{
$sql_where = "";
if (is_array($catalogs) && count($catalogs)) {
@ -931,7 +931,18 @@ abstract class Catalog extends database_object
$sql_where = "WHERE `song`.`catalog` IN $catlist";
}
$sql = "SELECT `artist`.id, `artist`.`name`, `artist`.`summary` FROM `song` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` $sql_where GROUP BY `song`.artist ORDER BY `artist`.`name`";
$sql_limit = "";
if ($offset > 0 && $size > 0) {
$sql_limit = "LIMIT $offset, $size";
} elseif ($size > 0) {
$sql_limit = "LIMIT $size";
} elseif ($offset > 0) {
// MySQL doesn't have notation for last row, so we have to use the largest possible BIGINT value
// https://dev.mysql.com/doc/refman/5.0/en/select.html
$sql_limit = "LIMIT $offset, 18446744073709551615";
}
$sql = "SELECT `artist`.id, `artist`.`name`, `artist`.`summary` FROM `song` LEFT JOIN `artist` ON `artist`.`id` = `song`.`artist` $sql_where GROUP BY `song`.artist ORDER BY `artist`.`name` $sql_limit";
$results = array();
$db_results = Dba::read($sql);

View file

@ -55,10 +55,10 @@ class Upnp_Api
socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);
socket_sendto($socket, $buf, strlen($buf), 0, $host, $port);
socket_close($socket);
usleep($delay*1000);
usleep($delay * 1000);
}
public static function sddpSend($delay=15, $host="239.255.255.250", $port=1900)
public static function sddpSend($delay=15, $host="239.255.255.250", $port=1900, $prefix="NT")
{
$strHeader = 'NOTIFY * HTTP/1.1' . "\r\n";
$strHeader .= 'HOST: ' . $host . ':' . $port . "\r\n";
@ -66,28 +66,28 @@ class Upnp_Api
$strHeader .= 'SERVER: DLNADOC/1.50 UPnP/1.0 Ampache/3.7' . "\r\n";
$strHeader .= 'CACHE-CONTROL: max-age=1800' . "\r\n";
$strHeader .= 'NTS: ssdp:alive' . "\r\n";
$rootDevice = 'NT: upnp:rootdevice' . "\r\n";
$rootDevice .= 'USN: uuid:' . self::UUIDSTR . '::upnp:rootdevice' . "\r\n". "\r\n";
$rootDevice = $prefix . ': upnp:rootdevice' . "\r\n";
$rootDevice .= 'USN: uuid:' . self::UUIDSTR . '::upnp:rootdevice' . "\r\n". "\r\n";
$buf = $strHeader . $rootDevice;
self::udpSend($buf, $delay, $host, $port);
$uuid = 'NT: uuid:' . self::UUIDSTR . "\r\n";
$uuid = $prefix . ': uuid:' . self::UUIDSTR . "\r\n";
$uuid .= 'USN: uuid:' . self::UUIDSTR . "\r\n". "\r\n";
$buf = $strHeader . $uuid;
self::udpSend($buf, $delay, $host, $port);
$deviceType = 'NT: urn:schemas-upnp-org:device:MediaServer:1' . "\r\n";
$deviceType = $prefix . ': urn:schemas-upnp-org:device:MediaServer:1' . "\r\n";
$deviceType .= 'USN: uuid:' . self::UUIDSTR . '::urn:schemas-upnp-org:device:MediaServer:1' . "\r\n". "\r\n";
$buf = $strHeader . $deviceType;
self::udpSend($buf, $delay, $host, $port);
$serviceCM = 'NT: urn:schemas-upnp-org:service:ConnectionManager:1' . "\r\n";
$serviceCM = $prefix . ': urn:schemas-upnp-org:service:ConnectionManager:1' . "\r\n";
$serviceCM .= 'USN: uuid:' . self::UUIDSTR . '::urn:schemas-upnp-org:service:ConnectionManager:1' . "\r\n". "\r\n";
$buf = $strHeader . $serviceCM;
self::udpSend($buf, $delay, $host, $port);
$serviceCD = 'NT: urn:schemas-upnp-org:service:ContentDirectory:1' . "\r\n";
$serviceCD = $prefix . ': urn:schemas-upnp-org:service:ContentDirectory:1' . "\r\n";
$serviceCD .= 'USN: uuid:' . self::UUIDSTR . '::urn:schemas-upnp-org:service:ContentDirectory:1' . "\r\n". "\r\n";
$buf = $strHeader . $serviceCD;
self::udpSend($buf, $delay, $host, $port);
@ -466,8 +466,10 @@ class Upnp_Api
case 'artists':
switch (count($pathreq)) {
case 1: // Get artists list
$artists = Catalog::get_artists();
list($maxCount, $artists) = self::_slice($artists, $start, $count);
//$artists = Catalog::get_artists();
//list($maxCount, $artists) = self::_slice($artists, $start, $count);
$artists = Catalog::get_artists($count, $start);
list($maxCount, $artists) = array(999999, $artists);
foreach ($artists as $artist) {
$artist->format();
$mediaItems[] = self::_itemArtist($artist, $parent);
@ -491,8 +493,10 @@ class Upnp_Api
case 'albums':
switch (count($pathreq)) {
case 1: // Get albums list
$album_ids = Catalog::get_albums();
list($maxCount, $album_ids) = self::_slice($album_ids, $start, $count);
//!!$album_ids = Catalog::get_albums();
//!!list($maxCount, $album_ids) = self::_slice($album_ids, $start, $count);
$album_ids = Catalog::get_albums($count, $start);
list($maxCount, $album_ids) = array(999999, $album_ids);
foreach ($album_ids as $album_id) {
$album = new Album($album_id);
$album->format();
@ -849,10 +853,10 @@ class Upnp_Api
private static function _replaceSpecialSymbols($title)
{
debug_event('upnp_class', 'replace <<< ' . $title, 5);
///debug_event('upnp_class', 'replace <<< ' . $title, 5);
// replace non letter or digits
$title = preg_replace('~[^\\pL\d\.\s\(\)]+~u', '-', $title);
debug_event('upnp_class', 'replace >>> ' . $title, 5);
$title = preg_replace('~[^\\pL\d\.\s\(\)\.\,\'\"]+~u', '-', $title);
///debug_event('upnp_class', 'replace >>> ' . $title, 5);
if ($title == "")
$title = '(no title)';
@ -912,7 +916,7 @@ class Upnp_Api
);
}
private static function _itemSong($song, $parent)
public static function _itemSong($song, $parent)
{
$api_session = (AmpConfig::get('require_session')) ? Stream::get_session() : false;
$art_url = Art::url($song->album, 'album', $api_session);
@ -927,8 +931,8 @@ class Upnp_Api
'dc:title' => self::_replaceSpecialSymbols($song->f_title),
'upnp:class' => (isset($arrFileType['class'])) ? $arrFileType['class'] : 'object.item.unknownItem',
'upnp:albumArtURI' => $art_url,
'upnp:artist' => $song->f_artist,
'upnp:album' => $song->f_album,
'upnp:artist' => self::_replaceSpecialSymbols($song->f_artist),
'upnp:album' => self::_replaceSpecialSymbols($song->f_album),
'upnp:genre' => Tag::get_display($song->tags, false, 'song'),
//'dc:date' => date("c", $song->addition_time),
'upnp:originalTrackNumber' => $song->track,
@ -940,7 +944,7 @@ class Upnp_Api
'bitrate' => $song->bitrate,
'sampleFrequency' => $song->rate,
//'nrAudioChannels' => '1',
'description' => $song->comment,
'description' => self::_replaceSpecialSymbols($song->comment),
);
}

View file

@ -0,0 +1,519 @@
<?php
/* vim:set softtabstop=4 shiftwidth=4 expandtab: */
/**
*
* LICENSE: GNU General Public License, version 2 (GPLv2)
* Copyright 2001 - 2014 Ampache.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License v2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
/**
* AmpacheUPnp Class
*
* This is the class for the UPnP localplay method to remote control
* a UPnP player Instance
*
*/
class AmpacheUPnP extends localplay_controller
{
/* Variables */
private $_version = '000001';
private $_description = 'Controls a UPnP instance';
/* @var UPnPPlayer $object */
private $_upnp;
/**
* Constructor
* This returns the array map for the localplay object
* REQUIRED for Localplay
*/
public function __construct()
{
/* Do a Require Once On the needed Libraries */
require_once AmpConfig::get('prefix') . '/modules/upnp/upnpplayer.class.php';
}
/**
* get_description
* This returns the description of this localplay method
*/
public function get_description()
{
return $this->_description;
}
/**
* get_version
* This returns the current version
*/
public function get_version()
{
return $this->_version;
}
/**
* is_installed
* This returns true or false if upnp controller is installed
*/
public function is_installed()
{
$sql = "SHOW TABLES LIKE 'localplay_upnp'";
$db_results = Dba::query($sql);
return (Dba::num_rows($db_results) > 0);
}
/**
* install
* This function installs the upnp localplay controller
*/
public function install()
{
$sql = "CREATE TABLE `localplay_upnp` (`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , ".
"`name` VARCHAR( 128 ) COLLATE utf8_unicode_ci NOT NULL , " .
"`owner` INT( 11 ) NOT NULL, " .
"`url` VARCHAR( 255 ) COLLATE utf8_unicode_ci NOT NULL " .
") ENGINE = MYISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
$db_results = Dba::query($sql);
// Add an internal preference for the users current active instance
Preference::insert('upnp_active', 'UPnP Active Instance', '0', '25', 'integer', 'internal');
User::rebuild_all_preferences();
return true;
}
/**
* uninstall
* This removes the localplay controller
*/
public function uninstall()
{
$sql = "DROP TABLE `localplay_upnp`";
$db_results = Dba::query($sql);
// Remove the pref we added for this
Preference::delete('upnp_active');
return true;
}
/**
* add_instance
* This takes key'd data and inserts a new upnp instance
*/
public function add_instance($data)
{
$sql = "INSERT INTO `localplay_upnp` (`name`,`url`, `owner`) " .
"VALUES (?, ?, ?)";
$db_results = Dba::query($sql, array($data['name'], $data['url'], $GLOBALS['user']->id));
return $db_results;
}
/**
* delete_instance
* This takes a UID and deletes the instance in question
*/
public function delete_instance($uid)
{
$sql = "DELETE FROM `localplay_upnp` WHERE `id` = ?";
$db_results = Dba::query($sql, array($uid));
return true;
}
/**
* get_instances
* This returns a key'd array of the instance information with
* [UID]=>[NAME]
*/
public function get_instances()
{
$sql = "SELECT * FROM `localplay_upnp` ORDER BY `name`";
$db_results = Dba::query($sql);
$results = array();
while ($row = Dba::fetch_assoc($db_results)) {
$results[$row['id']] = $row['name'];
}
return $results;
}
/**
* update_instance
* This takes an ID and an array of data and updates the instance specified
*/
public function update_instance($uid, $data)
{
$sql = "UPDATE `localplay_upnp` SET `url` = ?, `name` = ? WHERE `id` = ?";
$db_results = Dba::query($sql, array($data['url'], $data['name'], $uid));
return true;
}
/**
* instance_fields
* This returns a key'd array of [NAME]=>array([DESCRIPTION]=>VALUE,[TYPE]=>VALUE) for the
* fields so that we can on-the-fly generate a form
*/
public function instance_fields()
{
$fields['name'] = array('description' => T_('Instance Name'), 'type'=>'textbox');
$fields['url'] = array('description' => T_('URL'), 'type'=>'textbox');
return $fields;
}
/**
* get_instance
* This returns a single instance and all it's variables
*/
public function get_instance($instance='')
{
$instance = $instance ? $instance : AmpConfig::get('upnp_active');
$sql = "SELECT * FROM `localplay_upnp` WHERE `id` = ?";
$db_results = Dba::query($sql, array($instance));
$row = Dba::fetch_assoc($db_results);
return $row;
}
/**
* set_active_instance
* This sets the specified instance as the 'active' one
*/
public function set_active_instance($uid, $user_id='')
{
// Not an admin? bubkiss!
if (!$GLOBALS['user']->has_access('100')) {
$user_id = $GLOBALS['user']->id;
}
$user_id = $user_id ? $user_id : $GLOBALS['user']->id;
debug_event('upnp', 'set_active_instance userid: ' . $user_id, 5);
Preference::update('upnp_active', $user_id, intval($uid));
AmpConfig::set('upnp_active', intval($uid), true);
return true;
}
/**
* get_active_instance
* This returns the UID of the current active instance
* false if none are active
*/
public function get_active_instance()
{
}
public function add_url(Stream_URL $url)
{
debug_event('upnp', 'add_url: ' . $url->title . " | " . $url->url, 5);
if (!$this->_upnp) {
return false;
}
$this->_upnp->PlaylistAdd($url->title, $url->url);
return true;
}
/**
* delete_track
* Delete a track from the upnp playlist
*/
public function delete_track($track)
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->PlaylistRemove($track);
return true;
}
/**
* clear_playlist
* This deletes the entire upnp playlist.
*/
public function clear_playlist()
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->PlaylistClear();
return true;
}
/**
* play
* This just tells upnp to start playing, it does not
* take any arguments
*/
public function play()
{
if (!$this->_upnp) {
return false;
}
return $this->_upnp->Play();
}
/**
* pause
* This tells upnp to pause the current song
*/
public function pause()
{
if (!$this->_upnp) {
return false;
}
return $this->_upnp->Pause();
}
/**
* stop
* This just tells upnp to stop playing, it does not take
* any arguments
*/
public function stop()
{
if (!$this->_upnp) {
return false;
}
return $this->_upnp->Stop();
}
/**
* skip
* This tells upnp to skip to the specified song
*/
public function skip($pos)
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->Skip($pos);
return true;
}
/**
* next
* This just tells upnp to skip to the next song
*/
public function next()
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->Next();
return true;
}
/**
* prev
* This just tells upnp to skip to the prev song
*/
public function prev()
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->Prev();
return true;
}
/**
* volume
* This tells upnp to set the volume to the specified amount
*/
public function volume($volume)
{
if (!$this->_upnp) {
return false;
}
$this->_upnp->SetVolume($volume);
return true;
}
/**
* This tells upnp to increase the volume
*/
public function volume_up()
{
if (!$this->_upnp) {
return false;
}
return $this->_upnp->VolumeUp();
}
/**
* This tells upnp to decrease the volume
*/
public function volume_down()
{
if (!$this->_upnp) {
return false;
}
return $this->_upnp->VolumeDown();
}
/**
* repeat
* This tells upnp to set the repeating the playlist (i.e. loop) to either on or off
*/
public function repeat($state)
{
debug_event('upnp', 'repeat: ' . $state, 5);
if (!$this->_upnp) {
return false;
}
$this->_upnp->Repeat(array(
'repeat' => ($state ? 'all' : 'off')
));
return true;
}
/**
* random
* This tells upnp to turn on or off the playing of songs from the playlist in random order
*/
public function random($onoff)
{
debug_event('upnp', 'random: ' . $onoff, 5);
if (!$this->_upnp) {
return false;
}
$this->_upnp->PlayShuffle($onoff);
return true;
}
/**
* get
* This functions returns an array containing information about
* The songs that upnp currently has in it's playlist. This must be
* done in a standardized fashion
*/
public function get()
{
debug_event('upnp', 'get', 5);
if (!$this->_upnp) {
return false;
}
$playlist = $this->_upnp->GetPlaylistItems();
$results = array();
$idx = 1;
foreach ($playlist as $key => $item) {
$data = array();
$data['link'] = $item['link'];
$data['id'] = $idx;
$data['track'] = $idx;
$url_data = Stream_URL::parse($item['link']);
if ($url_data != null) {
$song = new Song($url_data['id']);
if ($song != null) {
$data['name'] = $song->get_artist_name() . ' - ' . $song->title;
}
}
if (!$data['name']) {
$data['name'] = $item['name'];
}
$results[] = $data;
$idx++;
}
return $results;
}
/**
* status
* This returns bool/int values for features, loop, repeat and any other features
* that this localplay method supports.
* This works as in requesting the upnp properties
*/
public function status()
{
debug_event('upnp', 'status', 5);
if (!$this->_upnp) {
return false;
}
$item = $this->_upnp->GetCurrentItem();
$status = array();
$status['state'] = $this->_upnp->GetState();
$status['volume'] = $this->_upnp->GetVolume();
$status['repeat'] = false;
$status['random'] = false;
$status['track'] = $item['link'];
$status['track_title'] = $item['name'];
$url_data = Stream_URL::parse($item['link']);
if ($url_data != null) {
$song = new Song($url_data['id']);
if ($song != null) {
$status['track_artist'] = $song->get_artist_name();
$status['track_album'] = $song->get_album_name();
}
}
return $status;
}
/**
* connect
* This functions creates the connection to upnp and returns
* a boolean value for the status, to save time this handle
* is stored in this class
*/
public function connect()
{
$options = self::get_instance();
debug_event('upnp', 'Trying to connect upnp instance ' . $options['name'] . ' ( ' . $options['url'] . ' )', '5');
$this->_upnp = new UPnPPlayer($options['name'], $options['url']);
debug_event('upnp', 'Connected.', '5');
return true;
}
}

259
modules/upnp/upnpdevice.php Normal file
View file

@ -0,0 +1,259 @@
<?php
class UPnPDevice
{
private $_settings = array(
"descriptionURL" => "",
"host" => "",
"controlURLs" => array(),
"eventURLs" => array()
);
public function UPnPDevice($descriptionUrl)
{
if (! $this->restoreDescriptionUrl($descriptionUrl))
$this->parseDescriptionUrl($descriptionUrl);
}
/*
* Reads description URL from session
*/
private function restoreDescriptionUrl($descriptionUrl)
{
debug_event('upnpdevice', 'readDescriptionUrl: ' . $descriptionUrl, 5);
$this->_settings = unserialize(Session::read('upnp_dev_' . $descriptionUrl));
if ($this->_settings['descriptionURL'] == $descriptionUrl) {
debug_event('upnpdevice', 'service Urls restored from session.', 5);
return true;
}
return false;
}
private function parseDescriptionUrl($descriptionUrl)
{
debug_event('upnpdevice', 'parseDescriptionUrl: ' . $descriptionUrl, 5);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $descriptionUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
//!!debug_event('upnpdevice', 'parseDescriptionUrl response: ' . $response, 5);
$responseXML = simplexml_load_string($response);
$services = $responseXML->device->serviceList->service;
foreach ($services as $service)
{
$serviceType = $service->serviceType;
$serviceTypeNames = explode(":", $serviceType);
$serviceTypeName = $serviceTypeNames[3];
$this->_settings['controlURLs'][$serviceTypeName] = (string)$service->controlURL;
$this->_settings['eventURLs'][$serviceTypeName] = (string)$service->eventSubURL;
}
$urldata = parse_url($descriptionUrl);
$this->_settings['host'] = $urldata['scheme'] . '://' . $urldata['host'] . ':' . $urldata['port'];
$this->_settings['descriptionURL'] = $descriptionUrl;
Session::create(array(
'type' => 'api',
'sid' => 'upnp_dev_' . $descriptionUrl,
'value' => serialize($this->_settings)
));
}
/**
* Sending HTTP-Request and returns parsed response
*
* @param string $method Method name
* @param array $arguments Key-Value array
*/
public function sendRequestToDevice( $method, $arguments, $type = 'RenderingControl')
{
$body ='<?xml version="1.0" encoding="utf-8"?>';
$body .='<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body>';
$body .=' <u:' . $method . ' xmlns:u="urn:schemas-upnp-org:service:' . $type . ':1">';
foreach( $arguments as $arg=>$value ) {
$body .=' <'.$arg.'>'.$value.'</'.$arg.'>';
}
$body .=' </u:' . $method . '>';
$body .='</s:Body></s:Envelope>';
$controlUrl = $this->_settings['host'] . ((substr($this->_settings['controlURLs'][$type], 0, 1) != "/") ? "/" : "") . $this->_settings['controlURLs'][$type];
//!! TODO - need to use scheme in header ??
$header = array(
'SOAPACTION: "urn:schemas-upnp-org:service:' . $type . ':1#' . $method . '"',
'CONTENT-TYPE: text/xml; charset="utf-8"',
'HOST: ' . $this->_settings['host'],
'Connection: close',
'Content-Length: ' . mb_strlen($body),
);
//debug_event('upnpdevice', 'sendRequestToDevice Met: ' . $method . ' | ' . $controlUrl, 5);
//debug_event('upnpdevice', 'sendRequestToDevice Body: ' . $body, 5);
//debug_event('upnpdevice', 'sendRequestToDevice Hdr: ' . print_r($header, true), 5);
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $controlUrl );
curl_setopt( $ch, CURLOPT_POST, 1 );
curl_setopt( $ch, CURLOPT_POSTFIELDS, $body );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, TRUE );
curl_setopt( $ch, CURLOPT_HEADER, TRUE );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $header );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, TRUE );
$response = curl_exec( $ch );
curl_close( $ch );
//debug_event('upnpdevice', 'sendRequestToDevice response: ' . $response, 5);
$headers = array();
$tmp = explode("\r\n\r\n", $response);
foreach($tmp as $key => $value)
{
if(substr($value, 0, 8) == 'HTTP/1.1')
{
$headers[] = $tmp[$key];
unset($tmp[$key]);
}
}
$response = join("\r\n", $tmp);
/*
$lastHeaders = $headers[count($headers) - 1];
$responseCode = $this->getResponseCode($lastHeaders);
debug_event('upnpdevice', 'sendRequestToDevice responseCode: ' . $responseCode, 5);
if ($responseCode == 500)
{
debug_event('upnpdevice', 'sendRequestToDevice HTTP-Code 500 - Create error response', 5);
}
else
{
debug_event('upnpdevice', 'sendRequestToDevice HTTP-Code OK - Create response', 5);
}
*/
return $response;
}
/**
* Filters response HTTP-Code from response headers
* @param string $headers HTTP response headers
* @return mixed Response code (int) or null if not found
*/
/*
private function getResponseCode($headers)
{
$tmp = explode("\n", $headers);
$firstLine = array_shift($tmp);
if(substr($headers, 0, 8) == 'HTTP/1.1') {
return substr($headers, 9, 3);
}
return null;
}
*/
// helper function for calls that require only an instance id
public function instanceOnly($command, $type = 'AVTransport', $id = 0)
{
$args = array( 'InstanceID' => $id );
$response = $this->sendRequestToDevice($command, $args, $type);
///$response = \Format::forge($response,'xml:ns')->to_array();
///return $response['s:Body']['u:' . $command . 'Response'];
return $response;
}
//!! UPNP subscription work not for all renderers, and works strange
//!! so now is not used
/**
* Subscribe
* Subscribe to UPnP event
*/
/*
public function Subscribe($type = 'AVTransport')
{
$web_path = AmpConfig::get('web_path');
$eventSubsUrl = $web_path . '/upnp/play-event.php?device=' . urlencode($this->_descrUrl);
$eventUrl = $this->_host . $this->_eventURLs[$type];
$header = array(
'HOST: ' . $this->_host,
'CALLBACK: <' . $eventSubsUrl . '>',
'NT: upnp:event',
'TIMEOUT: Second-180',
);
debug_event('upnpdevice', 'Subscribe with: ' . print_r($header, true), 5);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $eventUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'SUBSCRIBE');
$response = curl_exec($ch);
curl_close( $ch );
debug_event('upnpdevice', 'Subscribe response: ' . $response, 5);
$lines = explode("\r\n", trim($response));
foreach($lines as $line) {
$tmp = explode(':', $line);
$key = strtoupper(trim(array_shift($tmp)));
$value = trim(join(':', $tmp));
if ($key == 'SID')
{
debug_event('upnpdevice', 'Subscribtion SID: ' . $value, 5);
return $value;
}
}
return null;
}
*/
//!! UPNP subscription work not for all renderers, and works strange
//!! so now is not used
/**
* UnSubscribe
* Unsubscribe from UPnP event
*/
/*
public function UnSubscribe($sid, $type = 'AVTransport')
{
if (empty($sid))
return;
$eventUrl = $this->_host . $this->_eventURLs[$type];
$header = array(
'HOST: ' . $this->_host,
'SID: ' . $sid,
);
debug_event('upnpdevice', 'Unsubscribe from SID: ' . $sid . ' with: ' . "\n" . print_r($header, true), 5);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $eventUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'UNSUBSCRIBE');
$response = curl_exec($ch);
curl_close( $ch );
}
*/
}

View file

@ -0,0 +1,384 @@
<?php
/**
*
* LICENSE: GNU General Public License, version 2 (GPLv2)
* Copyright 2001 - 2015 Ampache.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License v2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
/**
* UPnPPlayer Class
*
* This player controls an instance of UPnP player
*
*/
class UPnPPlayer
{
/* @var UPnPPlaylist $object */
private $_playlist = null;
/* @var UPnPDevice $object */
private $_device;
private $_description_url = null;
// 0 - stopped, 1 - playing
private $_intState = 0;
/**
* Lazy initialization for UPNP device property
* @return UPnPDevice
*/
private function Device()
{
if (is_null($this->_device))
$this->_device = new UPnPDevice($this->_description_url);
return $this->_device;
}
/**
* Lazy initialization for UPNP playlist property
* @return UPnPPlaylist
*/
private function Playlist()
{
if (is_null($this->_playlist))
$this->_playlist = new UPnPPlaylist($this->_description_url);
return $this->_playlist;
}
/**
* UPnPPlayer
* This is the constructor,
*/
public function UPnPPlayer($name = "noname", $description_url = "http://localhost")
{
require_once AmpConfig::get('prefix') . '/modules/upnp/upnpdevice.php';
require_once AmpConfig::get('prefix') . '/modules/upnp/upnpplaylist.php';
debug_event('upnpPlayer', 'constructor: ' . $name . ' | ' . $description_url, 5);
$this->_description_url = $description_url;
$this->ReadIndState();
}
/**
* add
* append a song to the playlist
* $name Name to be shown in the playlist
* $link URL of the song
*/
public function PlayListAdd($name, $link)
{
$this->Playlist()->Add($name, $link);
return true;
}
/**
* delete_pos
* This deletes a specific track
*/
public function PlaylistRemove($track)
{
$this->Playlist()->RemoveTrack($track);
return true;
}
public function PlaylistClear()
{
$this->Playlist()->Clear();
return true;
}
/**
* GetPlayListItems
* This returns a delimited string of all of the filenames
* current in your playlist, only url's at the moment
*/
public function GetPlaylistItems()
{
return $this->Playlist()->AllItems();
}
public function GetCurrentItem()
{
return $this->Playlist()->CurrentItem();
}
public function GetState()
{
$response = $this->Device()->instanceOnly('GetTransportInfo');
$responseXML = simplexml_load_string($response);
list($state) = $responseXML->xpath('//CurrentTransportState');
//!!debug_event('upnpPlayer', 'GetState = ' . $state, 5);
return $state;
}
/**
* next
* go to next song
*/
public function Next($forcePlay = true)
{
// get current internal play state, for case if someone has changed it
if (! $forcePlay)
$this->ReadIndState();
if (($forcePlay || ($this->_intState == 1)) && ($this->Playlist()->Next()))
{
$this->Play();
return true;
}
return false;
}
/**
* prev
* go to previous song
*/
public function Prev()
{
if ($this->Playlist()->Prev()) {
$this->Play();
return true;
}
return false;
}
/**
* skip
* This skips to POS in the playlist
*/
public function Skip($pos)
{
if ($this->Playlist()->Skip($pos)) {
$this->Play();
return true;
}
return false;
}
private function prepareURIRequest($song, $prefix)
{
if ($song == null) return null;
$songUrl = $song['link'];
$songId = preg_replace('/(.+)\/oid\/(\d+)\/(.+)/i', '${2}', $songUrl);
$song = new song($songId);
$song->format();
$songItem = Upnp_Api::_itemSong($song, '');
$domDIDL = Upnp_Api::createDIDL($songItem);
$xmlDIDL = $domDIDL->saveXML();
return array(
'InstanceID' => 0,
$prefix . 'URI' => $songUrl,
$prefix . 'URIMetaData' => htmlentities($xmlDIDL)
);
}
private function CallAsyncURL($url) {
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_FRESH_CONNECT, true );
curl_setopt( $ch, CURLOPT_HEADER, false );
curl_exec( $ch );
curl_close( $ch );
}
/**
* play
* play the current song
*/
public function Play()
{
//!!$this->Stop();
$this->SetIntState(1);
$currentSongArgs = $this->prepareURIRequest($this->Playlist()->CurrentItem(), "Current");
$response = $this->Device()->sendRequestToDevice('SetAVTransportURI', $currentSongArgs, 'AVTransport');
$args = array( 'InstanceID' => 0, 'Speed' => 1);
$response = $this->Device()->sendRequestToDevice('Play', $args, 'AVTransport');
//!! UPNP subscription work not for all renderers, and works strange
//!! so now is not used
//$sid = $this->Device()->Subscribe();
//$_SESSION['upnp_SID'] = $sid;
// launch special page in background for periodically check play status
$url = AmpConfig::get('local_web_path') . "/upnp/playstatus.php";
$this->CallAsyncURL($url);
return true;
}
/**
* Stop
* stops the current song amazing!
*/
public function Stop()
{
$this->SetIntState(0);
$response = $this->Device()->instanceOnly('Stop');
//!! UPNP subscription work not for all renderers, and works strange
//!! so now is not used
//$sid = $_SESSION['upnp_SID'];
//$_SESSION['upnp_SID'] = "";
//$this->Device()->UnSubscribe($sid);
return true;
}
/**
* pause
* toggle pause mode on current song
*/
public function Pause()
{
$state = $this->GetState();
debug_event('upnpPlayer', 'Pause. prev state = ' . $state, 5);
if ($state == 'PLAYING') {
$response = $this->Device()->instanceOnly('Pause');
}
else {
$args = array( 'InstanceID' => 0, 'Speed' => 1);
$response = $this->Device()->sendRequestToDevice('Play', $args, 'AVTransport');
}
return true;
}
/**
* Repeat
* This toggles the repeat state
*/
public function Repeat($value)
{
//!! TODO not implemented yet
return true;
}
/**
* Random
* this toggles the random state
*/
public function Random($value)
{
//!! TODO not implemented yet
return true;
}
/**
*
*
*/
public function FullState()
{
//!! TODO not implemented yet
return "";
}
/**
* VolumeUp
* increases the volume
*/
public function VolumeUp()
{
$volume = $this->GetVolume() + 2;
return $this->SetVolume($volume);
}
/**
* VolumeDown
* decreases the volume
*/
public function VolumeDown()
{
$volume = $this->GetVolume() - 2;
return $this->SetVolume($volume);
}
/**
* SetVolume
*/
public function SetVolume($value)
{
$desiredVolume = Max(0, Min(100, $value));
$instanceId = 0;
$channel = 'Master';
$response = $this->Device()->sendRequestToDevice( 'SetVolume', array(
'InstanceID' => $instanceId,
'Channel' => $channel,
'DesiredVolume' => $desiredVolume
));
return true;
}
/**
* GetVolume
*/
public function GetVolume()
{
$instanceId = 0;
$channel = 'Master';
$response = $this->Device()->sendRequestToDevice( 'GetVolume', array(
'InstanceID' => $instanceId,
'Channel' => $channel
));
$responseXML = simplexml_load_string($response);
list($volume) = ($responseXML->xpath('//CurrentVolume'));
debug_event('upnpPlayer', 'GetVolume:' . $volume, 5);
return $volume;
}
private function SetIntState($state)
{
$this->_intState = $state;
$sid = 'upnp_ply_' . $this->_description_url;
$data = serialize($this->_intState);
if (! Session::exists('api', $sid))
Session::create(array('type' => 'api', 'sid' => $sid, 'value' => $data ));
else
Session::write($sid, $data);
debug_event('upnpPlayer', 'SetIntState:' . $this->_intState, 5);
}
private function ReadIndState()
{
$sid = 'upnp_ply_' . $this->_description_url;
$data = Session::read($sid);
$this->_intState = unserialize($data);
debug_event('upnpPlayer', 'ReadIndState:' . $this->_intState, 5);
}
} // End UPnPPlayer Class
?>

View file

@ -0,0 +1,141 @@
<?php
/**
*
* LICENSE: GNU General Public License, version 2 (GPLv2)
* Copyright 2001 - 2015 Ampache.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License v2
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
/**
* UPnPPlaylist Class
*/
class UPnPPlaylist
{
private $_deviceGUID = "";
private $_songs;
private $_current = 0;
/*
* Playlist is its own for each UPnP device
*/
public function UPnPPlaylist($deviceGUID)
{
$this->_deviceGUID = $deviceGUID;
$this->PlayListRead();
if (! is_array($this->_songs))
$this->Clear();
}
public function Add($name, $link)
{
$this->_songs[] = array('name' => $name, 'link' => $link);
$this->PlayListSave();
}
public function RemoveTrack($track)
{
unset($this->_songs[$track - 1]);
$this->PlayListSave();
}
public function Clear()
{
$this->_songs = array();
$this->_current = 0;
$this->PlayListSave();
}
public function AllItems()
{
return $this->_songs;
}
public function CurrentItem()
{
$item = $this->_songs[$this->_current];
return $item;
}
public function CurrentPos()
{
return $this->_current;
}
public function Next()
{
if ($this->_current < count($this->_songs) - 1) {
$this->_current++;
$this->PlayListSave();
return true;
}
return false;
}
public function NextItem()
{
if ($this->_current < count($this->_songs) - 1) {
$nxt = $this->_current + 1;
return $this->_songs[$nxt];
}
return null;
}
public function Prev()
{
if ($this->_current > 0) {
$this->_current--;
$this->PlayListSave();
return true;
}
return false;
}
public function Skip($pos)
{
// note that pos is started from 1 not from zero
if (($pos >= 1) && ($pos <= count($this->_songs))) {
$this->_current = $pos - 1;
$this->PlayListSave();
return true;
}
return false;
}
private function PlayListRead()
{
$sid = 'upnp_pls_' . $this->_deviceGUID;
$pls_data = unserialize(Session::read($sid));
$this->_songs = $pls_data['upnp_playlist'];
$this->_current = $pls_data['upnp_current'];
}
private function PlayListSave()
{
$sid = 'upnp_pls_' . $this->_deviceGUID;
$pls_data = serialize(array(
'upnp_playlist' => $this->_songs,
'upnp_current' => $this->_current
));
if (! Session::exists('api', $sid))
Session::create(array('type' => 'api', 'sid' => $sid, 'value' => $pls_data ));
else
Session::write($sid, $pls_data);
}
}
?>

View file

@ -18,7 +18,7 @@ $rootMediaItems[] = Upnp_Api::_videoMetadata('');
$requestRaw = file_get_contents('php://input');
if ($requestRaw != '') {
$upnpRequest = Upnp_Api::parseUPnPRequest($requestRaw);
debug_event('upnp', 'Request: ' . $requestRaw, '5');
//!!debug_event('upnp', 'Request: ' . $requestRaw, '5');
} else {
echo 'Error: no UPnP request.';
debug_event('upnp', 'No request', '5');
@ -100,5 +100,5 @@ $rootMediaItems[] = Upnp_Api::_videoMetadata('');
$soapXML = $domSOAP->saveXML();
echo $soapXML;
debug_event('upnp', 'Response: ' . $soapXML, '5');
//!!debug_event('upnp', 'Response: ' . $soapXML, '5');
?>

140
upnp/find.php Normal file
View file

@ -0,0 +1,140 @@
<?php
error_reporting( E_ALL );
ini_set('display_errors', 1);
class UPnPFind
{
/**
* Find devices by UPnP multicast message and stores them to cache
*
* @return array Parsed device list
*/
public static function findDevices()
{
$discover = self::discover(10);
return($discover); //!!
/*
$devices = array();
flush();
foreach ($discover as $response) {
$device = new Device();
if ($device->initByDiscoveryReponse($response)) {
$device->saveToCache();
try {
$client = $device->getClient('ConnectionManager');
$protocolInfo = $client->call('GetProtocolInfo');
$sink = $protocolInfo['Sink'];
$tmp = explode(',', $sink);
$protocols = array();
foreach ($tmp as $protocol) {
$t = explode(':', $protocol);
if ($t[0] == 'http-get') {
$protocols[] = $t[2];
}
}
} catch (UPnPException $upnpe) {
$protocols = array();
}
$device->protocolInfo = $protocols;
$cache[$device->getId()] = array(
'name' => $device->getName(),
'services' => $device->getServices(),
'icons' => $device->getIcons(),
'protocols' => $device->getProtocolInfo()
);
}
}
return $cache;
*/
}
/**
* Performs a standardized UPnP multicast request to 239.255.255.250:1900
* and listens $timeout seconds for responses
*
* Thanks to artheus (https://github.com/artheus/PHP-UPnP/blob/master/phpupnp.class.php)
*
* @param int $timeout Timeout to wait for responses
*
* @return array Response
*/
private static function discover($timeout = 2)
{
$msg = 'M-SEARCH * HTTP/1.1' . "\r\n";
$msg .= 'HOST: 239.255.255.250:1900' . "\r\n";
$msg .= 'MAN: "ssdp:discover"' . "\r\n";
$msg .= "MX: 3\r\n";
$msg .= "ST: upnp:rootdevice\r\n";
$msg .= '' . "\r\n";
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_set_option($socket, SOL_SOCKET, SO_BROADCAST, 1);
socket_sendto($socket, $msg, strlen($msg), 0, '239.255.255.250', 1900);
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $timeout, 'usec' => 0));
$response = array();
do {
$buf = null;
@socket_recvfrom($socket, $buf, 1024, MSG_WAITALL, $from, $port);
if (!is_null($buf))
$response[] = self::discoveryReponse2Array($buf);
} while (!is_null($buf));
//socket_close($socket);
return $response;
}
/**
* Transforms discovery response string to key/value array
*
* @param string $res discovery response
*
* @return \stdObj
*/
private static function discoveryReponse2Array($res)
{
$result = array();
$lines = explode("\n", trim($res));
if (trim($lines[0]) == 'HTTP/1.1 200 OK') {
array_shift($lines);
}
foreach ($lines as $line) {
$tmp = explode(':', trim($line));
$key = strtoupper(array_shift($tmp));
$value = (count($tmp) > 0 ? trim(join(':', $tmp)) : null);
$result[$key] = $value;
}
return (Object)$result;
}
}
$devices = UPnPFind::findDevices();
?>
<pre>
<?php print_r($devices); ?>
</pre>

View file

@ -8,6 +8,7 @@ if (!AmpConfig::get('upnp_backend')) {
}
if (($_GET['btnSend']) || ($_GET['btnSendAuto'])) {
$msIP = 1;
Upnp_Api::sddpSend($msIP);
}
?>

50
upnp/playstatus.php Normal file
View file

@ -0,0 +1,50 @@
<?php
// Send response to client to close connection
header('Connection: Close');
set_time_limit(0);
$path = dirname(__FILE__);
$prefix = realpath($path . '/../');
require_once $prefix . '/lib/init.php';
require_once $prefix . '/modules/localplay/upnp.controller.php';
require_once $prefix . '/modules/upnp/upnpplayer.class.php';
if (!AmpConfig::get('upnp_backend')) {
die("UPnP backend disabled..");
}
// get current UPnP player instance
$controller = new AmpacheUPnP();
$instance = $controller->get_instance();
echo "UPnP instance = " . $instance['name'] . "\n";
$deviceDescr = $instance['url'];
//!!echo "UPnP device = " . $deviceDescr . "\n";
$player = new UPnPPlayer("background controller", $deviceDescr);
//!!echo "Current playlist: \n" . print_r($player->GetPlaylistItems(), true);
//!!echo "Current item: \n" . print_r($player->GetCurrentItem(), true);
// periodically (every second) checking state of renderer, until it is STOPPED
$played = false;
while (($state = $player->GetState()) == "PLAYING") {
$played = true;
echo ".";
sleep(1);
}
echo "STATE = " . $state . "\n";
// If the song was played and then finished, start to play next song in list.
// Do not start anything if playback was stopped from beginning
if ($played)
{
echo "UPnP play next" . "\n";
if ($player->Next(false))
echo "Next song started" . "\n";
else
echo "Next song FAILED!" . "\n";
}
?>

4
upnp/readme.txt Normal file
View file

@ -0,0 +1,4 @@
playstatus.php - ...
find.php - ...