├── .gitignore ├── tests ├── bootstrap.php ├── AudioInspectorTest.php ├── TimeCalculatorTest.php ├── TranscodedSizeEstimatorTest.php ├── TranscoderTest.php ├── StreamerTest.php └── Mp3StreamTest.php ├── example.flac ├── .travis.yml ├── example.php ├── src └── Captbaritone │ └── TranscodeToMp3Stream │ ├── HeaderBuilder.php │ ├── Pipe.php │ ├── TimeCalculator.php │ ├── Transcoder.php │ ├── Streamer.php │ ├── TranscodedSizeEstimator.php │ ├── Mp3Stream.php │ └── AudioInspector.php ├── composer.json ├── phpunit.xml ├── composer.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | output($sourceMedia, $outputFilename, $kbps, $start, $end); 16 | 17 | -------------------------------------------------------------------------------- /tests/AudioInspectorTest.php: -------------------------------------------------------------------------------- 1 | getLength($file); 20 | $this->assertEquals('10', $length); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/HeaderBuilder.php: -------------------------------------------------------------------------------- 1 | source)); 9 | header('Cache-Control: public, must-revalidate, max-age=0'); 10 | header('Pragma: no-cache'); 11 | header('Accept-Ranges: bytes'); 12 | header("Content-Length: {$size}"); 13 | header('Content-type: audio/mpeg'); 14 | header("Content-Disposition: inline; filename=\"{$filename}\""); 15 | header("Content-Transfer-Encoding: binary"); 16 | //header("Last-Modified: $time"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/TimeCalculatorTest.php: -------------------------------------------------------------------------------- 1 | lengthInSeconds('01:01:01:15'); 12 | 13 | $this->assertEquals($seconds, 3661.2); 14 | } 15 | 16 | public function testDiffInSeconds() 17 | { 18 | $calc = new TimeCalculator(); 19 | 20 | $seconds = $calc->diffInSeconds('01:01:01:15', '00:01:01:00'); 21 | 22 | $this->assertEquals($seconds, 3600.2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/TranscodedSizeEstimatorTest.php: -------------------------------------------------------------------------------- 1 | estimatedBytes(1, 128); 12 | 13 | $this->assertEquals(16000, $estimate); 14 | } 15 | 16 | public function testEstimatedBytesAreWholeNumber() 17 | { 18 | $estimator = new TranscodedSizeEstimator(); 19 | 20 | $estimate = $estimator->estimatedBytes(1.0001, 128); 21 | 22 | $this->assertEquals(16002, $estimate); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/TranscoderTest.php: -------------------------------------------------------------------------------- 1 | command('testfile.flac', 'KBPS', 'start', 'end'); 19 | 20 | $expected = "exec avconv -ss 'start' -t 'end' -i 'testfile.flac' -ab 'KBPSk' -minrate 'KBPSk' -maxrate 'KBPSk' -bufsize 64k -f mp3 -map_metadata -1 - 2>/dev/null"; 21 | 22 | $this->assertEquals($command, $expected); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captbaritone/transcode-to-mp3-stream", 3 | "type": "library", 4 | "description": "Transcode various audio formats to mp3 streams on the fly", 5 | "keywords": ["mp3","transcode", "stream", "audio"], 6 | "homepage": "http://github.com/captbaritone/transcode-to-mp3-stream", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jordan Eldredge", 11 | "email": "jordanEldredge@gmail.com", 12 | "homepage": "http://jordaneldredge.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.3.0" 18 | }, 19 | "require-dev": { 20 | "mockery/mockery": "dev-master" 21 | }, 22 | "autoload": { 23 | "psr-0": { 24 | "Captbaritone\\TranscodeToMp3Stream": "src" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/Pipe.php: -------------------------------------------------------------------------------- 1 | handle = proc_open($cmd, [ 1 => [ 'pipe', 'w' ] ], $this->pipes); 12 | register_shutdown_function( [ $this, 'terminate' ] ); 13 | } 14 | 15 | public function fread($bytes) 16 | { 17 | return fread($this->pipes[1], $bytes); 18 | } 19 | 20 | public function feof() 21 | { 22 | return feof($this->pipes[1]); 23 | } 24 | 25 | public function close() 26 | { 27 | fclose($this->pipes[1]); 28 | return pclose($this->handle); 29 | } 30 | 31 | public function terminate() 32 | { 33 | return proc_terminate($this->handle); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | tests 16 | 17 | 18 | tests 19 | tests/AudioInspectorTest.php 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/TimeCalculator.php: -------------------------------------------------------------------------------- 1 | lengthInSeconds($a) - $this->lengthInSeconds($b)); 13 | } 14 | 15 | public function lengthInSeconds($time) 16 | { 17 | $units = $this->parseTime($time); 18 | 19 | $seconds = $units['frames'] / $this->framesPerSecond + 20 | $units['seconds'] + 21 | ($units['minutes'] * $this->secondsPerMinute) + 22 | ($units['hours'] * $this->secondsPerMinute * $this->minutesPerHour); 23 | 24 | return $seconds; 25 | } 26 | 27 | private function parseTime($time) 28 | { 29 | list($hours, $minutes, $seconds, $frames) = explode(':', $time); 30 | 31 | return array( 32 | 'hours' => $hours, 33 | 'minutes' => $minutes, 34 | 'seconds' => $seconds, 35 | 'frames' => $frames, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/Transcoder.php: -------------------------------------------------------------------------------- 1 | /dev/null"; // Pass the result to stdout 33 | return implode(' ', $args); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/Streamer.php: -------------------------------------------------------------------------------- 1 | pipe = $pipe ?: new Pipe(); 13 | } 14 | 15 | public function outputStream($cmd, $byteGoal) 16 | { 17 | $this->pipe->open($cmd); 18 | 19 | // Initilize our count of bytes sent 20 | $outputSize = 0; 21 | 22 | while (!$this->pipe->feof()) { 23 | 24 | $content = $this->pipe->fread(1); 25 | 26 | echo $content; 27 | 28 | $outputSize += strlen($content); 29 | 30 | // check to make sure we have't reached our goal 31 | if($outputSize >= $byteGoal) 32 | { 33 | break; 34 | } 35 | } 36 | 37 | echo $this->getPadding($outputSize, $byteGoal); 38 | 39 | $this->pipe->close(); 40 | } 41 | 42 | protected function getPadding($outputSize, $byteGoal) 43 | { 44 | // If we still haven't reached our goal, fill the remaining bytes with 45 | // "0" 46 | if($outputSize <= $byteGoal){ 47 | return str_pad('', $byteGoal - $outputSize, '0'); 48 | } 49 | 50 | return ''; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/StreamerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('open') 19 | ->once(); 20 | 21 | $pipe->shouldReceive('feof') 22 | ->once() 23 | ->andReturn(false); 24 | 25 | $pipe->shouldReceive('feof') 26 | ->once() 27 | ->andReturn(true); 28 | 29 | $pipe->shouldReceive('fread') 30 | ->with(1) 31 | ->once() 32 | ->andReturn("a"); 33 | 34 | $pipe->shouldReceive('close')->once(); 35 | 36 | $streamer = new Streamer($pipe); 37 | 38 | $streamer->outputStream($pipe, 3); 39 | $this->expectOutputString('a00'); 40 | } 41 | 42 | public function testOutputStreamTooLong() 43 | { 44 | $pipe = m::mock('Pipe'); 45 | 46 | $pipe->shouldReceive('open') 47 | ->once(); 48 | 49 | $pipe->shouldReceive('feof') 50 | ->once() 51 | ->andReturn(false); 52 | 53 | $pipe->shouldReceive('fread') 54 | ->with(1) 55 | ->andReturn("a"); 56 | 57 | $pipe->shouldReceive('close')->once(); 58 | 59 | $streamer = new Streamer($pipe); 60 | 61 | $streamer->outputStream($pipe, 1); 62 | $this->expectOutputString('a'); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/TranscodedSizeEstimator.php: -------------------------------------------------------------------------------- 1 | 12 | **/ 13 | class TranscodedSizeEstimator 14 | { 15 | 16 | protected $bitsPerByte = 8; 17 | 18 | 19 | /** 20 | * Estimate Bytes 21 | * 22 | * Estimate the byte size of a constant rate mp3 given it's lenght and kbps 23 | * 24 | * @param float $length length of the audio in seconds 25 | * @param int $kbps constant bitrate kbit per second 26 | * @return int approximate size, in bytes, of the mp3 27 | * @author Jordan Eldredge 28 | **/ 29 | public function estimatedBytes($length, $kbps) 30 | { 31 | return round($this->estimatedBits($length, $kbps) / $this->bitsPerByte); 32 | } 33 | 34 | /** 35 | * Estimate Bits 36 | * 37 | * Estimate the bit size of a constant rate mp3 given it's lenght and kbps 38 | * 39 | * @param float $length length of the audio in seconds 40 | * @param int $kbps constant bitrate kbit per second 41 | * @return int approximate size, in bits, of the mp3 42 | * @author Jordan Eldredge 43 | **/ 44 | private function estimatedBits($length, $kbps) 45 | { 46 | return $length * $kbps * 1000; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/Mp3Stream.php: -------------------------------------------------------------------------------- 1 | transcoder = $transcoder ?: new Transcoder(); 21 | $this->streamer = $streamer ?: new Streamer(); 22 | $this->headerBuilder = $headerBuilder ?: new HeaderBuilder(); 23 | $this->audioInspector = $audioInspector ?: new AudioInspector(); 24 | $this->transcodedSizeEstimator = $transcodedSizeEstimator ?: new TranscodedSizeEstimator(); 25 | } 26 | 27 | public function output($sourceMedia, $outputFilename = 'test.mp3', $kbps = 128, $start = 0, $end = 0) 28 | { 29 | 30 | $endTime = $end ?: $this->audioInspector->getLength($sourceMedia); 31 | $length = $endTime - $start; 32 | 33 | $cmd = $this->transcoder->command($sourceMedia, $kbps, $start, $length); 34 | 35 | $byteGoal = $this->transcodedSizeEstimator->estimatedBytes($length, $kbps); 36 | 37 | $this->headerBuilder->putHeader($outputFilename, $byteGoal); 38 | 39 | $this->streamer->outputStream($cmd, $byteGoal); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Mp3StreamTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('command') 27 | ->with($sourceMedia, $kbps, $start, $end) 28 | ->andReturn($command); 29 | 30 | $transcodedSizeEstimator = m::mock('TranscodedSizeEstimator'); 31 | $transcodedSizeEstimator->shouldReceive('estimatedBytes') 32 | ->once() 33 | ->with(0, $kbps) 34 | ->andReturn($byteGoal); 35 | 36 | $streamer = m::mock('Streamer'); 37 | $streamer->shouldReceive('outputStream') 38 | ->once() 39 | ->with($command, $byteGoal); 40 | 41 | $headerBuilder = m::mock('HeaderBuilder'); 42 | $headerBuilder->shouldReceive('putHeader') 43 | ->once() 44 | ->with($outputFilename, $byteGoal); 45 | 46 | 47 | $audioInspector = m::mock('AudioInspector'); 48 | $audioInspector->shouldReceive('getLength') 49 | ->once() 50 | ->andReturn(0); 51 | 52 | $mp3Stream = new Mp3Stream( 53 | $transcoder, 54 | $streamer, 55 | $headerBuilder, 56 | $audioInspector, 57 | $transcodedSizeEstimator 58 | ); 59 | 60 | $mp3Stream->output($sourceMedia, $outputFilename, $kbps, $start, $end); 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Captbaritone/TranscodeToMp3Stream/AudioInspector.php: -------------------------------------------------------------------------------- 1 | 10 | **/ 11 | class AudioInspector 12 | { 13 | 14 | public function getLength($file) 15 | { 16 | $file = escapeshellarg($file); 17 | $cmd = "avprobe -v quiet -show_format -show_streams {$file} 2>&1 | grep -m 1 'duration=' | grep -o '[0-9.]\+'"; 18 | 19 | $output = exec($cmd); 20 | return $output; 21 | } 22 | 23 | /** 24 | * Modern Get Length 25 | * 26 | * REQUIRES ffmpeg 0.9, which we don't have yet. 27 | * 28 | * Get the lenght of an audio or video media file 29 | * 30 | * @param string $file path to the media file we are querying 31 | * @return float decimal length of $file in seconds 32 | * @author Jordan Eldredge 33 | **/ 34 | public function modernGetLength($file) 35 | { 36 | $json = $this->probe($file); 37 | return $json->streams[0]->duration; 38 | } 39 | 40 | /** 41 | * Probe 42 | * 43 | * REQUIRES ffmpeg 0.9, which we don't have yet. 44 | * 45 | * Get ffprobe's media information for a file using it's json output 46 | * 47 | * @param string $file path to the media file we are querying 48 | * @return obj ffprobe's media information 49 | * @author Jordan Eldredge 50 | **/ 51 | protected function probe($file) 52 | { 53 | $file = escapeshellarg($file); 54 | $cmd = "ffprobe -v quiet -print_format json -show_format -show_streams '{$file}'"; 55 | exec($cmd, $outputLines); 56 | $json = implode("\n", $outputLines); 57 | 58 | return json_decode($json); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" 5 | ], 6 | "hash": "fb0d420d7dd2e507abd5d89e2d7f8fea", 7 | "packages": [ 8 | 9 | ], 10 | "packages-dev": [ 11 | { 12 | "name": "mockery/mockery", 13 | "version": "dev-master", 14 | "source": { 15 | "type": "git", 16 | "url": "https://github.com/padraic/mockery.git", 17 | "reference": "e7ac8f05f9e954d4387732e58d9be43b102fbe93" 18 | }, 19 | "dist": { 20 | "type": "zip", 21 | "url": "https://api.github.com/repos/padraic/mockery/zipball/e7ac8f05f9e954d4387732e58d9be43b102fbe93", 22 | "reference": "e7ac8f05f9e954d4387732e58d9be43b102fbe93", 23 | "shasum": "" 24 | }, 25 | "require": { 26 | "lib-pcre": ">=7.0", 27 | "php": ">=5.3.2" 28 | }, 29 | "require-dev": { 30 | "hamcrest/hamcrest-php": "~1.1" 31 | }, 32 | "type": "library", 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "0.9.x-dev" 36 | } 37 | }, 38 | "autoload": { 39 | "psr-0": { 40 | "Mockery": "library/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "BSD-3-Clause" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Pádraic Brady", 50 | "email": "padraic.brady@gmail.com", 51 | "homepage": "http://blog.astrumfutura.com" 52 | }, 53 | { 54 | "name": "Dave Marshall", 55 | "email": "dave.marshall@atstsolutions.co.uk", 56 | "homepage": "http://davedevelopment.co.uk" 57 | } 58 | ], 59 | "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", 60 | "homepage": "http://github.com/padraic/mockery", 61 | "keywords": [ 62 | "BDD", 63 | "TDD", 64 | "library", 65 | "mock", 66 | "mock objects", 67 | "mockery", 68 | "stub", 69 | "test", 70 | "test double", 71 | "testing" 72 | ], 73 | "time": "2014-01-17 11:55:29" 74 | } 75 | ], 76 | "aliases": [ 77 | 78 | ], 79 | "minimum-stability": "stable", 80 | "stability-flags": { 81 | "mockery/mockery": 20 82 | }, 83 | "platform": { 84 | "php": ">=5.3.0" 85 | }, 86 | "platform-dev": [ 87 | 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transcode to MP3 Stream PHP Library 2 | 3 | [![Build Status](https://travis-ci.org/captbaritone/transcode-to-mp3-stream.png?branch=master)](https://travis-ci.org/captbaritone/transcode-to-mp3-stream) 4 | 5 | Transcode, on the fly, any media file that ffmpeg can read into an MP3 stream 6 | playable natively by any modern browser. The library handles the annoying 7 | requirements of generating the correct headers and relies on ffmpeg to handle 8 | the actual transcoding. 9 | 10 | Useful for cases where you have a large collection of audio files which you 11 | want users to be able to stream, but don't wish to transcode them all in 12 | advance. 13 | 14 | Works well with HTML5/JS audio players such as 15 | [audio.js](http://kolber.github.io/audiojs/). 16 | 17 | ## Status 18 | 19 | Beta. This package is working, but may still have some kinks to workout. 20 | 21 | ## Dependencies 22 | 23 | We require shell access to `ffmpeg` with the `lame` codec for mp3 encoding. 24 | We also require a newish version of `ffprobe` (which comes with `ffmpeg`). 25 | However, the current version of in the Ubuntu repository is not modern enough. 26 | I'll update with more on that later. 27 | 28 | On Ubuntu, these packages do the trick for me: 29 | 30 | sudo apt-get install ffmpeg 31 | sudo apt-get install libavcodec-extra-53 32 | 33 | ## Installation 34 | 35 | Add this line to your `composer.json` file's "require" section: 36 | 37 | "captbaritone/transcode-to-mp3-stream": "dev-master" 38 | 39 | Then issue at the command line, in your projects directory: 40 | 41 | composer update 42 | 43 | ## Usage 44 | 45 | I'm assuming, you have the package installed via composer. 46 | 47 | The only method you should need is `output()` on the `Mp3Stream` object. It 48 | takes the following arguments, of which only the first is required: 49 | 50 | - `$sourceMedia` Path to the file you wish to transcode 51 | - `$outputFilename` The filename for the transcoded output 52 | - `$kbps` The constant bitrate bits per second to encode the output with 53 | - `$start` The point in the source where we want to start (in seconds) 54 | - `$end` The point in the source where we want to stop (in seconds) 55 | 56 | ### Simple usage: 57 | 58 | output('example.flac'); 64 | 65 | ## Example 66 | 67 | See `example.php` for an example of how the script can be used. 68 | 69 | If you have a new enough version of PHP, you could test it by issuing this in 70 | your terminal... 71 | 72 | cd /path/to/transcode-to-mp3-stream 73 | php -S localhost:9000 example.php 74 | 75 | 76 | ...and then opening `http://localhost:8000` in your browser. 77 | 78 | ## Testing 79 | 80 | Assuming you have both composer and phpunit install globally: 81 | 82 | git clone git@github.com:captbaritone/transcode-to-mp3-stream.git 83 | cd transcode-to-mp3-stream 84 | composer --dev install 85 | phpunit 86 | 87 | ## Help/questions 88 | 89 | If you have any questions, or get stuck please let me know: 90 | . 91 | 92 | ## License 93 | 94 | This project is released under the MIT License. The example audio file is an 95 | excerpt from a Creative Commons recording of J.S. Bach's Goldberg Variations. 96 | You can find the whole recording, and more information at the [Open Goldberg 97 | Variations] website. 98 | 99 | [Open Goldberg Variations]: http://www.opengoldbergvariations.org/ 100 | 101 | --------------------------------------------------------------------------------