type = $type; $this->media = $media_ids; $this->user_id = $GLOBALS['user']->id; if (!is_array($this->media)) { settype($this->media,'array'); } } // Constructor /** * start *runs this and depending on the type passed it will *call the correct function */ public function start() { if (!count($this->media) AND !count($this->urls)) { debug_event('stream','Error: No Songs Passed on ' . $this->type . ' stream','2'); return false; } // We're starting insert the session into session_stream if (!self::get_session()) { debug_event('stream','Session Insertion failure, aborting','3'); return false; } $methods = get_class_methods('Stream'); $create_function = "create_" . $this->type; // If in the class, call it if (in_array($create_function,$methods)) { $this->{$create_function}(); } // Assume M3u incase they've pooched the type else { $this->create_m3u(); } } // start /** * add_urls * Add an array of urls, it may be a single one who knows, this * is used for things that aren't coming from media objects */ public function add_urls($urls=array()) { if (!is_array($urls)) { return false; } $this->urls = array_merge($urls,$this->urls); } // manual_url_add /** * get_session * This returns the current stream session */ public static function get_session() { if (!self::$session_inserted) { self::insert_session(self::$session); } return self::$session; } // get_session /** * set_session * This overrides the normal session value, without adding * an additional session into the database, should be called * with care */ public static function set_session($sid) { self::$session_inserted = true; self::$session=$sid; } // set_session /** * insert_session * This inserts a row into the session_stream table */ public static function insert_session($sid='',$uid='') { $sid = $sid ? Dba::escape($sid) : Dba::escape(self::$session); $uid = $uid ? Dba::escape($uid) : Dba::escape($GLOBALS['user']->id); $expire = time() + Config::get('stream_length'); $sql = "INSERT INTO `session_stream` (`id`,`expire`,`user`) " . "VALUES('$sid','$expire','$uid')"; $db_results = Dba::query($sql); if (!$db_results) { return false; } self::$session_inserted = true; return true; } // insert_session /** * session_exists * This checks to see if the passed stream session exists and is valid */ public static function session_exists($sid) { $sid = Dba::escape($sid); $time = time(); $sql = "SELECT * FROM `session_stream` WHERE `id`='$sid' AND `expire` > '$time'"; $db_results = Dba::query($sql); if ($row = Dba::fetch_assoc($db_results)) { return true; } return false; } // session_exists /** * gc_session * This function performes the garbage collection stuff, run on extend and on now playing refresh * There is an array of agents that we will never GC because of their nature, MPD being the best example */ public static function gc_session($ip='',$agent='',$uid='',$sid='') { $append_array = array('MPD'); $time = time(); $sql = "DELETE FROM `session_stream` WHERE `expire` < '$time'"; $db_results = Dba::query($sql); foreach ($append_array as $append_agent) { if (strstr(strtoupper($agent),$append_agent)) { // We're done here jump ship! return true; } } // end foreach // We need all of this to run this query if ($ip AND $agent AND $uid AND $sid) { $sql = "DELETE FROM `session_stream` WHERE `ip`='$ip' AND `agent`='$agent' AND `user`='$uid' AND `id` != '$sid'"; $db_results = Dba::query($sql); } } // gc_session /** * extend_session * This takes the passed sid and does a replace into also setting the user * agent and IP also do a little GC in this function */ public static function extend_session($sid,$uid) { $expire = time() + Config::get('stream_length'); $sid = Dba::escape($sid); $agent = Dba::escape($_SERVER['HTTP_USER_AGENT']); $ip = Dba::escape(inet_pton($_SERVER['REMOTE_ADDR'])); $uid = Dba::escape($uid); $sql = "UPDATE `session_stream` SET `expire`='$expire', `agent`='$agent', `ip`='$ip' " . "WHERE `id`='$sid'"; $db_results = Dba::query($sql); self::gc_session($ip,$agent,$uid,$sid); return true; } // extend_session /** * create_simplem3u * this creates a simple m3u without any of the extended information */ public function create_simple_m3u() { header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.m3u"); header("Content-Type: audio/x-mpegurl;"); // Flip for the poping! asort($this->urls); /* Foreach songs */ foreach ($this->media as $element) { $type = array_shift($element); echo call_user_func(array($type,'play_url'),array_shift($element)) . "\n"; } // end foreach /* Foreach the additional URLs */ foreach ($this->urls as $url) { echo "$url\n"; } } // simple_m3u /** * create_m3u * creates an m3u file, this includes the EXTINFO and as such can be * large with very long playlsits */ public function create_m3u() { // Send the client an m3u playlist header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.m3u"); header("Content-Type: audio/x-mpegurl;"); echo "#EXTM3U\n"; // Foreach the songs in this stream object foreach ($this->media as $element) { $type = array_shift($element); $media = new $type(array_shift($element)); $media->format(); switch ($type) { case 'song': echo "#EXTINF:$media->time," . $media->f_artist_full . " - " . $media->title . "\n"; break; case 'video': echo "#EXTINF: Video - $media->title\n"; break; case 'radio': echo "#EXTINF: Radio - $media->name [$media->frequency] ($media->site_url)\n"; break; case 'random': echo "#EXTINF:Random URL\n"; break; default: echo "#EXTINF:URL-Add\n"; break; } echo call_user_func(array($type,'play_url'),$media->id) . "\n"; } // end foreach /* Foreach URLS */ foreach ($this->urls as $url) { echo "#EXTINF: URL-Add\n"; echo $url . "\n"; } } // create_m3u /** * create_pls * This creates a new pls file from an array of songs and * urls, exciting I know */ public function create_pls() { /* Count entries */ $total_entries = count($this->media) + count($this->urls); // Send the client a pls playlist header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.pls"); header("Content-Type: audio/x-scpls;"); echo "[Playlist]\n"; echo "NumberOfEntries=$total_entries\n"; foreach ($this->media as $element) { $i++; $type = array_shift($element); $media = new $type(array_shift($element)); $media->format(); switch ($type) { case 'song': $name = $media->f_artist_full . " - " . $media->title . "." . $media->type; $length = $media->time; break; default: $name = 'URL-Add'; $length='-1'; break; } $url = call_user_func(array($type,'play_url'),$media->id); echo "File" . $i . "=$url\n"; echo "Title" . $i . "=$name\n"; echo "Length" . $i . "=$length\n"; } // end foreach songs /* Foreach Additional URLs */ foreach ($this->urls as $url) { $i++; echo "File" . $i ."=$url\n"; echo "Title". $i . "=AddedURL\n"; echo "Length" . $i . "=-1\n"; } // end foreach urls echo "Version=2\n"; } // create_pls /** * create_asx * creates an ASX playlist (Thx Samir Kuthiala) This should really only be used * if all of the content is ASF files. */ public function create_asx() { header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.asx"); header("Content-Type: video/x-ms-wmv;"); echo "\n"; echo "Ampache ASX Playlist"; foreach ($this->media as $element) { $type = array_shift($element); $media = new $type(array_shift($element)); $media->format(); switch ($type) { case 'song': $name = $media->f_album_full . " - " . $media->title . "." . $media->type; $author = $media->f_artist_full; break; default: $author = 'Ampache'; $name = 'URL-Add'; break; } // end switch $url = call_user_func(array($type,'play_url'),$media->id); echo "\n"; echo "$name\n"; echo "$author\n"; echo "\t\t".$media->year."\n"; echo "\t\ttime."\" />\n"; echo "\t\tf_album_full."\" />\n"; echo "\t\tget_genre_name()."\" />\n"; echo "\t\tf_artist_full."\" />\n"; echo "\t\t\n"; echo "\n"; echo "\n"; } // end foreach /* Foreach urls */ foreach ($this->urls as $url) { echo "\n"; echo "AddURL\n"; echo "AddURL\n"; echo "\n"; echo "\n"; } // end foreach echo "\n"; } // create_asx /** * create_xspf * creates an XSPF playlist (Thx PB1DFT) */ public function create_xspf() { $flash_hack = ''; if (isset($_REQUEST['flash_hack'])) { if (!Config::get('require_session')) { $flash_hack = '&sid=' . session_id(); } } // Itterate through the songs foreach ($this->media as $element) { $type = array_shift($element); $media = new $type(array_shift($element)); $media->format(); $xml = array(); switch ($type) { default: case 'song': $xml['track']['title'] = $media->title; $xml['track']['creator'] = $media->f_artist_full; $xml['track']['info'] = Config::get('web_path') . "/albums.php?action=show&album=" . $media->album; $xml['track']['image'] = Config::get('web_path') . "/image.php?id=" . $media->album . "&thumb=3&sid=" . session_id(); $xml['track']['album'] = $media->f_album_full; $length = $media->time; break; } // type $xml['track']['location'] = call_user_func(array($type,'play_url'),$media->id) . $flash_hack; $xml['track']['identifier'] = $xml['track']['location']; $xml['track']['duration'] = $length * 1000; $result .= xmlData::keyed_array($xml,1); } // end foreach xmlData::set_type('xspf'); header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.xspf"); header("Content-Type: application/xspf+xml; charset=utf-8"); echo xmlData::header(); echo $result; echo xmlData::footer(); } // create_xspf /** * create_xspf_player * due to the fact that this is an integrated player (flash) we actually * have to do a little 'cheating' to make this work, we are going to take * advantage of tmp_playlists to do all of this hotness */ public function create_xspf_player() { /* Build the extra info we need to have it pass */ $play_info = "?action=show&tmpplaylist_id=" . $GLOBALS['user']->playlist->id; // start ugly evil javascript code //FIXME: This needs to go in a template, here for now though //FIXME: This preference doesn't even exists, we'll eventually //FIXME: just make it the default if (Config::get('embed_xspf') == 1 ){ header("Location: ".Config::get('web_path')."/index.php?xspf&play_info=".$GLOBALS['user']->playlist->id); } else { echo "\n"; echo "" . Config::get('site_title') . "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; echo "\n"; } } // create_xspf_player /** * create_localplay * This calls the Localplay API and attempts to * add, and then start playback */ public function create_localplay() { // First figure out what their current one is and create the object $localplay = new Localplay(Config::get('localplay_controller')); $localplay->connect(); foreach ($this->media as $element) { $type = array_shift($element); switch ($type) { case 'video': // Add check for video support case 'song': case 'radio': case 'random': $media = new $type(array_shift($element)); break; default: $media = array_shift($element); break; } // switch on types $localplay->add($media); } // foreach object /** * Add urls after the fact */ foreach ($this->urls as $url) { $localplay->add($url); } $localplay->play(); } // create_localplay /** * create_democratic * This 'votes' on the songs it inserts them into * a tmp_playlist with user of -1 (System) */ public function create_democratic() { $democratic = Democratic::get_current_playlist(); $democratic->set_parent(); $democratic->vote($this->media); } // create_democratic /** * create_download * This prompts for a download of the song, only a single * element can by in song_ids */ private function create_download() { // There should only be one here... foreach ($this->media as $element) { $type = array_shift($element); $media = new $type(array_shift($element)); $url = call_user_func(array($type,'play_url'),$media->id); // Append the fact we are downloading $url .= '&action=download'; // Header redirect baby! header("Location: $url"); exit; } } //create_download /** * create_ram *this functions creates a RAM file for use by Real Player */ public function create_ram() { header("Cache-control: public"); header("Content-Disposition: filename=ampache_playlist.ram"); header("Content-Type: audio/x-pn-realaudio ram;"); foreach ($this->media as $element) { $type = array_shift($element); echo $url = call_user_func(array($type,'play_url'),array_shift($element)) . "\n"; } // foreach songs } // create_ram /** * start_downsample * This is a rather complext function that starts the downsampling of a song and returns the * opened file handled a reference to the song object is passed so that the changes we make * in here affect the external object, References++ */ public static function start_downsample(&$song,$now_playing_id=0,$song_name=0,$start=0) { /* Check to see if bitrates are set if so let's go ahead and optomize! */ $max_bitrate = Config::get('max_bit_rate'); $min_bitrate = Config::get('min_bit_rate'); $time = time(); $user_sample_rate = Config::get('sample_rate'); $browser = new Browser(); if (!$song_name) { $song_name = $song->f_artist_full . " - " . $song->title . "." . $song->type; } if ($max_bitrate > 1 AND $min_bitrate < $max_bitrate AND $min_bitrate > 0) { $last_seen_time = $time - 1200; //20 min. $sql = "SELECT COUNT(*) FROM now_playing, user_preference, preference " . "WHERE preference.name = 'play_type' AND user_preference.preference = preference.id " . "AND now_playing.user = user_preference.user AND user_preference.value='downsample'"; $db_results = Dba::query($sql); $results = Dba::fetch_row($db_results); // Current number of active streams (current is already in now playing, worst case make it 1) $active_streams = intval($results[0]); if (!$active_streams) { $active_streams = '1'; } /* If only one user, they'll get all available. Otherwise split up equally. */ $sample_rate = floor($max_bitrate/$active_streams); /* If min_bitrate is set, then we'll exit if the bandwidth would need to be split up smaller than the min. */ if ($min_bitrate > 1 AND ($max_bitrate/$active_streams) < $min_bitrate) { /* Log the failure */ debug_event('downsample',"Error: Max bandwidith already allocated. $active_streams Active Streams",'2'); echo "Maximum bandwidth already allocated. Try again later."; exit(); } else { $sample_rate = floor($max_bitrate/$active_streams); } // end else // Never go over the users sample rate if ($sample_rate > $user_sample_rate) { $sample_rate = $user_sample_rate; } debug_event('downsample',"Downsampled: $active_streams current active streams, downsampling to $sample_rate",'2'); } // end if we've got bitrates else { $sample_rate = $user_sample_rate; } /* Validate the bitrate */ $sample_rate = self::validate_bitrate($sample_rate); /* Never Upsample a song */ if (($sample_rate*1000) > $song->bitrate) { $sample_rate = self::validate_bitrate($song->bitrate/1000); $sample_ratio = '1'; } else { /* Set the Sample Ratio */ $sample_ratio = $sample_rate/($song->bitrate/1000); } // Set the new size for the song $song->size = floor($sample_ratio*$song->size); /* Get Offset */ $offset = ( $start*$song->time )/( $song->size ); $offsetmm = floor($offset/60); $offsetss = floor($offset-$offsetmm*60); $offset = sprintf("%02d.%02d",$offsetmm,$offsetss); /* Get EOF */ $eofmm = floor($song->time/60); $eofss = floor($song->time-$eofmm*60); $eof = sprintf("%02d.%02d",$eofmm,$eofss); $song_file = escapeshellarg($song->file); /* Replace Variables */ $downsample_command = Config::get($song->stream_cmd()); $downsample_command = str_replace("%FILE%",$song_file,$downsample_command,$file_exists); $downsample_command = str_replace("%OFFSET%",$offset,$downsample_command,$offset_exists); $downsample_command = str_replace("%EOF%",$eof,$downsample_command,$eof_exists); $downsample_command = str_replace("%SAMPLE%",$sample_rate,$downsample_command,$sample_exists); if (!$file_exists || !$offset_exists || !$eof_exists || !$sample_exists) { debug_event('downsample','Error: Downsample command missing a varaible values are File:' . $file_exists . ' Offset:' . $offset_exists . ' Eof:' . $eof_exists . ' Sample:' . $sample_exists,'1'); } // If we are debugging log this event $message = "Start Downsample: $downsample_command"; debug_event('downsample',$message,'3'); $fp = popen($downsample_command, 'rb'); // Return our new handle return ($fp); } // start_downsample /** * validate_bitrate * this function takes a bitrate and returns a valid one */ public static function validate_bitrate($bitrate) { /* Round to standard bitrates */ $sample_rate = 16*(floor($bitrate/16)); return $sample_rate; } // validate_bitrate /** * gc_now_playing * This will garbage collect the now playing data, * this is done on every play start */ public static function gc_now_playing() { // Remove any now playing entries for session_streams that have been GC'd $sql = "DELETE FROM `now_playing` USING `now_playing` " . "LEFT JOIN `session_stream` ON `session_stream`.`id`=`now_playing`.`id` " . "WHERE `session_stream`.`id` IS NULL OR `now_playing`.`expire` < '" . time() . "'"; $db_results = Dba::query($sql); } // gc_now_playing /** * insert_now_playing * This will insert the now playing data * This fucntion is used by the /play/index.php song * primarily, but could be used by other people */ public static function insert_now_playing($oid,$uid,$length,$sid,$type) { $time = time()+$length; $session_id = Dba::escape($sid); $object_type = 'song'; // Do a replace into ensuring that this client always only has a single row $sql = "REPLACE INTO `now_playing` (`id`,`object_id`,`object_type`, `user`, `expire`)" . " VALUES ('$session_id','$oid','$object_type', '$uid', '$time')"; $db_result = Dba::write($sql); } // insert_now_playing /** * clear_now_playing * There really isn't anywhere else for this function, shouldn't have deleted it in the first * place */ public static function clear_now_playing() { $sql = "TRUNCATE `now_playing`"; $db_results = Dba::query($sql); return true; } // clear_now_playing /** * get_now_playing * This returns the now playing information */ public static function get_now_playing($filter=NULL) { $sql = "SELECT `session_stream`.`agent`,`now_playing`.* " . "FROM `now_playing` " . "LEFT JOIN `session_stream` ON `session_stream`.`id`=`now_playing`.`id` " . "ORDER BY `now_playing`.`expire` DESC"; $db_results = Dba::read($sql); $results = array(); while ($row = Dba::fetch_assoc($db_results)) { $type = $row['object_type']; $media = new $type($row['object_id']); $media->format(); $client = new User($row['user']); $results[] = array('media'=>$media,'client'=>$client,'agent'=>$row['agent'],'expire'=>$row['expire']); } // end while return $results; } // get_now_playing /** * check_lock_media * This checks to see if the media is already being played, if it is then it returns false * else return true */ public static function check_lock_media($media_id,$type) { $media_id = Dba::escape($media_id); $type = Dba::escape($type); $sql = "SELECT `object_id` FROM `now_playing` WHERE `object_id`='$media_id' AND `object_type`='$type'"; $db_results = Dba::read($sql); if (Dba::num_rows($db_results)) { debug_event('Stream','Unable to play media currently locked by another user','3'); return false; } return true; } // check_lock_media /** * auto_init * This is called on class load it sets the session */ public static function _auto_init() { // Generate the session ID self::$session = md5(uniqid(rand(), true)); } // auto_init /** * run_playlist_method * This takes care of the different types of 'playlist methods' the reason this is here * is because it deals with streaming rather then playlist mojo. If something needs to happen * this will echo the javascript required to cause a reload of the iframe. */ public static function run_playlist_method() { // If this wasn't ajax included run away if (AJAX_INCLUDE != '1') { return false; } // If we're doin the flash magic then run away as well if (Config::get('play_type') == 'xspf_player') { return false; } switch (Config::get('playlist_method')) { default: case 'clear': case 'default': return true; break; case 'send': $_SESSION['iframe']['target'] = Config::get('web_path') . '/stream.php?action=basket'; break; case 'send_clear': $_SESSION['iframe']['target'] = Config::get('web_path') . '/stream.php?action=basket&playlist_method=clear'; break; } // end switch on method // Load our javascript echo ""; } // run_playlist_method /** * get_base_url * This returns the base requirements for a stream URL this does not include anything after the index.php?sid=???? */ public static function get_base_url() { if (Config::get('require_session')) { $session_string = 'sid=' . Stream::get_session() . '&'; } $web_path = Config::get('web_path'); if (Config::get('force_http_play') OR !empty(self::$force_http)) { $web_path = str_replace("https://", "http://",$web_path); } if (Config::get('http_port') != '80') { if (preg_match("/:(\d+)/",$web_path,$matches)) { $web_path = str_replace(':' . $matches['1'],':' . Config::get('http_port'),$web_path); } else { $web_path = str_replace($_SERVER['HTTP_HOST'],$_SERVER['HTTP_HOST'] . ':' . Config::get('http_port'),$web_path); } } $url = $web_path . "/play/index.php?$session_string"; return $url; } // get_base_url } //end of stream class ?>