PageRenderTime 51ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/apps/client/lib/MediaProxy.class.php

https://github.com/esion/streeme
PHP | 349 lines | 245 code | 29 blank | 75 comment | 24 complexity | 8e87a254541a9a8e4d861472c490adad MD5 | raw file
  1. <?php
  2. /**
  3. * The Media Proxy / Gateway for streeme. This class conatins a standard system for delivering music files to the user
  4. * from nearly any valid media file. This service requires the PEAR HTTP_Download and HTTP libraries.
  5. * @package streeme
  6. * @subpackage play
  7. * @author Richard Hoar
  8. */
  9. error_reporting( 0 ); //HTTP download is extremely noisy
  10. require_once( 'HTTP/Download.php' );
  11. require_once( dirname( __FILE__ ) . '/StreemeUtil.class.php' );
  12. class MediaProxy
  13. {
  14. //private class variables
  15. private
  16. $source_bitrate,
  17. $source_duration,
  18. $source_format,
  19. $source_extension,
  20. $source_type,
  21. $source_basename,
  22. $source_filename,
  23. $source_file_length,
  24. $source_file_mtime,
  25. $target_extension,
  26. $target_type,
  27. $filename,
  28. $types,
  29. $ffmpeg_executable,
  30. $ffmpeg_args,
  31. $allow_transcoding,
  32. $use_chunked_encoding,
  33. $user_requested_format,
  34. $user_requested_bitrate;
  35. //user options
  36. protected $target_bitrate = false;
  37. protected $target_format = false;
  38. protected $is_icy_response = false;
  39. /**
  40. * Constructor - Hydrates class variables based on a song_id
  41. * @param unique_song_id str: song id (unique id in the song table)
  42. * @return bool: false if no song is found
  43. */
  44. public function __construct( $unique_song_id )
  45. {
  46. //get the song details by unique id
  47. $result = Doctrine_Core::getTable('Song')->getSongByUniqueId( $unique_song_id );
  48. if( !$result ) return false;
  49. //read configuration file /apps/client/config/app.yml
  50. $this->ffmpeg_executable = sprintf( '"%s"', sfConfig::get( 'app_ffmpeg_executable' ) );
  51. $this->allow_transcoding = sfConfig::get( 'app_allow_ffmpeg_transcoding' );
  52. //get the filename and test if it exists
  53. $this->filename = StreemeUtil::itunes_format_decode( $result->filename );
  54. if( !isset( $this->filename ) || empty( $this->filename ) )
  55. {
  56. $this->log( sprintf( 'There was no filename found for the key: %s', $unique_song_id ) );
  57. return false;
  58. }
  59. if( !is_readable( $this->filename ) )
  60. {
  61. $this->log( sprintf( 'The file for key %s could not be read from the filesystem', $unique_song_id ) );
  62. $this->filename = null;
  63. return false;
  64. }
  65. //get the source file information
  66. $fstat = stat( $this->filename );
  67. $file_info = pathinfo( $this->filename );
  68. //FFMPEG type list
  69. $this->types = array(
  70. 'mp3' => 'audio/mpeg',
  71. 'ogg' => 'audio/ogg',
  72. );
  73. //create three letter target_format extensions
  74. foreach( $this->types as $k => $v )
  75. {
  76. $this->target_formats[] = $k;
  77. }
  78. //extract source file details
  79. $this->source_bitrate = (int) $result->bitrate;
  80. $this->source_duration = (int) $result->accurate_length;
  81. $this->source_format = $file_info[ 'extension' ];
  82. $this->source_extension = '.' . $file_info[ 'extension' ];
  83. $this->source_type = $this->types[ $file_info[ 'extension' ] ];
  84. $this->source_basename = $file_info[ 'basename' ];
  85. $this->source_filename = $file_info[ 'filename' ];
  86. $this->source_file_length = $fstat[ 'size' ];
  87. $this->source_file_mtime = $fstat[ 'mtime' ];
  88. $result->free();
  89. unset( $result );
  90. }
  91. /**
  92. * Set the target bitrate for the stream
  93. * @param bitrate int: a bitrate in max kbps - will be scaled for VBR formats
  94. */
  95. public function setTargetBitrate( $bitrate )
  96. {
  97. if( $this->allow_transcoding )
  98. {
  99. $this->log( sprintf( 'Setting target bitrate to: %s', $bitrate ) );
  100. $this->target_bitrate = ( $bitrate ) ? (int) $bitrate : false;
  101. $this->user_requested_bitrate = ( $this->target_bitrate ) ? true : false;
  102. }
  103. else
  104. {
  105. $this->log( sprintf( 'Tried to set bitrate to: %s, but transcoding is not allowed.', $bitrate ) );
  106. }
  107. }
  108. /**
  109. * Set the target format for the stream
  110. * @param format str: a target format by 3 letter file extension
  111. */
  112. public function setTargetFormat( $format )
  113. {
  114. if( $this->allow_transcoding )
  115. {
  116. $this->log( sprintf( 'Setting target format to: %s', $format ) );
  117. $this->target_format = ( $format && in_array( strtolower( $format ), $this->target_formats ) ) ? strtolower( $format ) : false;
  118. $this->user_requested_format = ( $this->target_format ) ? true : false;
  119. $this->target_extension = '.' . $this->target_format;
  120. $this->target_type = $this->types[ $this->target_format ];
  121. }
  122. else
  123. {
  124. $this->log( sprintf( 'Tried to set format to: %s, but transcoding is not allowed.', $format ) );
  125. }
  126. }
  127. /**
  128. * Set the stream to use non HTTP standard headers for ICECAST compatible stream recievers
  129. * @param is_icy_response boolean
  130. */
  131. public function setIsIcyResponse( $is_icy_response )
  132. {
  133. if( $this->allow_transcoding )
  134. {
  135. $this->log( sprintf( 'ICY send protocol is: ', ( $is_icy_response ) ? 'on' : 'off' ) );
  136. $this->is_icy_response = ( $is_icy_response ) ? true : false;
  137. }
  138. else
  139. {
  140. $this->log( sprintf( 'Tried to change icy response state to: %s, but transcoding is not allowed.', ( $is_icy_response ) ? 'on' : 'off' ) );
  141. }
  142. }
  143. /**
  144. * play - this method will play the selection using class variables made in the constructor
  145. * this is the main public method for the class
  146. */
  147. public function play()
  148. {
  149. //determine right send method
  150. if(
  151. ( $this->user_requested_bitrate || $this->user_requested_format )
  152. && ( ( $this->target_bitrate <= $this->source_bitrate ) || ( strtolower( $this->source_extension ) == $this->target_extension ) )
  153. && !$this->is_icy_response
  154. )
  155. {
  156. $this->log( sprintf('Attempting to play filename: %s in format: %s with bitrate: %s', $this->filename, $this->target_format, $this->target_bitrate ));
  157. $this->stream_modify();
  158. }
  159. else if(
  160. ( $this->user_requested_bitrate || $this->user_requested_format )
  161. && $this->is_icy_response
  162. && $this->target_format == 'mp3'
  163. )
  164. {
  165. $this->log( sprintf('Attempting to play filename: %s in format: %s with bitrate: %s using icy headers', $this->filename, $this->target_format, $this->target_bitrate ));
  166. $this->stream_icy();
  167. }
  168. else
  169. {
  170. $this->log( sprintf('Attempting to play original filename: %s', $this->filename, $this->target_format, $this->target_bitrate ));
  171. $this->stream_original();
  172. }
  173. }
  174. /**
  175. * Instead of streaming the original file, we'll feed it to FFMPEG for modification and copy its
  176. * output to the output bufer in binary mode. You may want a fastish computer for this.
  177. * method will only work if you have the ffmpg executable installed an enabled with the correct codec support
  178. */
  179. private function stream_modify()
  180. {
  181. header("HTTP/1.1 200 OK");
  182. header("Content-Type: " . ( ( $this->user_requested_format ) ? $this->target_type : $this->source_type ) );
  183. header("Content-Disposition: inline; filename=" . $this->source_filename . $this->target_extension );
  184. header("Content-Encoding: none");
  185. $this->ffmpeg_passthru();
  186. }
  187. /**
  188. * Stream this file using nonstandard HTTP headers for shoutcast servers.
  189. * it will output ICY 200 OK for shoutcast clients with no/incomplete HTTP/1.1 support.
  190. * method will only work if you have the ffmpeg executable installed an enabled with the correct codec support
  191. */
  192. private function stream_icy()
  193. {
  194. //special ICY - ice/shoutcast headers
  195. header( "ICY 200 OK" );
  196. header( "icy-name: Streeme Client Server" );
  197. header( "icy-genre: Unknown Genre" );
  198. header( "icy-pub: 1" );
  199. header( "icy-br: " . $this->target_bitrate );
  200. header( "icy-metaint: 8192" );
  201. header( "Content-Type: " . ( ( $this->user_requested_format ) ? $this->target_type : $this->source_type ) );
  202. header("Content-Encoding: none");
  203. $this->ffmpeg_passthru();
  204. }
  205. /**
  206. * Stream the original file from anywhere on the user's PC. This function will serve the original file
  207. * and offer ranges for seeking through the content.
  208. */
  209. private function stream_original()
  210. {
  211. //does the user have apache mod XSendFile installed? use that as a first priority
  212. //otherwise we can send it using php's PEAR HTTP_Download functionality
  213. $mods = apache_get_modules();
  214. $flip = array_flip( $mods );
  215. $mod_number = (string) $flip[ 'mod_xsendfile' ];
  216. if( !empty( $mod_number ) )
  217. {
  218. $this->log('Sending File using X-Sendfile Module');
  219. header("X-Sendfile: $this->filename");
  220. header("Content-Type: $this->source_type");
  221. header("Content-Disposition: attachment; filename=\"$this->source_basename\"");
  222. header("Content-Length: $this->source_file_length" );
  223. exit;
  224. }
  225. else
  226. {
  227. $this->log('Sending File using Pear HTTP Download');
  228. $params = array(
  229. 'File' => $this->filename,
  230. 'ContentType' => $this->source_type,
  231. 'BufferSize' => 32000,
  232. 'ContentDisposition' => array( HTTP_DOWNLOAD_INLINE, $this->source_basename ),
  233. );
  234. $error = HTTP_Download::staticSend( $params, false );
  235. }
  236. }
  237. /**
  238. * get the arguments for FFMPEG when resampling - set content lengths by algorithm (guesses)
  239. * @return str: commandline arguments for ffmpeg
  240. */
  241. private function get_ffmpeg_args()
  242. {
  243. $args = '-y '; //play without prompts / overwrite
  244. $args .= sprintf( '-i "%s" ', $this->filename ); //source filename
  245. $this->argformat = ( $this->user_requested_format ) ? $this->target_format : $this->source_format;
  246. $this->argbitrate = (int) ( $this->user_requested_bitrate ) ? $this->target_bitrate : $this->source_bitrate;
  247. switch ( $this->argformat )
  248. {
  249. case 'mp3':
  250. $args .= sprintf( '-ab %dk ', intval( $this->argbitrate ) ); //bitrate
  251. $args .= sprintf( '-acodec %s ', 'libmp3lame' ); //codec
  252. $args .= sprintf( '-f %s ', 'mp3' ); //container
  253. break;
  254. case 'ogg':
  255. $args .= sprintf( '-aq %d ', floor( intval( $this->argbitrate ) / 2 ) ); //vbr quality
  256. $args .= sprintf( '-acodec %s ', 'vorbis' );
  257. $args .= sprintf( '-f %s ', 'ogg' );
  258. break;
  259. }
  260. $args .= ' - ';
  261. return trim( $args );
  262. }
  263. /**
  264. * Use FFMPEG in a process to send re-compressed files on the fly
  265. * @return bool: false if user has not allowed ffmpeg transcoding
  266. */
  267. private function ffmpeg_passthru()
  268. {
  269. if( $this->allow_transcoding )
  270. {
  271. $this->log('Beginning Transcode Process...');
  272. $this->ffmpeg_args = $this->get_ffmpeg_args();
  273. switch ( $this->argformat )
  274. {
  275. case 'mp3':
  276. $this->output_mp3();
  277. break;
  278. case 'ogg':
  279. $this->output_ogg();
  280. break;
  281. }
  282. exit;
  283. }
  284. $this->log('Transcoding is disabled: check your app.yml file for options.');
  285. return false;
  286. }
  287. /**
  288. * Send an MP3 to the output buffer with an inaccurate content-length guess
  289. * calculate the new filesize ( this algortihm is a huge hack )
  290. */
  291. private function output_mp3()
  292. {
  293. $this->log( sprintf( 'Transcoding MP3 using ffmpeg command: %s %s', $this->ffmpeg_executable, $this->ffmpeg_args ) );
  294. $new_filesize = (( $this->source_duration / 1000 ) //time in seconds
  295. * ( $this->target_bitrate * 1000 ) //bitrate
  296. / 8 ) // convert to bytes
  297. - 1024; //trim 1024 bytes for headers
  298. header( 'Content-Length:' . $new_filesize );
  299. $this->log(sprintf( 'Content Length modified to %s bytes', $new_filesize ) );
  300. passthru( $this->ffmpeg_executable . ' ' . $this->ffmpeg_args );
  301. }
  302. /**
  303. * Send an OGG/Vorbis audio file to the output buffer with a very large filesize
  304. */
  305. private function output_ogg()
  306. {
  307. $this->log( sprintf( 'Transcoding OGG using ffmpeg command: %s %s', $this->ffmpeg_executable, $this->ffmpeg_args ) );
  308. header( 'Content-Length: 999999999' );
  309. passthru( $this->ffmpeg_executable . ' ' . $this->ffmpeg_args );
  310. }
  311. /**
  312. * Log Media Proxy Activity
  313. *
  314. * @param string $message
  315. */
  316. public function log( $message )
  317. {
  318. file_put_contents( dirname(__FILE__) . '/../../../log/proxy.log', date('Y-m-d h:i:s' ) . ' - {StreemeMediaProxy} ' . $message . "\r\n", FILE_APPEND);
  319. }
  320. }