diff options
-rw-r--r-- | config/ampache.cfg.php.dist | 91 | ||||
-rwxr-xr-x | docs/CHANGELOG | 1 | ||||
-rw-r--r-- | lib/class/media.interface.php | 39 | ||||
-rw-r--r-- | lib/class/radio.class.php | 20 | ||||
-rw-r--r-- | lib/class/song.class.php | 133 | ||||
-rw-r--r-- | lib/class/stream.class.php | 55 | ||||
-rw-r--r-- | lib/class/video.class.php | 23 | ||||
-rw-r--r-- | lib/init.php | 2 | ||||
-rw-r--r-- | play/index.php | 69 |
9 files changed, 210 insertions, 223 deletions
diff --git a/config/ampache.cfg.php.dist b/config/ampache.cfg.php.dist index 4829b298..4d6434c8 100644 --- a/config/ampache.cfg.php.dist +++ b/config/ampache.cfg.php.dist @@ -7,7 +7,7 @@ ; if this config file is up to date ; this is compared against a value hard-coded ; into the init script -config_version = 11 +config_version = 12 ;################### ; Path Vars # @@ -515,43 +515,58 @@ refresh_limit = "60" ; These are commands used to transcode non-streaming ; formats to the target file type for streaming. ; This can be useful in re-encoding file types that don't stream -; very well, or if your player doesn't support some file types. -; This is also the string used when 'downsampling' is selected -; as some people have complained its not bloody obvious, any programs -; referenced in the downsample commands must be installed manually and in -; the web server path, and executable by the web server -; REQUIRED variables -; transcode_TYPE = true/false ## True to force transcode regardless of prefs -; transcode_TYPE_target = TARGET_FILE_TYPE -; transcode_cmd_TYPE = TRANSCODE_COMMAND -; %FILE% = filename -; %OFFSET% = offset -; %SAMPLE% = sample rate -; %EOF% = end of file in min.sec - -; List of filetypes to transcode -transcode_m4a = true -transcode_m4a_target = mp3 -;transcode_flac = true -transcode_flac_target = mp3 -;transcode_mp3 = false -transcode_mp3_target = mp3 -;transcode_ogg = false -transcode_ogg_target = mp3 - -; These are the commands that will be run to transcode the file -transcode_cmd_flac = "flac -dc %FILE% | lame -b %SAMPLE% -S - - " -transcode_cmd_m4a = "faad -f 2 -w %FILE% | lame -r -b %SAMPLE% -S - -" -transcode_cmd_mp3 = "mp3splt -qnf %FILE% %OFFSET% %EOF% -o - | lame --mp3input -q 3 -b %SAMPLE% -S - -" -transcode_cmd_ogg = "oggsplt -qn %FILE% %OFFSET% %EOF% -o - | oggdec -Q -o - - | lame -S -q 3 -b %SAMPLE% -S - -" - -; Alternative command works better for some people -;transcode_cmd_m4a = "alac %FILE% | lame -h -b %SAMPLE% -S - -" -;transcode_cmd_ogg = "mp3splt -qn %FILE% %OFFSET% %EOF% -o - | oggdec -Q -o - - | lame -S -q 3 -b %SAMPLE% -S - -" -;transcode_cmd_flac = "flac -dc %FILE% | lame -rb %SAMPLE% -S - -" - -; This line seems to work better for windows, switch if needed -;transcode_cmd_mp3 = "lame -q 3 -b %SAMPLE% -S %FILE% - -" +; very well, or if your player doesn't support some file types. +; +; 'Downsampling' will also use these commands. +; +; To state the bleeding obvious, any programs referenced in the downsample +; commands must be installed, in the web server's search path (or referenced +; by their full path), and executable by the web server. + +; Input type selection +; TYPE is the extension. 'allowed' certifies that transcoding works properly for +; this input format. 'required' further forbids the direct streaming of a format +; (e.g. if you store everything in FLAC, but don't want to ever stream that.) +; transcode_TYPE = {allowed|required|false} +; DEFAULT: false +;transcode_m4a = allowed +;transcode_flac = required +;transcode_mp3 = allowed + +; Default output format +; DEFAULT: none +;encode_target = mp3 + +; Override the default output format on a per-type basis +; encode_target_TYPE = TYPE +; DEFAULT: none +;encode_target_flac = ogg + +; Command configuration. Substitutions will be made as follows: +; %FILE% => filename +; %SAMPLE% => target sample rate +; You can do fancy things like VBR, but consider whether the consequences are +; acceptable in your environment. + +; Master transcode command +; transcode_cmd should be a single command that supports multiple file types, +; such as ffmpeg or avconv. It's still possible to make a configuration that's +; equivalent to the old default, but if you find that necessary you should be +; clever enough to figure out how on your own. +; DEFAULT: none +;transcode_cmd = "ffmpeg -i %FILE" + +; Specific transcode commands +; It shouldn't be necessary in most cases, but you can override the transcode +; command for specific source formats. It still needs to accept the +; encoding arguments, so the easiest approach is to use your normal command as +; a clearing-house. +; transcode_cmd_TYPE = TRANSCODE_CMD +;transcode_cmd_mid = "timidity -Or -o – %FILE% | ffmpeg -i pipe:0" + +; encode_args_TYPE = TRANSCODE_CMD_ARGS +;encode_args_mp3 = "-vn -b:a %SAMPLE%K -c:a mp3 -f mp3 pipe:1" +;encode_args_ogg = "-vn -b:a %SAMPLE%K -c:a vorbis -f ogg pipe:1" ;###################################################### ; these options allow you to configure your rss-feed diff --git a/docs/CHANGELOG b/docs/CHANGELOG index 54f1fe27..68c7b73b 100755 --- a/docs/CHANGELOG +++ b/docs/CHANGELOG @@ -4,6 +4,7 @@ -------------------------------------------------------------------------- v.3.6-FUTURE + - Made transcoding and its configuration more flexible - Made transcoded streams more standards compliant by not sending a random value as the Content-Length or claiming that ranged requests are supported diff --git a/lib/class/media.interface.php b/lib/class/media.interface.php index 617e12b5..b82b5ea8 100644 --- a/lib/class/media.interface.php +++ b/lib/class/media.interface.php @@ -1,8 +1,6 @@ <?php /* vim:set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */ /** - * media Interface - * * * LICENSE: GNU General Public License, version 2 (GPLv2) * Copyright (c) 2001 - 2011 Ampache.org All Rights Reserved @@ -19,11 +17,6 @@ * 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. - * - * @package Ampache - * @copyright 2001 - 2011 Ampache.org - * @license http://opensource.org/licenses/gpl-2.0 GPLv2 - * @link http://www.ampache.org/ */ /** @@ -32,51 +25,45 @@ * This defines how the media file classes should * work, this lists all required functions and the expected * input - * - * @package Ampache - * @copyright 2001 - 2011 Ampache.org - * @license http://opensource.org/licenses/gpl-2.0 GPLv2 - * @link http://www.ampache.org/ - * @see Video - * @see Radio - * @see Random - * @see Song */ interface media { /** * format * - * @return + * Creates the gussied-up member variables for output */ public function format(); /** - * native_stream + * get_stream_types * - * @return mixed + * Returns an array of strings; current types are 'native' + * and 'transcode' */ - public function native_stream(); + public function get_stream_types(); /** * play_url * - * @param int $oid ID - * @return mixed + * Returns the url to stream the specified object + * */ public static function play_url($oid); /** - * stream_cmd + * get_transcode_settings * - * @return mixed + * Should only be called if 'transcode' was returned by get_stream_types + * Returns a raw transcode command for this item; the optional target + * parameter can be used to request a specific format instead of the + * default from the configuration file. */ - public function stream_cmd(); + public function get_transcode_settings($target = null); /** * has_flag * - * @return mixed */ public function has_flag(); diff --git a/lib/class/radio.class.php b/lib/class/radio.class.php index d4be4f76..a8d33ef3 100644 --- a/lib/class/radio.class.php +++ b/lib/class/radio.class.php @@ -188,13 +188,11 @@ class Radio extends database_object implements media { } // delete /** - * native_stream + * get_stream_types * This is needed by the media interface */ - public function native_stream() { - - - + public function get_stream_types() { + return array('foreign'); } // native_stream /** @@ -220,13 +218,13 @@ class Radio extends database_object implements media { } // has_flag /** - * stream_cmd - * Needed by the media interface + * get_transcode_settings + * + * This will probably never be implemented */ - public function stream_cmd() { - - - } // stream_cmd + public function get_transcode_settings() { + return false; + } } //end of radio class diff --git a/lib/class/song.class.php b/lib/class/song.class.php index 70f6e881..ab38524a 100644 --- a/lib/class/song.class.php +++ b/lib/class/song.class.php @@ -62,11 +62,7 @@ class Song extends database_object implements media { public $mbid; // MusicBrainz ID /* 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 @@ -85,8 +81,9 @@ class Song extends database_object implements media { foreach ($info as $key=>$value) { $this->$key = $value; } - // Format the Type of the song - $this->format_type(); + $data = pathinfo($this->file); + $this->type = strtolower($data['extension']); + $this->mime = self::type_to_mime($this->type); } return true; @@ -222,61 +219,53 @@ class Song extends database_object implements media { } // fill_ext_info /** - * format_type - * gets the type of song we are trying to - * play, used to set mime headers and to trick - * players into playing them correctly + * type_to_mime + * + * Returns the mime type for the specified file extension/type */ - public function format_type($override='') { - - // If we pass an override for downsampling or whatever then use it - if (!empty($override)) { - $this->type = $override; - } - else { - $data = pathinfo($this->file); - $this->type = strtolower($data['extension']); - } - - switch ($this->type) { + public static function type_to_mime($type) { + // FIXME: This should really be done the other way around. + // Store the mime type in the database, and provide a function + // to make it a human-friendly type. + switch ($type) { case 'spx': case 'ogg': - $this->mime = "application/ogg"; + return 'application/ogg'; break; case 'wma': case 'asf': - $this->mime = "audio/x-ms-wma"; + return 'audio/x-ms-wma'; break; case 'mp3': case 'mpeg3': - $this->mime = "audio/mpeg"; + return 'audio/mpeg'; break; case 'rm': case 'ra': - $this->mime = "audio/x-realaudio"; + return 'audio/x-realaudio'; break; case 'flac'; - $this->mime = "audio/x-flac"; + return 'audio/x-flac'; break; case 'wv': - $this->mime = 'audio/x-wavpack'; + return 'audio/x-wavpack'; break; case 'aac': case 'mp4': case 'm4a': - $this->mime = "audio/mp4"; + return 'audio/mp4'; break; case 'mpc': - $this->mime = "audio/x-musepack"; + return 'audio/x-musepack'; break; default: - $this->mime = "audio/mpeg"; + return 'audio/mpeg'; break; } return true; - } // format_type + } /** * get_album_name @@ -923,63 +912,51 @@ class Song extends database_object implements media { } // get_recently_played - /** - * native_stream - * This returns true/false if this can be natively streamed - */ - public function native_stream() { - - if ($this->_transcoded) { return false; } - - $conf_var = 'transcode_' . $this->type; + public function get_stream_types() { + $types = array(); + $transcode = Config::get('transcode_' . $this->type); - if (Config::get($conf_var)) { - $this->set_transcode(); - return false; + if ($transcode != 'required') { + $types[] = 'native'; + } + if (make_bool($transcode)) { + $types[] = 'transcode'; } - return true; - - } // end native_stream - - /** - * set_transcode - * - * We want to transcode, set up the variables correctly - */ - public function set_transcode() { - if ($this->_transcoded) { return; } + return $types; + } // end stream_types - $conf_type = 'transcode_' . $this->type . '_target'; - $conf_cmd = 'transcode_cmd_' . $this->type; + public function get_transcode_settings($target = null) { + $source = $this->type; - $this->_transcoded = true; - $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; + if ($target) { + debug_event('transcode', 'Explicit format request', 5); + } + else if ($target = Config::get('encode_target_' . $source)) { + debug_event('transcode', 'Defaulting to configured target format for ' . $source, 5); + } + else if ($target = Config::get('encode_target')) { + debug_event('transcode', 'Using default target format', 5); + } + else { + $target = $source; + debug_event('transcode', 'No default target for ' . $source . ', choosing to resample', 5); } - debug_event('transcode', 'Transcoding from ' . - $this->transcoded_from . ' to ' . $this->type, 5); - } + debug_event('transcode', 'Transcoding from ' . $source . ' to ' . $target, 5); - /** - * stream_cmd - * - * test if the song type streams natively and - * if not returns a transcoding command from the config - */ - public function stream_cmd() { + $cmd = Config::get('transcode_cmd_' . $source) ?: Config::get('transcode_cmd'); + $args = Config::get('encode_args_' . $target); - if ($this->native_stream()) { - return null; + if (!$args) { + debug_event('transcode', 'Target format ' . $target . ' is not properly configured', 2); + return false; } - - return $this->_transcode_cmd; - } // end stream_cmd + debug_event('transcode', 'Command: ' . $cmd . ' Arguments: ' . $args, 5); + return array('format' => $target, + 'command' => $cmd . ' ' . $args); + } } // end of song class ?> diff --git a/lib/class/stream.class.php b/lib/class/stream.class.php index 871f8394..4a25885c 100644 --- a/lib/class/stream.class.php +++ b/lib/class/stream.class.php @@ -142,17 +142,18 @@ class Stream { * 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++ + * resampling of a song and returns the opened file handle. */ - public static function start_transcode(&$song, $song_name = 0) { + public static function start_transcode($song) { + $transcode_settings = $song->get_transcode_settings(); + // Bail out early if we're unutterably broken + if ($transcode_settings == false) { + debug_event('stream', 'Transcode requested, but get_transcode_settings failed', 2); + return false; + } - // 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(); // FIXME: This should be configurable for each output type $user_sample_rate = Config::get('sample_rate'); @@ -161,10 +162,6 @@ class Stream { $min_bitrate = $user_sample_rate; } - if (!$song_name) { - $song_name = $song->f_artist_full . " - " . $song->title . "." . $song->type; - } - // Are there site-wide constraints? (Dynamic downsampling.) if ($max_bitrate > 1 ) { $sql = 'SELECT COUNT(*) FROM `now_playing` ' . @@ -180,15 +177,16 @@ class Stream { $results = Dba::fetch_row($db_results); $active_streams = intval($results[0]) ?: 0; - debug_event('transcode', 'Active streams: ' . $active_streams, 5); + debug_event('stream', 'Active transcoding streams: ' . $active_streams, 5); // We count as one for the algorithm + // FIXME: Should this reflect the actual bit rates? $active_streams++; $sample_rate = floor($max_bitrate / $active_streams); // Exit if this would be insane if ($sample_rate < ($min_bitrate ?: 8)) { - debug_event('transcode', 'Max bandwidth already allocated. Active streams: ' . $active_streams, 2); + debug_event('stream', 'Max transcode bandwidth already allocated. Active streams: ' . $active_streams, 2); header('HTTP/1.1 503 Service Temporarily Unavailable'); exit(); } @@ -203,26 +201,23 @@ class Stream { $sample_rate = $user_sample_rate; } - debug_event('transcode', 'Configured bitrate is ' . $sample_rate, 5); + debug_event('stream', 'Configured bitrate is ' . $sample_rate, 5); - /* Validate the bitrate */ + // Validate the bitrate $sample_rate = self::validate_bitrate($sample_rate); // Never upsample a song - if ($song->resampled && ($sample_rate * 1000) > $song->bitrate) { - debug_event('transcode', 'Clamping bitrate to avoid upsampling to ' . $sample_rate, 5); + if ($song->type == $transcode_settings['format'] && ($sample_rate * 1000) > $song->bitrate) { + debug_event('stream', 'Clamping bitrate to avoid upsampling to ' . $sample_rate, 5); $sample_rate = self::validate_bitrate($song->bitrate / 1000); } - debug_event('transcode', 'Final bitrate is ' . $sample_rate, 5); + debug_event('stream', 'Final transcode bitrate is ' . $sample_rate, 5); $song_file = scrub_arg($song->file); - $transcode_command = $song->stream_cmd(); - if ($transcode_command == null) { - debug_event('downsample', 'song->stream_cmd() returned null', 2); - return null; - } + // Finalise the command line + $command = $transcode_settings['command']; $string_map = array( '%FILE%' => $song_file, @@ -230,20 +225,20 @@ class Stream { ); foreach ($string_map as $search => $replace) { - $transcode_command = str_replace($search, $replace, $transcode_command, $ret); + $command = str_replace($search, $replace, $command, $ret); if (!$ret) { debug_event('downsample', "$search not in downsample command", 5); } } - debug_event('downsample', "Downsample command: $transcode_command", 3); + debug_event('downsample', "Downsample command: $command", 3); - $fp = popen($transcode_command, 'rb'); - - // Return our new handle - return $fp; + return array( + 'handle' => popen($command, 'rb'), + 'format' => $transcode_settings['format'] + ); - } // start_downsample + } /** * validate_bitrate diff --git a/lib/class/video.class.php b/lib/class/video.class.php index 22bafa19..3802048a 100644 --- a/lib/class/video.class.php +++ b/lib/class/video.class.php @@ -95,13 +95,9 @@ class Video extends database_object implements media { } // format - /** - * native_stream - * This returns true or false on the downsampling mojo - */ - public function native_stream() { + public function get_stream_types() { - return true; + return array('native'); } // native_stream @@ -126,16 +122,13 @@ class Video extends database_object implements media { } // play_url /** - * stream_cmd - * test and see if the video needs to be natively streamed - * if not it returns the transocding command from the config file - * we can't use this->type because its been formated for the downsampling + * get_transcode_settings + * + * FIXME: Video transcoding is not implemented */ - public function stream_cmd() { - - - - } // stream_cmd + public function get_transcode_settings($target = null) { + return false; + } /** * has_flag diff --git a/lib/init.php b/lib/init.php index 51f16561..a93f1800 100644 --- a/lib/init.php +++ b/lib/init.php @@ -71,7 +71,7 @@ if ($link) { /** This is the version.... fluf nothing more... **/ $results['version'] = '3.6-Alpha4-DEV'; -$results['int_config_version'] = '11'; +$results['int_config_version'] = '12'; if ($results['force_ssl']) { $http_type = 'https://'; diff --git a/play/index.php b/play/index.php index 86932f81..3909c234 100644 --- a/play/index.php +++ b/play/index.php @@ -309,32 +309,49 @@ if (Config::get('track_user_ip')) { $GLOBALS['user']->insert_ip_history(); } -$downsample_remote = false; +$force_downsample = false; if (Config::get('downsample_remote')) { 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); - $downsample_remote = true; + debug_event('play', 'Downsampling enabled for non-local address ' . $_SERVER['REMOTE_ADDR'], 5); + $force_downsample = true; } } -// If they are downsampling, or if the song is not a native stream or it's non-local -if (((Config::get('transcode') == 'always' && !$video) || - !$media->native_stream() || - $downsample_remote) && Config::get('transcode') != 'never') { - debug_event('downsample', - 'Decided to transcode. Transcode:' . Config::get('transcode') . - ' Native Stream: ' . ($media->native_stream() ? 'true' : 'false') . - ' Remote: ' . ($downsample_remote ? 'true' : 'false'), 5); +// Determine whether to transcode +$transcode = false; +$transcode_cfg = Config::get('transcode'); +$valid_types = $media->get_stream_types(); +if ($transcode_cfg != 'never' && in_array('transcode', $valid_types)) { + if ($transcode_cfg == 'always') { + $transcode = true; + debug_event('play', 'Transcoding due to always', 5); + } + else if ($force_downsample) { + $transcode = true; + debug_event('play', 'Transcoding due to downsample_remote', 5); + } + else if (!in_array('native', $valid_types)) { + $transcode = true; + debug_event('play', 'Transcoding because native streaming is unavailable', 5); + } + else { + debug_event('play', 'Decided not to transcode', 5); + } +} + +if ($transcode) { header('Accept-Ranges: none'); - $media->set_transcode(); - $fp = Stream::start_transcode($media, $media_name, $start); - $media_name = $media->f_artist_full . " - " . $media->title . "." . $media->type; - $transcoded = true; -} // end if downsampling + $transcoder = Stream::start_transcode($media); + $fp = $transcoder['handle']; + $media_name = $media->f_artist_full . " - " . $media->title . "." . $transcoder['format']; +} +else if (!in_array('native', $valid_types)) { + debug_event('play', 'Not transcoding and native streaming is not supported, aborting', 2); + exit(); +} else { header('Accept-Ranges: bytes'); $fp = fopen($media->file, 'rb'); - $transcoded = false; } if (!is_resource($fp)) { @@ -347,7 +364,7 @@ if (get_class($media) == 'Song') { Stream::insert_now_playing($media->id,$uid,$media->time,$sid,get_class($media)); } -if ($transcoded) { +if ($transcode) { $stream_size = null; } else { @@ -368,8 +385,8 @@ if ($start > 0 || $end > 0 ) { $stream_size = $media->size - $start; } - if ($transcoded) { - debug_event('play', 'Bad client behaviour. Content-Range header received, which we cannot fulfill due to transcoding', 1); + if ($transcode) { + debug_event('play', 'Bad client behaviour. Content-Range header received, which we cannot fulfill due to transcoding', 2); $stream_size = null; } else { @@ -385,19 +402,23 @@ else { debug_event('play','Starting stream of ' . $media->file . ' with size ' . $media->size, 5); } -$browser->downloadHeaders($media_name, $media->mime, false, $stream_size); +$mime = $transcode + ? $media->type_to_mime($transcoder['format']) + : $media->mime; + +$browser->downloadHeaders($media_name, $mime, false, $stream_size); $bytes_streamed = 0; // Actually do the streaming do { - $read_size = $transcoded + $read_size = $transcode ? 2048 : min(2048, $stream_size - $bytes_streamed); $buf = fread($fp, $read_size); print($buf); $bytes_streamed += strlen($buf); -} while (!feof($fp) && (connection_status() == 0) && ($transcoded || $bytes_streamed < $stream_size)); +} while (!feof($fp) && (connection_status() == 0) && ($transcode || $bytes_streamed < $stream_size)); $real_bytes_streamed = $bytes_streamed; // Need to make sure enough bytes were sent. @@ -439,7 +460,7 @@ else { // We do this regardless of play amount. if ($demo_id) { $democratic->delete_from_oid($oid,'song'); } -if ($transcoded) { +if ($transcode) { pclose($fp); } else { |