├── .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 | [](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 |
--------------------------------------------------------------------------------