From f7c1e57cf021f1d6f00b209f1640bc24c6303391 Mon Sep 17 00:00:00 2001 From: Paul Arthur Date: Fri, 8 Apr 2011 23:15:33 -0400 Subject: Transcoding/streaming cleanup. Derive our new filesize from the length, not the previous bitrate and size. Allow higher bitrates than the source when they're different formats. Return HTTP error codes when an error occurs. Minor cleanup. --- lib/class/song.class.php | 34 ++++++------ lib/class/stream.class.php | 114 ++++++++++++++++++++++------------------ lib/ui.lib.php | 3 +- play/index.php | 128 +++++++++++++++++++++------------------------ 4 files changed, 143 insertions(+), 136 deletions(-) diff --git a/lib/class/song.class.php b/lib/class/song.class.php index b6899267..1d5329a0 100644 --- a/lib/class/song.class.php +++ b/lib/class/song.class.php @@ -63,7 +63,10 @@ class Song extends database_object implements media { /* Setting Variables */ public $_transcoded = false; + public $resampled = false; public $_fake = false; // If this is a 'construct_from_array' object + public $transcoded_from; + private $_transcode_cmd; /** * Constructor @@ -970,10 +973,20 @@ class Song extends database_object implements media { $conf_var = 'transcode_' . $this->type; $conf_type = 'transcode_' . $this->type . '_target'; + $conf_cmd = 'transcode_cmd_' . $this->type; if (Config::get($conf_var)) { $this->_transcoded = true; - debug_event('auto_transcode','Transcoding to ' . $this->type,'5'); + $this->_transcoded_from = $this->type; + $this->_transcode_cmd = Config::get($conf_cmd); + + $this->format_type(Config::get($conf_type)); + if ($this->type == $this->_transcoded_from) { + $this->_resampled = true; + } + + debug_event('transcode', 'Transcoding from ' . + $this->_transcoded_from . ' to ' . $this->type, 5); return false; } @@ -983,26 +996,17 @@ class Song extends database_object implements media { /** * stream_cmd + * * test if the song type streams natively and * if not returns a transcoding command from the config - * we can't use this->type because its been formated for the - * downsampling */ public function stream_cmd() { - // Find the target for this transcode - $conf_type = 'transcode_' . $this->type . '_target'; - $stream_cmd = 'transcode_cmd_' . $this->type; - $this->format_type(Config::get($conf_type)); - - if (Config::get($stream_cmd)) { - return $stream_cmd; + if ($this->native_stream()) { + return null; } - else { - debug_event('Downsample','Error: Transcode ' . $stream_cmd . ' for ' . $this->type . ' not found, using downsample','2'); - } - - return false; + + return $this->_transcode_cmd; } // end stream_cmd diff --git a/lib/class/stream.class.php b/lib/class/stream.class.php index f9ba484f..c64b3a2e 100644 --- a/lib/class/stream.class.php +++ b/lib/class/stream.class.php @@ -602,14 +602,17 @@ class Stream { } // 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++ + * start_transcode + * + * This is a rather complex function that starts the transcoding or + * resampling of a song and returns the opened file handle. 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) { + public static function start_transcode(&$song, $song_name = 0, $start = 0) { - /* Check to see if bitrates are set if so let's go ahead and optomize! */ + // Check to see if bitrates are set. + // If so let's go ahead and optimize! $max_bitrate = Config::get('max_bit_rate'); $min_bitrate = Config::get('min_bit_rate'); $time = time(); @@ -629,33 +632,33 @@ class Stream { $db_results = Dba::read($sql); $results = Dba::fetch_row($db_results); - // Current number of active streams (current is already in now playing, worst case make it 1) + // 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'; } + debug_event('transcode', "Active streams: $active_streams", 5); - /* If only one user, they'll get all available. Otherwise split up equally. */ - $sample_rate = floor($max_bitrate/$active_streams); + // 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."; + // If min_bitrate is set, then we'll exit if the + // bandwidth would need to be lower. + if ($min_bitrate > 1 AND ($max_bitrate / $active_streams) < $min_bitrate) { + debug_event('transcode', "Max bandwidth already allocated. Active streams: $active_streams", 2); + header('HTTP/1.1 503 Service Temporarily Unavailable'); exit(); - } else { - $sample_rate = floor($max_bitrate/$active_streams); + $sample_rate = floor($max_bitrate / $active_streams); } // end else - // Never go over the users sample rate + // Never go over the user's 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'); + debug_event('transcode', "Downsampling to $sample_rate", 5); } // end if we've got bitrates - else { $sample_rate = $user_sample_rate; } @@ -663,57 +666,64 @@ class Stream { /* 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); + // Never upsample a song + if ($song->resampled && ($sample_rate * 1000) > $song->bitrate) { + $sample_rate = self::validate_bitrate($song->bitrate / 1000); } - // Set the new size for the song - $song->size = floor($sample_ratio*$song->size); + // Set the new size for the song (in bytes) + $song->size = floor($sample_rate * $song->time * 125); /* Get Offset */ - $offset = ( $start*$song->time )/( $song->size ); - $offsetmm = floor($offset/60); - $offsetss = floor($offset-$offsetmm*60); + $offset = ($start * $song->time) / $song->size; + $offsetmm = floor($offset / 60); + $offsetss = floor($offset - ($offsetmm * 60)); // If flac then format it slightly differently - if ($song->type == 'flac') { - $offset = sprintf("%02d:%02d",$offsetmm,$offsetss); + // HACK + if ($song->transcoded_from == 'flac') { + $offset = sprintf('%02d:%02d', $offsetmm, $offsetss); } else { - $offset = sprintf("%02d.%02d",$offsetmm,$offsetss); + $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); + $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); + $transcode_command = $song->stream_cmd(); + if ($transcode_command == null) { + debug_event('downsample', 'song->stream_cmd() returned null', 2); + return null; + } - if (!$file_exists || !$offset_exists || !$eof_exists || !$sample_exists) { - debug_event('downsample', 'Warning: Downsample command missing a variable; values are File:' . $file_exists . ' Offset:' . $offset_exists . ' Eof:' . $eof_exists . ' Sample:' . $sample_exists, '1'); + $string_map = array( + '%FILE%' => $song_file, + '%OFFSET%' => $offset, + '%OFFSET_MM%' => $offsetmm, + '%OFFSET_SS%' => $offsetss, + '%EOF%' => $eof, + '%EOF_MM%' => $eofmm, + '%EOF_SS%' => $eofss, + '%SAMPLE%' => $sample_rate + ); + + foreach ($string_map as $search => $replace) { + $transcode_command = str_replace($search, $replace, $transcode_command, $ret); + if (!$ret) { + debug_event('downsample', "$search not in downsample command", 5); + } } - // If we are debugging log this event - $message = "Start Downsample using CMD: $downsample_command"; - debug_event('downsample',$message,'3'); + debug_event('downsample', "Downsample command: $transcode_command", 3); - $fp = popen($downsample_command, 'rb'); + $fp = popen($transcode_command, 'rb'); // Return our new handle - return ($fp); + return $fp; } // start_downsample diff --git a/lib/ui.lib.php b/lib/ui.lib.php index c7d21741..ded371a1 100644 --- a/lib/ui.lib.php +++ b/lib/ui.lib.php @@ -103,10 +103,11 @@ if (!function_exists('ngettext')) { * access_denied * Throws an error if they try to do something that they aren't allowed to. */ -function access_denied() { +function access_denied($error = "Access Denied") { // Clear any crap we've got up top ob_end_clean(); + header("HTTP/1.1 403 $error"); require_once Config::get('prefix') . '/templates/show_denied.inc.php'; exit; diff --git a/play/index.php b/play/index.php index 8cf6d472..3e0945ba 100644 --- a/play/index.php +++ b/play/index.php @@ -54,8 +54,8 @@ $n = sscanf($_SERVER['HTTP_RANGE'], "bytes=%d-%d",$start,$end); /* First things first, if we don't have a uid/oid stop here */ if (empty($oid) && empty($demo_id) && empty($random)) { - debug_event('Play',"Error: No Object UID Specified, nothing to play",'2'); - echo "Error: No Object UID Specified, nothing to play"; + debug_event('play', 'No object UID specified, nothing to play', 2); + header('HTTP/1.1 400 Nothing To Play'); exit; } @@ -65,8 +65,8 @@ if (isset($xml_rpc) AND Config::get('xml_rpc') AND !isset($uid)) { } if (!isset($uid)) { - debug_event('Play','Error: No User specified','2'); - echo "Error: No User Specified"; + debug_event('play', 'No user specified', 2); + header('HTTP/1.1 400 No User Specified'); exit; } @@ -76,20 +76,20 @@ Preference::init(); /* If the user has been disabled (true value) */ if (make_bool($GLOBALS['user']->disabled)) { - debug_event('user_disabled',"Error $user->username is currently disabled, stream access denied",'3'); - echo "Error: User Disabled"; + debug_event('access_denied', "$user->username is currently disabled, stream access denied",'3'); + header('HTTP/1.1 403 User Disabled'); exit; } // If require session is set then we need to make sure we're legit if (Config::get('require_session')) { if (!Config::get('require_localnet_session') AND Access::check_network('network',$GLOBALS['user']->id,'5')) { - // Localnet defined IP and require localnot session has been turned off we let this one through - debug_event('LocalNet','Streaming Access Granted to Localnet defined IP ' . $_SERVER['REMOTE_ADDR'],'5'); + debug_event('play', 'Streaming access allowed for local network IP ' . $_SERVER['REMOTE_ADDR'],'5'); } elseif(!Stream::session_exists($sid)) { - debug_event('session_expired',"Streaming Access Denied: " . $GLOBALS['user']->username . "'s session has expired",'3'); - die(_("Session Expired: please log in again at") . " " . Config::get('web_path') . "/login.php"); + debug_event('access_denied', 'Streaming access denied: ' . $GLOBALS['user']->username . "'s session has expired", 3); + header('HTTP/1.1 403 Session Expired'); + exit; } // Now that we've confirmed the session is valid @@ -103,7 +103,7 @@ $GLOBALS['user']->update_last_seen(); /* If we are in demo mode.. die here */ if (Config::get('demo_mode') || (!Access::check('interface','25') AND !isset($xml_rpc))) { - debug_event('access_denied',"Streaming Access Denied:" .Config::get('demo_mode') . "is the value of demo_mode. Current user level is " . $GLOBALS['user']->access,'3'); + debug_event('access_denied', "Streaming Access Denied:" .Config::get('demo_mode') . "is the value of demo_mode. Current user level is " . $GLOBALS['user']->access,'3'); access_denied(); exit; } @@ -223,11 +223,12 @@ if (!$media->file OR !is_readable($media->file)) { if (is_object($tmp_playlist)) { $tmp_playlist->delete_track($oid); } - - debug_event('Play',"Error song $media->file ($media->title) does not have a valid filename specified",'2'); - echo "Error: Invalid Song Specified, file not found or file unreadable"; + // FIXME: why are these separate? // Remove the song votes if this is a democratic song if ($demo_id) { $democratic->delete_from_oid($oid,'song'); } + + debug_event('play', "Song $media->file ($media->title) does not have a valid filename specified", 2); + header('HTTP/1.1 404 Invalid song, file not found or file unreadable'); exit; } @@ -238,7 +239,7 @@ if(version_compare(PHP_VERSION, '5.3.0', '<=')) { } // don't abort the script if user skips this song because we need to update now_playing -ignore_user_abort(TRUE); +ignore_user_abort(true); // Format the song name $media_name = $media->f_artist_full . " - " . $media->title . "." . $media->type; @@ -295,38 +296,38 @@ header("Accept-Ranges: bytes" ); // Prevent the script from timing out set_time_limit(0); -/* We're about to start record this persons IP */ +// We're about to start. Record this user's IP. if (Config::get('track_user_ip')) { $GLOBALS['user']->insert_ip_history(); } -// If we've got downsample remote enabled if (Config::get('downsample_remote')) { - if (!Access::check_network('network',$GLOBALS['user']->id,'0')) { - debug_event('Downsample','Network Downsample ' . $_SERVER['REMOTE_ADDR'] . ' is not in Local definition','5'); - $not_local = true; + if (!Access::check_network('network', $GLOBALS['user']->id,'0')) { + debug_event('downsample', 'Address ' . $_SERVER['REMOTE_ADDR'] . ' is not in a network defined as local', 5); + $remote = true; } -} // if downsample remote is enabled +} // If they are downsampling, or if the song is not a native stream or it's non-local -if (((Config::get('transcode') == 'always' AND !$video) || !$media->native_stream() || - isset($not_local)) && Config::get('transcode') != 'never') { - debug_event('Downsample','Starting Downsample {Transcode:' . Config::get('transcode') . '} {Native Stream:' . $media->native_stream() .'} {Not Local:' . $not_local . '}','5'); - $fp = Stream::start_downsample($media,$lastid,$media_name,$start); +if (((Config::get('transcode') == 'always' AND !$video) || + !$media->native_stream() || + isset($remote)) && Config::get('transcode') != 'never') { + debug_event('downsample', + 'Decided to transcode. Transcode:' . Config::get('transcode') . + ' Native Stream: ' . ($media->native_stream() ? 'true' : 'false') . + ' Remote: ' . ($remote ? 'true' : 'false'), 5); + $fp = Stream::start_transcode($media, $media_name, $start); $media_name = $media->f_artist_full . " - " . $media->title . "." . $media->type; - // Note that this is downsampling - $downsampled_song = true; + $transcoded = true; } // end if downsampling else { - // Send file, possible at a byte offset $fp = fopen($media->file, 'rb'); +} - if (!is_resource($fp)) { - debug_event('Play',"Error: Unable to open $media->file for reading",'2'); - if ($demo_id) { $democratic->delete_from_oid($oid,'song'); } - cleanup_and_exit($lastid); - } -} // else not downsampling +if (!is_resource($fp)) { + debug_event('play', "Failed to open $media->file for streaming", 2); + exit(); +} // Put this song in the now_playing table only if it's a song for now... if (get_class($media) == 'Song') { @@ -334,7 +335,6 @@ if (get_class($media) == 'Song') { } if ($start > 0 || $end > 0 ) { - // Calculate stream size from byte range if(isset($end)) { $end = min($end,$media->size-1); @@ -344,71 +344,63 @@ if ($start > 0 || $end > 0 ) { $stream_size = $media->size - $start; } - debug_event('Play','Content-Range header recieved, skipping ahead ' . $start . ' bytes out of ' . $media->size,'5'); + debug_event('play', 'Content-Range header received, skipping ' . $start . ' bytes out of ' . $media->size, 5); $browser->downloadHeaders($media_name, $media->mime, false, $media->size); - if (!$downsampled_song) { - fseek( $fp, $start ); + if (!$transcoded) { + fseek($fp, $start); } $range = $start ."-". $end . "/" . $media->size; - header("HTTP/1.1 206 Partial Content"); + header('HTTP/1.1 206 Partial Content'); header("Content-Range: bytes $range"); - header("Content-Length: ".($stream_size)); + header("Content-Length: $stream_size"); } - -/* Last but not least pump em out */ else { - debug_event('Play','Starting stream of ' . $media->file . ' with size ' . $media->size,'5'); + debug_event('play','Starting stream of ' . $media->file . ' with size ' . $media->size, 5); header("Content-Length: $media->size"); $browser->downloadHeaders($media_name, $media->mime, false, $media->size); $stream_size = $media->size; } -/* Let's force them to actually play a portion of the song before - * we count it in the statistics - */ $bytes_streamed = 0; -$min_bytes_streamed = $media->size / 2; // Actually do the streaming do { - $read_size = min(2048,$stream_size-$bytes_streamed); - if ($read_size < 1) { break; } - $buf = fread($fp, $read_size); + $buf = fread($fp, 2048); print($buf); $bytes_streamed += strlen($buf); -} while (!feof($fp) && (connection_status() == 0) AND $bytes_streamed < $stream_size); +} while (!feof($fp) && (connection_status() == 0) && ($bytes_streamed < $stream_size)); -// Need to make sure enough bytes were sent. Some players (Windows Media Player) won't work if specified content length is not sent. +// Need to make sure enough bytes were sent. Some players (Windows Media Player) +// won't work if specified content length is not sent. if($bytes_streamed < $stream_size AND (connection_status() == 0)) { print(str_repeat(' ',$stream_size - $bytes_streamed)); } // Make sure that a good chunk of the song has been played -if ($bytes_streamed > $min_bytes_streamed AND get_class($media) == 'Song') { - debug_event('Play','Registering stats for ' . $media->title,'5'); - - $GLOBALS['user']->update_stats($media->id); - /* Set the Song as Played if it isn't already */ - $media->set_played(); +if ($bytes_streamed > $media->size / 2) { + // This check looks suspicious + if (get_class($media) == 'Song') { + debug_event('play', 'Registering stats for ' . $media->title, 5); + $GLOBALS['user']->update_stats($media->id); + $media->set_played(); + } -} // if enough bytes are streamed +} else { - debug_event('Play',$bytes_streamed .' of ' . $media->size . ' streamed, less than ' . $min_bytes_streamed . ' not collecting stats','5'); + debug_event('play', $bytes_streamed .' of ' . $media->size . ' streamed; not collecting stats', 5); } - -/* If this is a voting tmp playlist remove the entry, we do this regardless of play amount */ +// If this is a democratic playlist remove the entry. +// We do this regardless of play amount. if ($demo_id) { $democratic->delete_from_oid($oid,'song'); } -/* Clean up any open ends */ -if (Config::get('play_type') == 'downsample' || !$media->native_stream()) { - @pclose($fp); +if ($transcoded) { + pclose($fp); } else { - @fclose($fp); + fclose($fp); } -// Note that the stream has ended -debug_event('Play','Stream Ended at ' . $bytes_streamed . ' bytes out of ' . $media->size,'5'); +debug_event('play', 'Stream ended at ' . $bytes_streamed . ' bytes out of ' . $media->size, 5); ?> -- cgit