├── _config.yml ├── .gitignore ├── tests ├── files │ ├── .gitignore │ ├── enc.key │ └── test.mp4 ├── TestCase.php ├── FileManagerTest.php ├── HLSFiltersTest.php ├── MediaTest.php ├── DASHFiltersTest.php ├── DASHTest.php └── HLSTest.php ├── renovate.json ├── src ├── HLSFlag.php ├── Exception │ ├── Exception.php │ ├── StreamingExceptionInterface.php │ ├── RuntimeException.php │ └── InvalidArgumentException.php ├── Filters │ ├── AudioDASHFilter.php │ ├── AudioHLSFilter.php │ ├── FormatFilter.php │ ├── StreamFilterInterface.php │ ├── StreamToFileFilter.php │ ├── StreamFilter.php │ ├── HLSFilterV2.php │ ├── DASHFilter.php │ └── HLSFilter.php ├── helpers.php ├── Clouds │ ├── CloudInterface.php │ ├── Cloud.php │ ├── S3.php │ ├── MicrosoftAzure.php │ └── GoogleCloudStorage.php ├── Format │ ├── StreamFormat.php │ ├── VP9.php │ ├── HEVC.php │ └── X264.php ├── StreamInterface.php ├── StreamToFile.php ├── FFMpegListener.php ├── Streaming.php ├── Traits │ ├── Representations.php │ └── Formats.php ├── Capture.php ├── Utiles.php ├── RepresentationInterface.php ├── CommandBuilder.php ├── DASH.php ├── RepsCollection.php ├── Media.php ├── FFMpeg.php ├── HLSPlaylist.php ├── Representation.php ├── File.php ├── HLSKeyInfo.php ├── Stream.php ├── Metadata.php ├── AutoReps.php ├── HLSSubtitle.php └── HLS.php ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── tests.yml ├── SECURITY.md ├── phpunit.xml ├── LICENSE ├── composer.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-modernist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /tests/files/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !enc.key 4 | !test.mp4 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/HLSFlag.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarstream/PHP-FFmpeg-video-streaming/HEAD/src/HLSFlag.php -------------------------------------------------------------------------------- /tests/files/enc.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarstream/PHP-FFmpeg-video-streaming/HEAD/tests/files/enc.key -------------------------------------------------------------------------------- /tests/files/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quasarstream/PHP-FFmpeg-video-streaming/HEAD/tests/files/test.mp4 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [quasarstream] 3 | patreon: HadronEpoch 4 | open_collective: hadronepoch 5 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Exception; 13 | 14 | class Exception extends \Exception implements StreamingExceptionInterface 15 | { 16 | } -------------------------------------------------------------------------------- /src/Exception/StreamingExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Exception; 13 | 14 | 15 | interface StreamingExceptionInterface extends \Throwable 16 | { 17 | } -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Exception; 13 | 14 | class RuntimeException extends \RuntimeException implements StreamingExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Exception; 13 | 14 | class InvalidArgumentException extends \InvalidArgumentException implements StreamingExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | You can use these versions of required libraries that are currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | -------------------- | ------------------ | 9 | | PHP > 7.2.x | :white_check_mark: | 10 | | FFMpeg > 4.x.x | :white_check_mark: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you discover a security vulnerability within this package, please send an e-mail to Amin Yazdanpanah via: contact [AT] aminyazdanpanah • com. 15 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | srcDir = __DIR__ . '/files'; 15 | } 16 | 17 | public function getFFMpeg(): FFMpeg 18 | { 19 | return FFMpeg::create(); 20 | } 21 | 22 | public function getVideo() 23 | { 24 | $service = $this->getFFMpeg(); 25 | return $service->open($this->srcDir . '/test.mp4'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Filters/AudioDASHFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Filters; 14 | 15 | 16 | use Streaming\StreamInterface; 17 | 18 | class AudioDASHFilter extends StreamFilter 19 | { 20 | 21 | /** 22 | * @param StreamInterface $stream 23 | * @return mixed 24 | */ 25 | public function streamFilter(StreamInterface $stream): void 26 | { 27 | // @TODO: Implement streamFilter() method. 28 | } 29 | } -------------------------------------------------------------------------------- /src/Filters/AudioHLSFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Filters; 14 | 15 | 16 | use Streaming\StreamInterface; 17 | 18 | class AudioHLSFilter extends FormatFilter 19 | { 20 | 21 | /** 22 | * @param StreamInterface $stream 23 | * @return mixed 24 | */ 25 | public function streamFilter(StreamInterface $stream): void 26 | { 27 | // TODO: Implement streamFilter() method. 28 | } 29 | } -------------------------------------------------------------------------------- /src/Filters/FormatFilter.php: -------------------------------------------------------------------------------- 1 | $format->getVideoCodec(), 20 | 'c:a' => $format->getAudioCodec(), 21 | ]); 22 | 23 | $options = Utiles::arrayToFFmpegOpt( 24 | array_merge($format->getAdditionalParameters() ?? []) 25 | ); 26 | 27 | return array_merge($basic, $options); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Filters/StreamFilterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Filters; 13 | 14 | use FFMpeg\Filters\FilterInterface; 15 | use Streaming\StreamInterface; 16 | 17 | interface StreamFilterInterface extends FilterInterface 18 | { 19 | /** 20 | * @param StreamInterface $stream 21 | * @return mixed 22 | */ 23 | public function streamFilter(StreamInterface $stream): void; 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function apply(): array; 29 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ------------------ | --- 3 | | Bug fix? | no 4 | | New feature? | no 5 | | BC breaks? | no 6 | | Deprecations? | no 7 | | Fixed tickets | fixes #issuenum 8 | | Related issues/PRs | #issuenum 9 | | License | MIT 10 | 11 | #### What's in this PR? 12 | 13 | Explain the contents of the PR. 14 | 15 | #### Why? 16 | 17 | Which problem does the PR fix? 18 | 19 | #### Example Usage 20 | 21 | ```php 22 | $foo = new Foo(); 23 | 24 | // Now we can do 25 | $foo->doSomething(); 26 | 27 | // Remove this section if not needed 28 | ~~~ 29 | 30 | #### BC Breaks/Deprecations 31 | 32 | Describe BC breaks/deprecations here (Remove this section if not needed). 33 | 34 | #### To Do 35 | 36 | - [ ] Create tests -------------------------------------------------------------------------------- /src/Filters/StreamToFileFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Filters; 14 | 15 | 16 | use Streaming\StreamInterface; 17 | 18 | class StreamToFileFilter extends FormatFilter 19 | { 20 | 21 | /** 22 | * @param $media 23 | * @return mixed 24 | */ 25 | public function streamFilter(StreamInterface $media): void 26 | { 27 | $this->filter = array_merge( 28 | $this->getFormatOptions($media->getFormat()), 29 | $media->getParams() 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | use FFMpeg\FFProbe; 13 | use Psr\Log\LoggerInterface; 14 | use Streaming\FFMpeg; 15 | 16 | 17 | if (!function_exists('ffmpeg')) { 18 | 19 | /** 20 | * @param array $config 21 | * @param LoggerInterface|null $logger 22 | * @param FFProbe|null $probe 23 | * @return FFMpeg 24 | */ 25 | function ffmpeg(array $config = [], LoggerInterface $logger = null, FFProbe $probe = null): FFMpeg 26 | { 27 | return FFMpeg::create($config, $logger, $probe); 28 | } 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '/../...' 16 | 2. On Line '...' 17 | 3. The code '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop/Server (please complete the following information):** 27 | - OS: [e.g. Linux] 28 | - Version [e.g. Ubuntu 18] 29 | - FFmpeg vesion [e.g. 4.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /src/Clouds/CloudInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Clouds; 14 | 15 | 16 | interface CloudInterface 17 | { 18 | /** 19 | * Upload a entire directory to a cloud 20 | * @param string $dir 21 | * @param array $options 22 | */ 23 | public function uploadDirectory(string $dir, array $options): void; 24 | 25 | /** 26 | * Download a file from a cloud 27 | * @param string $save_to 28 | * @param array $options 29 | */ 30 | public function download(string $save_to, array $options): void; 31 | } -------------------------------------------------------------------------------- /tests/FileManagerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\File; 15 | 16 | class FileManagerTest extends TestCase 17 | { 18 | public function testMakeDir() 19 | { 20 | $path = $this->srcDir . DIRECTORY_SEPARATOR . "test_make_dir"; 21 | File::makeDir($path); 22 | 23 | $this->assertDirectoryExists($path); 24 | } 25 | 26 | public function testTmp() 27 | { 28 | $tmp_file = File::tmp(); 29 | $tmp_dir = File::tmpDir(); 30 | 31 | $this->assertIsString($tmp_file); 32 | $this->assertIsString($tmp_dir); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/HLSFiltersTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\Filters\StreamFilter; 15 | use Streaming\Filters\HLSFilter; 16 | use Streaming\HLS; 17 | 18 | class HLSFiltersTest extends TestCase 19 | { 20 | public function testFilterClass() 21 | { 22 | $this->assertInstanceOf(StreamFilter::class, $this->getFilter()); 23 | } 24 | 25 | private function getFilter() 26 | { 27 | return new HLSFilter($this->getHLS()); 28 | } 29 | 30 | private function getHLS() 31 | { 32 | $hls = new HLS($this->getVideo()); 33 | 34 | return $hls->X264() 35 | ->autoGenerateRepresentations() 36 | ->setHlsAllowCache(false) 37 | ->setHlsTime(10); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Format/StreamFormat.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Format; 14 | 15 | 16 | use FFMpeg\Format\Video\DefaultVideo; 17 | use Streaming\Exception\InvalidArgumentException; 18 | 19 | abstract class StreamFormat extends DefaultVideo 20 | { 21 | /** 22 | * @param int $kiloBitrate 23 | * @return DefaultVideo|void 24 | */ 25 | public function setKiloBitrate($kiloBitrate) 26 | { 27 | throw new InvalidArgumentException("You can not set this option, use Representation instead"); 28 | } 29 | 30 | /** 31 | * @param int $kiloBitrate 32 | * @return DefaultVideo|void 33 | */ 34 | public function setAudioKiloBitrate($kiloBitrate) 35 | { 36 | throw new InvalidArgumentException("You can not set this option, use Representation instead"); 37 | } 38 | } -------------------------------------------------------------------------------- /src/StreamInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use FFMpeg\Format\VideoInterface; 17 | 18 | interface StreamInterface 19 | { 20 | /** 21 | * @return Media 22 | */ 23 | public function getMedia(): Media; 24 | 25 | /** 26 | * @return VideoInterface 27 | */ 28 | public function getFormat(): VideoInterface; 29 | 30 | /** 31 | * @param int $option 32 | * @return string 33 | */ 34 | public function pathInfo(int $option): string; 35 | 36 | /** 37 | * @param string $path 38 | * @param array $clouds 39 | * @return mixed 40 | */ 41 | public function save(string $path = null, array $clouds = []): Stream; 42 | 43 | /** 44 | * @param string $url 45 | */ 46 | public function live(string $url): void; 47 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Amin Yazdanpanah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Filters/StreamFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Filters; 13 | 14 | use Streaming\StreamInterface; 15 | 16 | abstract class StreamFilter implements StreamFilterInterface 17 | { 18 | private $priority = 2; 19 | 20 | protected $filter = []; 21 | 22 | /** 23 | * Filter constructor. 24 | * @param StreamInterface $stream 25 | */ 26 | public function __construct(StreamInterface $stream) 27 | { 28 | $this->streamFilter($stream); 29 | } 30 | 31 | /** 32 | * Applies the filter on the the stream media 33 | * 34 | * @return array An array of arguments 35 | */ 36 | public function apply(): array 37 | { 38 | return $this->getFilter(); 39 | } 40 | 41 | /** 42 | * Returns the priority of the filter. 43 | * 44 | * @return integer 45 | */ 46 | public function getPriority(): int 47 | { 48 | return $this->priority; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getFilter(): array 55 | { 56 | return $this->filter; 57 | } 58 | } -------------------------------------------------------------------------------- /src/StreamToFile.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use Streaming\Filters\StreamFilterInterface; 17 | use Streaming\Filters\StreamToFileFilter; 18 | 19 | class StreamToFile extends Stream 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private $params = []; 25 | 26 | /** 27 | * @param array $params 28 | * @return StreamToFile 29 | */ 30 | public function setParams(array $params): StreamToFile 31 | { 32 | $this->params = $params; 33 | return $this; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function getParams(): array 40 | { 41 | return $this->params; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | protected function getPath(): string 48 | { 49 | return implode(".", [$this->getFilePath(), $this->pathInfo(PATHINFO_EXTENSION) ?? "mp4"]); 50 | } 51 | 52 | /** 53 | * @return StreamToFileFilter 54 | */ 55 | protected function getFilter(): StreamFilterInterface 56 | { 57 | return new StreamToFileFilter($this); 58 | } 59 | } -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: PHP FFMPEG - VIDEO STREAMING - TESTS 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up PHP 8.4 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: 8.4 23 | tools: composer:v2 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate --strict 27 | 28 | - name: Cache Composer packages 29 | id: composer-cache 30 | uses: actions/cache@v3 31 | with: 32 | path: vendor 33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-php- 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress 39 | 40 | - name: Install FFmpeg 41 | run: | 42 | sudo apt-get update 43 | sudo apt-get install -y ffmpeg 44 | ffmpeg -version 45 | 46 | - name: Check PHP version and extensions 47 | run: | 48 | php -v 49 | php -m | grep -i gmp 50 | which ffmpeg 51 | 52 | - name: Run PHPUnit tests 53 | run: vendor/bin/phpunit 54 | -------------------------------------------------------------------------------- /src/FFMpegListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use Alchemy\BinaryDriver\Listeners\ListenerInterface; 17 | use Evenement\EventEmitter; 18 | 19 | class FFMpegListener extends EventEmitter implements ListenerInterface 20 | { 21 | /** @var string */ 22 | private $event; 23 | 24 | /** 25 | * FFMpegListener constructor. 26 | * @param string $event 27 | */ 28 | public function __construct($event = 'listen') 29 | { 30 | $this->event = $event; 31 | } 32 | 33 | /** 34 | * Handle the output of a ProcessRunner 35 | * 36 | * @param string $type The data type, one of Process::ERR, Process::OUT constants 37 | * @param string $data The output 38 | */ 39 | public function handle($type, $data) 40 | { 41 | foreach (explode(PHP_EOL, $data) as $line) { 42 | $this->emit($this->event, [$line]); 43 | } 44 | } 45 | 46 | /** 47 | * An array of events that should be forwarded to BinaryInterface 48 | * 49 | * @return array 50 | */ 51 | public function forwardedEvents() 52 | { 53 | return [$this->event]; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Filters/HLSFilterV2.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Filters; 13 | 14 | use Streaming\StreamInterface; 15 | 16 | class HLSFilterV2 extends FormatFilter 17 | { 18 | /** 19 | * This is a new version of HLSFilter that the master playlist will be created by FFmpeg 20 | * 21 | * 22 | * 23 | * ffmpeg ^ 24 | * -i path/to/video ^ 25 | * -i path/to/video ^ 26 | * -i path/to/video ^ 27 | * -c:v libx264 -c:a copy ^ 28 | * -s:v:0 1920x1080 -b:v:0 4096k -s:v:1 1280x720 -b:v:1 2048k -s:v:2 854x480 -b:v:2 750k ^ 29 | * -map 0:a -map 0:v -map 1:v -map 2:v ^ 30 | * -var_stream_map "a:0,agroup:audio v:0,agroup:audio v:1,agroup:audio v:2,agroup:audio" ^ 31 | * -f hls -hls_segment_type mpegts -hls_list_size 0 -hls_time 5 -hls_allow_cache 0 -master_pl_name master-playlist.m3u8 -y playlist%v.m3u8 32 | * 33 | * 34 | * 35 | * */ 36 | 37 | /** 38 | * @param StreamInterface $stream 39 | * @return mixed 40 | */ 41 | public function streamFilter(StreamInterface $stream): void 42 | { 43 | // TODO: Implement streamFilter() method. 44 | // add Mapping audio and video stream #60 45 | } 46 | } -------------------------------------------------------------------------------- /tests/MediaTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\DASH; 15 | use Streaming\HLS; 16 | use Streaming\Media; 17 | use FFMpeg\FFProbe\DataMapping\Stream; 18 | 19 | class MediaTest extends TestCase 20 | { 21 | public function testMediaClass() 22 | { 23 | $media = $this->getVideo(); 24 | $this->assertInstanceOf(Media::class, $media); 25 | } 26 | 27 | public function testDASH() 28 | { 29 | $this->assertInstanceOf(DASH::class, $this->getDASH()); 30 | } 31 | 32 | public function testHLS() 33 | { 34 | $this->assertInstanceOf(HLS::class, $this->getHLS()); 35 | } 36 | 37 | public function testGetPath() 38 | { 39 | $media = $this->getVideo(); 40 | $get_path_info = pathinfo($media->getPathfile()); 41 | 42 | $this->assertIsArray($get_path_info); 43 | $this->assertArrayHasKey('dirname', $get_path_info); 44 | $this->assertArrayHasKey('filename', $get_path_info); 45 | } 46 | 47 | private function getDASH() 48 | { 49 | $media = $this->getVideo(); 50 | return $media->DASH(); 51 | } 52 | 53 | private function getHLS() 54 | { 55 | $media = $this->getVideo(); 56 | return $media->HLS(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Streaming.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use Streaming\Traits\Representations; 15 | 16 | 17 | abstract class Streaming extends Stream 18 | { 19 | use Representations; 20 | 21 | /** @var string */ 22 | private $strict = "-2"; 23 | 24 | /** @var array */ 25 | private $additional_params = []; 26 | 27 | /** 28 | * Streaming constructor. 29 | * @param Media $media 30 | */ 31 | public function __construct(Media $media) 32 | { 33 | $this->reps = new RepsCollection(); 34 | parent::__construct($media); 35 | } 36 | /** 37 | * @return array 38 | */ 39 | public function getAdditionalParams(): array 40 | { 41 | return $this->additional_params; 42 | } 43 | 44 | /** 45 | * @param array $additional_params 46 | * @return Stream 47 | */ 48 | public function setAdditionalParams(array $additional_params) 49 | { 50 | $this->additional_params = $additional_params; 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param string $strict 56 | * @return Stream 57 | */ 58 | public function setStrict(string $strict): Stream 59 | { 60 | $this->strict = $strict; 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getStrict(): string 68 | { 69 | return $this->strict; 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /tests/DASHFiltersTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\DASH; 15 | use Streaming\Filters\DASHFilter; 16 | use Streaming\Filters\StreamFilter; 17 | 18 | class DASHFiltersTest extends TestCase 19 | { 20 | public function testFilterClass() 21 | { 22 | $this->assertInstanceOf(StreamFilter::class, $this->getFilter()); 23 | } 24 | 25 | public function testGetApply() 26 | { 27 | $apply = $this->getFilter()->apply(); 28 | 29 | $this->assertIsArray($apply); 30 | 31 | $this->assertEquals( 32 | [ 33 | "-c:v", "libx265", "-c:a", "aac", "-keyint_min", "25", "-g", "250", "-sc_threshold", "40", 34 | "-use_timeline", "1", "-use_template", "1", "-init_seg_name", "test_init_\$RepresentationID$.\$ext$", 35 | "-media_seg_name", "test_chunk_\$RepresentationID\$_\$Number%05d$.\$ext$", "-seg_duration", "10", 36 | "-hls_playlist", "0", "-f", "dash", "-adaptation_sets", "id=0,streams=v id=1,streams=a", "-map", "0", 37 | "-s:v:0", "256x144", "-b:v:0", "103k", "-map", "0", "-s:v:1", "426x240", "-b:v:1", "138k", "-map", "0", 38 | "-s:v:2", "640x360", "-b:v:2", "207k", "-strict", "-2" 39 | ], 40 | $apply); 41 | } 42 | 43 | private function getFilter() 44 | { 45 | return new DASHFilter($this->getDASH()); 46 | } 47 | 48 | private function getDASH() 49 | { 50 | $hls = new DASH($this->getVideo()); 51 | 52 | return $hls->HEVC() 53 | ->autoGenerateRepresentations() 54 | ->setAdaption('id=0,streams=v id=1,streams=a'); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Clouds/Cloud.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Clouds; 14 | 15 | 16 | use Streaming\Exception\InvalidArgumentException; 17 | use Streaming\File; 18 | 19 | class Cloud 20 | { 21 | /** 22 | * @param array $clouds 23 | * @param string $tmp_dir 24 | */ 25 | public static function uploadDirectory(array $clouds, string $tmp_dir): void 26 | { 27 | if (isset($clouds['cloud'])) { 28 | $clouds = [$clouds]; 29 | } 30 | 31 | foreach ($clouds as $cloud) { 32 | static::transfer($cloud, __FUNCTION__, $tmp_dir); 33 | } 34 | } 35 | 36 | /** 37 | * @param array $cloud 38 | * @param string|null $save_to 39 | * @return array 40 | */ 41 | public static function download(array $cloud, string $save_to = null): array 42 | { 43 | $prefix = $cloud['options']['Key'] ?? $cloud['options']['object_name'] ?? $cloud['options']['blob'] ?? uniqid('stream_', true); 44 | list($save_to, $is_tmp) = $save_to ? [$save_to, false] : [File::tmp($prefix), true]; 45 | static::transfer($cloud, __FUNCTION__, $save_to); 46 | 47 | return [$save_to, $is_tmp]; 48 | } 49 | 50 | /** 51 | * @param $cloud_c 52 | * @param $method 53 | * @param $path 54 | */ 55 | private static function transfer(array $cloud_c, string $method, string $path): void 56 | { 57 | extract($cloud_c); 58 | if (isset($cloud) && $cloud instanceof CloudInterface) { 59 | call_user_func_array([$cloud, $method], [$path, $options ?? []]); 60 | } else { 61 | throw new InvalidArgumentException('The cloud key must be instance of the CloudInterface'); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Format/VP9.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Format; 13 | 14 | /** 15 | * The VP9 video format 16 | */ 17 | final class VP9 extends StreamFormat 18 | { 19 | private const MODULUS = 2; 20 | 21 | /** 22 | * VP9 constructor. 23 | * @param string $video_codec 24 | * @param string|null $audio_codec 25 | * @param bool $default_init_opts 26 | */ 27 | public function __construct(string $video_codec = 'libvpx-vp9', string $audio_codec = 'aac', bool $default_init_opts = true) 28 | { 29 | $this 30 | ->setVideoCodec($video_codec) 31 | ->setAudioCodec($audio_codec); 32 | 33 | /** 34 | * set the default value of h265 codec options 35 | * see https://ffmpeg.org/ffmpeg-codecs.html#Options-26 for more information about options 36 | */ 37 | if ($default_init_opts) { 38 | //@TODO: add default vp9 39 | } 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getAvailableAudioCodecs() 46 | { 47 | return ['libvorbis']; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | public function getAvailableVideoCodecs(): array 54 | { 55 | return ['libvpx', 'libvpx-vp9']; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getModulus() 62 | { 63 | return static::MODULUS; 64 | } 65 | 66 | /** 67 | * Returns true if the current format supports B-Frames. 68 | * 69 | * @see https://wikipedia.org/wiki/Video_compression_picture_types 70 | * 71 | * @return Boolean 72 | */ 73 | public function supportBFrames() 74 | { 75 | return true; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Traits/Representations.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Traits; 13 | 14 | use Streaming\AutoReps; 15 | use Streaming\Exception\InvalidArgumentException; 16 | use Streaming\RepresentationInterface; 17 | use Streaming\RepsCollection; 18 | 19 | trait Representations 20 | { 21 | /** @var RepsCollection */ 22 | protected $reps; 23 | 24 | /** 25 | * add a representation 26 | * @param RepresentationInterface $rep 27 | * @return $this 28 | */ 29 | public function addRepresentation(RepresentationInterface $rep) 30 | { 31 | $this->reps->add($rep); 32 | return $this; 33 | } 34 | 35 | /** 36 | * add representations using an array 37 | * @param array $reps 38 | * @return $this 39 | */ 40 | public function addRepresentations(array $reps) 41 | { 42 | array_walk($reps, [$this, 'addRepresentation']); 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param array|null $sides 48 | * @param array|null $k_bitrate 49 | * @param bool $acceding_order 50 | * @return $this 51 | */ 52 | public function autoGenerateRepresentations(array $sides = null, array $k_bitrate = null, bool $acceding_order = true) 53 | { 54 | if (!$this->format) { 55 | throw new InvalidArgumentException('First you must set the format of the video'); 56 | } 57 | 58 | $reps = new AutoReps($this->getMedia(), $this->getFormat(), $sides, $k_bitrate); 59 | $reps->sort($acceding_order); 60 | 61 | foreach ($reps as $rep) { 62 | $this->addRepresentation($rep); 63 | } 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @return RepsCollection 70 | */ 71 | public function getRepresentations(): RepsCollection 72 | { 73 | return $this->reps; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Capture.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use Streaming\Exception\RuntimeException; 17 | 18 | class Capture 19 | { 20 | /** 21 | * @var string 22 | */ 23 | private $video; 24 | /** 25 | * @var string|null 26 | */ 27 | private $audio; 28 | /** 29 | * @var bool 30 | */ 31 | private $screen; 32 | 33 | /** 34 | * Camera constructor. 35 | * @param string $video 36 | * @param string|null $audio 37 | * @param bool $screen 38 | */ 39 | public function __construct(string $video, string $audio = null, $screen = false) 40 | { 41 | $this->video = $video; 42 | $this->audio = $audio; 43 | $this->screen = $screen; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function linux(): array 50 | { 51 | return [$this->video, ['f' => $this->screen ? 'x11grab' : 'v4l2']]; 52 | } 53 | 54 | /** 55 | * @return array 56 | */ 57 | public function windows(): array 58 | { 59 | $path = "video=$this->video"; 60 | if (!is_null($this->audio)) { 61 | $path .= ":audio=$this->audio"; 62 | } 63 | 64 | return [$path, ['f' => 'dshow']]; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function osX(): array 71 | { 72 | return [$this->video, ['f' => 'avfoundation']]; 73 | } 74 | 75 | /** 76 | * @throw Runtime exception 77 | */ 78 | public function unknown() 79 | { 80 | throw new RuntimeException("Unknown operating system! It cannot run the camera on this platform"); 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function getOptions(): array 87 | { 88 | return call_user_func([$this, Utiles::getOS()]); 89 | } 90 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aminyazdanpanah/php-ffmpeg-video-streaming", 3 | "description": "📼 PHP FFMpeg - Video Streaming - DASH, HLS http://video.aminyazdanpanah.com", 4 | "type": "library", 5 | "keywords": [ 6 | "dash", 7 | "mpeg-dash", 8 | "hls", 9 | "video", 10 | "mp4", 11 | "video-streaming", 12 | "adaptive-http-streaming", 13 | "streaming", 14 | "video-streaming", 15 | "live-streaming", 16 | "http-liv-streaming", 17 | "dynamic-adaptive-streaming-over-http", 18 | "cloud", 19 | "clouds", 20 | "google-cloud-storage", 21 | "s3", 22 | "amazons3", 23 | "microsoft-azure", 24 | "encoding", 25 | "mpd", 26 | "m3u8", 27 | "encryption", 28 | "drm", 29 | "key-rotation", 30 | "clearkey" 31 | ], 32 | "homepage": "https://github.com/aminyazdanpanah/PHP-FFmpeg-video-streaming", 33 | "license": "MIT", 34 | "authors": [ 35 | { 36 | "name": "Amin Yazdanpanah", 37 | "email": "github@aminyazdanpanah.com", 38 | "homepage": "https://www.aminyazdanpanah.com" 39 | } 40 | ], 41 | "minimum-stability": "dev", 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "autoload": { 46 | "files": [ 47 | "src/helpers.php" 48 | ], 49 | "psr-4": { 50 | "Streaming\\": "src" 51 | } 52 | }, 53 | "require-dev": { 54 | "phpunit/phpunit": "^8.4", 55 | "aws/aws-sdk-php": "^3.0@dev", 56 | "google/cloud-storage": "dev-main", 57 | "microsoft/azure-storage-blob": "dev-master" 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "Tests\\FFMpegStreaming\\": "tests" 62 | } 63 | }, 64 | "require": { 65 | "php": "^7.2 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4", 66 | "php-ffmpeg/php-ffmpeg": "^0.15 || 0.16 || 0.17 || 0.18 || 0.19 || ^1.0 || ^1.1 || ^1.2 || ^1.3", 67 | "symfony/filesystem": "4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Format/HEVC.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Format; 13 | 14 | final class HEVC extends StreamFormat 15 | { 16 | private const MODULUS = 2; 17 | 18 | /** 19 | * HEVC constructor. 20 | * @param string $video_codec 21 | * @param string|null $audio_codec 22 | * @param bool $default_init_opts 23 | */ 24 | public function __construct(string $video_codec = 'libx265', string $audio_codec = 'aac', bool $default_init_opts = true) 25 | { 26 | $this 27 | ->setVideoCodec($video_codec) 28 | ->setAudioCodec($audio_codec); 29 | 30 | /** 31 | * set the default value of h265 codec options 32 | * see https://ffmpeg.org/ffmpeg-codecs.html#Options-29 for more information about options 33 | */ 34 | if ($default_init_opts) { 35 | $this->setAdditionalParameters([ 36 | 'keyint_min' => 25, 37 | 'g' => 250, 38 | 'sc_threshold' => 40 39 | ]); 40 | } 41 | } 42 | 43 | /** 44 | * Returns the list of available audio codecs for this format. 45 | * 46 | * @return array 47 | */ 48 | public function getAvailableAudioCodecs() 49 | { 50 | return ['aac', 'libvo_aacenc', 'libfaac', 'libmp3lame', 'libfdk_aac']; 51 | } 52 | 53 | public function getAvailableVideoCodecs(): array 54 | { 55 | return ['libx265', 'h265', 'hevc_nvenc']; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getModulus() 62 | { 63 | return static::MODULUS; 64 | } 65 | 66 | /** 67 | * Returns true if the current format supports B-Frames. 68 | * 69 | * @see https://wikipedia.org/wiki/Video_compression_picture_types 70 | * 71 | * @return Boolean 72 | */ 73 | public function supportBFrames() 74 | { 75 | return true; 76 | } 77 | } -------------------------------------------------------------------------------- /src/Format/X264.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Format; 13 | 14 | final class X264 extends StreamFormat 15 | { 16 | private const MODULUS = 2; 17 | 18 | /** 19 | * X264 constructor. 20 | * @param string $video_codec 21 | * @param string $audio_codec 22 | * @param bool $default_init_opts 23 | */ 24 | public function __construct($video_codec = 'libx264', string $audio_codec = 'aac', bool $default_init_opts = true) 25 | { 26 | $this 27 | ->setVideoCodec($video_codec) 28 | ->setAudioCodec($audio_codec); 29 | 30 | /** 31 | * set the default value of h264 codec options 32 | * see https://ffmpeg.org/ffmpeg-codecs.html#Options-28 for more information about options 33 | * return array 34 | */ 35 | if ($default_init_opts) { 36 | $this->setAdditionalParameters([ 37 | 'bf' => 1, 38 | 'keyint_min' => 25, 39 | 'g' => 250, 40 | 'sc_threshold' => 40 41 | ]); 42 | } 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function getAvailableAudioCodecs() 49 | { 50 | return ['aac', 'libvo_aacenc', 'libfaac', 'libmp3lame', 'libfdk_aac']; 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | public function getAvailableVideoCodecs() 57 | { 58 | return ['libx264', 'h264', 'h264_afm', 'h264_nvenc']; 59 | } 60 | 61 | /** 62 | * @return int 63 | */ 64 | public function getModulus() 65 | { 66 | return static::MODULUS; 67 | } 68 | 69 | /** 70 | * Returns true if the current format supports B-Frames. 71 | * 72 | * @see https://wikipedia.org/wiki/Video_compression_picture_types 73 | * 74 | * @return Boolean 75 | */ 76 | public function supportBFrames() 77 | { 78 | return true; 79 | } 80 | } -------------------------------------------------------------------------------- /src/Utiles.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | class Utiles 17 | { 18 | /** 19 | * @param string $str 20 | * @return string 21 | */ 22 | public static function appendSlash(string $str): string 23 | { 24 | return $str ? rtrim($str, '/') . "/" : $str; 25 | } 26 | 27 | /** 28 | * @param array $array 29 | * @param string $glue 30 | */ 31 | public static function concatKeyValue(array &$array, string $glue = ""): void 32 | { 33 | array_walk($array, function (&$value, $key) use ($glue) { 34 | $value = "$key$glue$value"; 35 | }); 36 | } 37 | 38 | /** 39 | * @param array $array 40 | * @param string $start_with 41 | * @return array 42 | */ 43 | public static function arrayToFFmpegOpt(array $array, string $start_with = "-"): array 44 | { 45 | $new = []; 46 | foreach ($array as $key => $value) { 47 | if(is_string($key)){ 48 | array_push($new, $start_with . $key, $value); 49 | }else{ 50 | $new = null; 51 | break; 52 | } 53 | } 54 | 55 | return $new ?? $array; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public static function getOS(): string 62 | { 63 | switch (true) { 64 | case stristr(PHP_OS, 'DAR'): 65 | return "osX"; 66 | case stristr(PHP_OS, 'WIN'): 67 | return "windows"; 68 | case stristr(PHP_OS, 'LINUX'): 69 | return "linux"; 70 | default : 71 | return "unknown"; 72 | } 73 | } 74 | 75 | /** 76 | * @param bool $isAutoSelect 77 | * @return string 78 | */ 79 | public static function convertBooleanToYesNo(bool $isAutoSelect): string 80 | { 81 | return $isAutoSelect ? "YES" : "NO"; 82 | } 83 | } -------------------------------------------------------------------------------- /src/RepresentationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use FFMpeg\Coordinate\AspectRatio; 17 | use FFMpeg\Coordinate\Dimension; 18 | 19 | interface RepresentationInterface 20 | { 21 | /** 22 | * @return string 23 | */ 24 | public function size2string(): ?string; 25 | 26 | /** 27 | * @param $width 28 | * @param $height 29 | * @return Representation 30 | */ 31 | public function setResize(int $width, int $height): Representation; 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getKiloBitrate(): int; 37 | 38 | /** 39 | * @return int|null 40 | */ 41 | public function getAudioKiloBitrate(); 42 | 43 | /** 44 | * Sets the video kiloBitrate value. 45 | * 46 | * @param integer $kiloBitrate 47 | * @return Representation 48 | */ 49 | public function setKiloBitrate(int $kiloBitrate): Representation; 50 | 51 | /** 52 | * Sets the video kiloBitrate value. 53 | * 54 | * @param integer $audioKiloBitrate 55 | * @return Representation 56 | */ 57 | public function setAudioKiloBitrate(int $audioKiloBitrate): Representation; 58 | 59 | /** 60 | * @return int 61 | */ 62 | public function getWidth(): int; 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function getHeight(): int; 68 | 69 | /** 70 | * @param array $hls_stream_info 71 | * @return Representation 72 | */ 73 | public function setHlsStreamInfo(array $hls_stream_info): Representation; 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getHlsStreamInfo(): array; 79 | 80 | /** 81 | * @param Dimension $size 82 | * @return Representation 83 | */ 84 | public function setSize(Dimension $size): Representation; 85 | 86 | /** 87 | * @return AspectRatio 88 | */ 89 | public function getRatio(): AspectRatio; 90 | } -------------------------------------------------------------------------------- /src/CommandBuilder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use FFMpeg\Format\VideoInterface; 17 | 18 | class CommandBuilder 19 | { 20 | /** @var Media */ 21 | private $media; 22 | 23 | /** @var \FFMpeg\Filters\FiltersCollection */ 24 | private $filters; 25 | 26 | /** @var \FFMpeg\Driver\FFMpegDriver */ 27 | private $driver; 28 | 29 | /** @var VideoInterface */ 30 | private $format; 31 | 32 | /** 33 | * CommandBuilder constructor. 34 | * @param Media $media 35 | * @param VideoInterface $format 36 | */ 37 | public function __construct(Media $media, VideoInterface $format) 38 | { 39 | $this->media = $media; 40 | $this->filters = $this->media->getFiltersCollection(); 41 | $this->driver = $this->media->getFFMpegDriver(); 42 | $this->format = $format; 43 | } 44 | 45 | /** 46 | * @param VideoInterface $format 47 | * @param string $path 48 | * @return array 49 | * @TODO: optimize this function 50 | */ 51 | public function build(VideoInterface $format, string $path): array 52 | { 53 | $commands = []; 54 | 55 | foreach ($this->filters as $filter) { 56 | $commands = array_merge($this->getInputOptions(), $filter->apply($this->media->baseMedia(), $format)); 57 | } 58 | 59 | if ($this->driver->getConfiguration()->has('ffmpeg.threads')) { 60 | $commands = array_merge($commands, ['-threads', $this->driver->getConfiguration()->get('ffmpeg.threads')]); 61 | } 62 | 63 | array_push($commands, $path); 64 | 65 | return $commands; 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | private function getInputOptions(): array 72 | { 73 | $path = $this->media->getPathfile(); 74 | $input_options = Utiles::arrayToFFmpegOpt($this->media->getInputOptions()); 75 | 76 | return array_merge($input_options, $this->format->getInitialParameters() ?? [], ['-y', '-i', $path]); 77 | } 78 | } -------------------------------------------------------------------------------- /src/DASH.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use Streaming\Filters\DASHFilter; 15 | use Streaming\Filters\StreamFilterInterface; 16 | 17 | class DASH extends Streaming 18 | { 19 | /** @var string */ 20 | private $adaption; 21 | 22 | /** @var string */ 23 | private $seg_duration = 10; 24 | 25 | /** @var bool */ 26 | private $generate_hls_playlist = false; 27 | 28 | /** 29 | * @return mixed 30 | */ 31 | public function getAdaption() 32 | { 33 | return $this->adaption; 34 | } 35 | 36 | /** 37 | * @param mixed $adaption 38 | * @return DASH 39 | */ 40 | public function setAdaption(string $adaption): DASH 41 | { 42 | $this->adaption = $adaption; 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param string $seg_duration 48 | * @return DASH 49 | */ 50 | public function setSegDuration(string $seg_duration): DASH 51 | { 52 | $this->seg_duration = $seg_duration; 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function getSegDuration(): string 60 | { 61 | return $this->seg_duration; 62 | } 63 | 64 | /** 65 | * @param bool $generate_hls_playlist 66 | * @return DASH 67 | */ 68 | public function generateHlsPlaylist(bool $generate_hls_playlist = true): DASH 69 | { 70 | $this->generate_hls_playlist = $generate_hls_playlist; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return bool 76 | */ 77 | public function isGenerateHlsPlaylist(): bool 78 | { 79 | return $this->generate_hls_playlist; 80 | } 81 | 82 | /** 83 | * @return DASHFilter 84 | */ 85 | protected function getFilter(): StreamFilterInterface 86 | { 87 | return new DASHFilter($this); 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | protected function getPath(): string 94 | { 95 | return implode(".", [$this->getFilePath(), "mpd"]); 96 | } 97 | } -------------------------------------------------------------------------------- /src/Traits/Formats.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Traits; 13 | 14 | use FFMpeg\Format\VideoInterface; 15 | use Streaming\Exception\InvalidArgumentException; 16 | use Streaming\Format\HEVC; 17 | use Streaming\Format\StreamFormat; 18 | use Streaming\Format\VP9; 19 | use Streaming\Format\X264; 20 | 21 | trait Formats 22 | { 23 | /** @var VideoInterface | \FFMpeg\Format\Video\DefaultVideo */ 24 | protected $format; 25 | 26 | /** 27 | * @param string $video_codec 28 | * @param string|null $audio_codec 29 | * @param bool $default_init_opts 30 | * @return $this 31 | */ 32 | public function x264(string $video_codec = 'libx264', string $audio_codec = 'aac', bool $default_init_opts = true) 33 | { 34 | $this->setFormat(new X264($video_codec, $audio_codec, $default_init_opts)); 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param string $video_codec 40 | * @param string|null $audio_codec 41 | * @param bool $default_init_opts 42 | * @return $this 43 | */ 44 | public function hevc(string $video_codec = 'libx265', string $audio_codec = 'aac', bool $default_init_opts = true) 45 | { 46 | $this->setFormat(new HEVC($video_codec, $audio_codec, $default_init_opts)); 47 | return $this; 48 | } 49 | 50 | /** 51 | * @param string $video_codec 52 | * @param string|null $audio_codec 53 | * @param bool $default_init_opts 54 | * @return $this 55 | */ 56 | public function vp9(string $video_codec = 'libvpx-vp9', string $audio_codec = 'aac', bool $default_init_opts = true) 57 | { 58 | $this->setFormat(new VP9($video_codec, $audio_codec, $default_init_opts)); 59 | return $this; 60 | } 61 | 62 | /** 63 | * @return VideoInterface 64 | */ 65 | public function getFormat(): VideoInterface 66 | { 67 | return $this->format; 68 | } 69 | 70 | /** 71 | * @param mixed $format 72 | * @return $this 73 | * @throws InvalidArgumentException 74 | */ 75 | public function setFormat(StreamFormat $format) 76 | { 77 | $this->format = $format; 78 | return $this; 79 | } 80 | } -------------------------------------------------------------------------------- /tests/DASHTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\DASH; 15 | use Streaming\Stream; 16 | use Streaming\Format\StreamFormat; 17 | use Streaming\Representation; 18 | 19 | class DASHTest extends TestCase 20 | { 21 | public function testDASHClass() 22 | { 23 | $this->assertInstanceOf(Stream::class, $this->getDASH()); 24 | } 25 | 26 | public function testFormat() 27 | { 28 | $dash = $this->getDASH(); 29 | $dash->HEVC(); 30 | 31 | $this->assertInstanceOf(StreamFormat::class, $dash->getFormat()); 32 | } 33 | 34 | public function testAutoRepresentations() 35 | { 36 | $dash = $this->getDASH(); 37 | $dash->HEVC() 38 | ->autoGenerateRepresentations(); 39 | $representations = $dash->getRepresentations()->all(); 40 | 41 | $this->assertIsArray($representations); 42 | $this->assertInstanceOf(Representation::class, current($representations)); 43 | $this->assertEquals('256x144', $representations[0]->size2string()); 44 | $this->assertEquals('426x240', $representations[1]->size2string()); 45 | $this->assertEquals('640x360', $representations[2]->size2string()); 46 | 47 | $this->assertEquals(103, $representations[0]->getKiloBitrate()); 48 | $this->assertEquals(138, $representations[1]->getKiloBitrate()); 49 | $this->assertEquals(207, $representations[2]->getKiloBitrate()); 50 | } 51 | 52 | public function testSet() 53 | { 54 | $dash = $this->getDASH(); 55 | $dash->setAdaption('test-adaption'); 56 | 57 | $this->assertEquals('test-adaption', $dash->getAdaption()); 58 | } 59 | 60 | public function testSave() 61 | { 62 | $dash = $this->getDASH(); 63 | $export_class = $dash->HEVC() 64 | ->autoGenerateRepresentations() 65 | ->save($this->srcDir . '/dash/test.mpd'); 66 | 67 | 68 | $this->assertFileExists($this->srcDir . '/dash/test.mpd'); 69 | $this->assertInstanceOf(Stream::class, $export_class); 70 | } 71 | 72 | private function getDASH() 73 | { 74 | return new DASH($this->getVideo()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Clouds/S3.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Clouds; 14 | 15 | 16 | use Streaming\Exception\InvalidArgumentException; 17 | use Streaming\Exception\RuntimeException; 18 | use Streaming\File; 19 | 20 | class S3 implements CloudInterface 21 | { 22 | /** @var \Aws\S3\S3Client*/ 23 | private $s3; 24 | 25 | /** 26 | * AWS constructor. 27 | * @param $config 28 | */ 29 | public function __construct(array $config) 30 | { 31 | if(!class_exists('\Aws\S3\S3Client')){ 32 | throw new RuntimeException('Aws\S3\S3Client not found. make sure the package is installed: composer require aws/aws-sdk-php'); 33 | } 34 | 35 | $this->s3 = new \Aws\S3\S3Client($config);; 36 | } 37 | 38 | /** 39 | * @param string $dir 40 | * @param array $options 41 | */ 42 | public function uploadDirectory(string $dir, array $options): void 43 | { 44 | if(!isset($options['dest'])){ 45 | throw new InvalidArgumentException("You should set the dest in the array"); 46 | } 47 | $dest = $options['dest']; 48 | unset($options['dest'], $options['filename']); 49 | 50 | try { 51 | (new \Aws\S3\Transfer($this->s3, $dir, $dest, $options))->transfer(); 52 | } catch (\Aws\S3\Exception\S3Exception $e) { 53 | throw new RuntimeException("An error occurred while uploading files: " . $e->getMessage(), $e->getCode(), $e); 54 | } 55 | } 56 | 57 | /** 58 | * @param string $save_to 59 | * @param array $options 60 | * @throws RuntimeException 61 | */ 62 | public function download(string $save_to, array $options): void 63 | { 64 | try { 65 | $file = $this->s3->getObject($options); 66 | if ($file['ContentLength'] > 0 && !empty($file['ContentType'])) { 67 | File::put($save_to, $file->get('Body')); 68 | } else { 69 | throw new RuntimeException("File not found!"); 70 | } 71 | } catch (\Aws\S3\Exception\S3Exception $e) { 72 | throw new RuntimeException("An error occurred while downloading the file: " . $e->getMessage(), $e->getCode(), $e); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 44 | 45 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 46 | 47 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 48 | 49 | **Happy coding**! 50 | -------------------------------------------------------------------------------- /src/RepsCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | class RepsCollection implements \Countable, \IteratorAggregate 17 | { 18 | /** @return array */ 19 | private $representations = []; 20 | 21 | /** 22 | * RepsCollection constructor. 23 | * @param array $representations 24 | */ 25 | public function __construct(array $representations = []) 26 | { 27 | array_walk($representations, [$this, 'add']); 28 | } 29 | 30 | /** 31 | * @return null|Representation 32 | */ 33 | public function first() 34 | { 35 | return reset($this->representations) ?: null; 36 | } 37 | 38 | /** 39 | * @return null|Representation 40 | */ 41 | public function end() 42 | { 43 | return end($this->representations) ?: null; 44 | } 45 | 46 | /** 47 | * @param RepresentationInterface $representation 48 | * @return RepsCollection 49 | */ 50 | public function add(RepresentationInterface $representation) 51 | { 52 | array_push($this->representations, $representation); 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function all(): array 60 | { 61 | return $this->representations; 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function keys(): array 68 | { 69 | return array_keys($this->representations); 70 | } 71 | 72 | /** 73 | * @param $key 74 | * @param null $default 75 | * @return Representation | mixed 76 | */ 77 | public function get($key, $default = null) 78 | { 79 | return $this->representations[$key] ?? $default; 80 | } 81 | 82 | /** 83 | * @param callable $func 84 | */ 85 | public function map(callable $func) 86 | { 87 | $this->representations = array_map($func, $this->representations); 88 | } 89 | 90 | /** 91 | * count of representations 92 | */ 93 | public function count(): int 94 | { 95 | return count($this->representations); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function getIterator(): \Traversable 102 | { 103 | return new \ArrayIterator($this->representations); 104 | } 105 | } -------------------------------------------------------------------------------- /src/Media.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use FFMpeg\Media\MediaTypeInterface; 15 | use FFMpeg\Media\Video; 16 | 17 | 18 | /** @mixin \FFMpeg\Media\Video */ 19 | class Media 20 | { 21 | /** @var \FFMpeg\Media\Video */ 22 | private $media; 23 | 24 | /** @var bool */ 25 | private $is_tmp; 26 | 27 | /** @var array */ 28 | private $input_options; 29 | 30 | /** 31 | * Media constructor. 32 | * @param MediaTypeInterface $media 33 | * @param bool $is_tmp 34 | * @param array $input_options 35 | */ 36 | public function __construct(MediaTypeInterface $media, bool $is_tmp, array $input_options = []) 37 | { 38 | $this->media = $media; 39 | $this->is_tmp = $is_tmp; 40 | $this->input_options = $input_options; 41 | } 42 | 43 | /** 44 | * @return DASH 45 | */ 46 | public function dash(): DASH 47 | { 48 | return new DASH($this); 49 | } 50 | 51 | /** 52 | * @return HLS 53 | */ 54 | public function hls(): HLS 55 | { 56 | return new HLS($this); 57 | } 58 | 59 | /** 60 | * @return StreamToFile 61 | */ 62 | public function stream2file(): StreamToFile 63 | { 64 | return new StreamToFile($this); 65 | } 66 | 67 | /** 68 | * @return bool 69 | */ 70 | public function isTmp(): bool 71 | { 72 | return $this->is_tmp; 73 | } 74 | 75 | /** 76 | * @return Video | \FFMpeg\Media\Audio 77 | */ 78 | public function baseMedia(): MediaTypeInterface 79 | { 80 | return $this->media; 81 | } 82 | 83 | /** 84 | * @param $argument 85 | * @return Media | \FFMpeg\Media\Video 86 | */ 87 | private function isInstanceofArgument($argument) 88 | { 89 | return ($argument instanceof $this->media) ? $this : $argument; 90 | } 91 | 92 | /** 93 | * @param $method 94 | * @param $parameters 95 | * @return Media | \FFMpeg\Media\Video 96 | */ 97 | public function __call($method, $parameters) 98 | { 99 | return $this->isInstanceofArgument( 100 | call_user_func_array([$this->media, $method], $parameters) 101 | ); 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | public function getInputOptions(): array 108 | { 109 | return $this->input_options; 110 | } 111 | } -------------------------------------------------------------------------------- /src/Clouds/MicrosoftAzure.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Clouds; 14 | 15 | 16 | use Streaming\Exception\InvalidArgumentException; 17 | use Streaming\Exception\RuntimeException; 18 | use Streaming\File; 19 | 20 | class MicrosoftAzure implements CloudInterface 21 | { 22 | /** @var \MicrosoftAzure\Storage\Blob\BlobRestProxy*/ 23 | private $blobClient; 24 | 25 | /** 26 | * MicrosoftAzure constructor. 27 | * @param $connection 28 | * @param array $options 29 | */ 30 | public function __construct($connection, array $options = []) 31 | { 32 | if(!class_exists('\MicrosoftAzure\Storage\Blob\BlobRestProxy')){ 33 | throw new RuntimeException('MicrosoftAzure\Storage\Blob\BlobRestProxy not found. make sure the package is installed: composer require microsoft/azure-storage-blob'); 34 | } 35 | 36 | $this->blobClient = \MicrosoftAzure\Storage\Blob\BlobRestProxy::createBlobService($connection, $options); 37 | } 38 | 39 | /** 40 | * Upload a entire directory to a cloud 41 | * @param string $dir 42 | * @param array $options 43 | */ 44 | public function uploadDirectory(string $dir, array $options): void 45 | { 46 | if(!isset($options['container'])){ 47 | throw new InvalidArgumentException("You should set the container in the array"); 48 | } 49 | 50 | unset($options['filename']); 51 | 52 | try { 53 | foreach (scandir($dir) as $filename) { 54 | $path = $dir . DIRECTORY_SEPARATOR . $filename; 55 | 56 | if (is_file($path)) { 57 | $this->blobClient->createBlockBlob($options['container'], $filename, fopen($path, "r"), $options['CreateBlockBlobOptions'] ?? null); 58 | } 59 | } 60 | } catch (\MicrosoftAzure\Storage\Common\Exceptions\ServiceException $e) { 61 | throw new RuntimeException(sprintf("An error occurred while uploading files: %s", $e->getMessage()), $e->getCode(), $e); 62 | } 63 | } 64 | 65 | /** 66 | * Download a file from a cloud 67 | * @param string $save_to 68 | * @param array $options 69 | */ 70 | public function download(string $save_to, array $options): void 71 | { 72 | if(!isset($options['container']) || !isset($options['blob'])){ 73 | throw new InvalidArgumentException("You should set the container and blob in the array"); 74 | } 75 | 76 | try { 77 | $blob = $this->blobClient->getBlob($options['container'], $options['blob']); 78 | File::put($save_to, $blob->getContentStream()); 79 | } catch (\MicrosoftAzure\Storage\Common\Exceptions\ServiceException $e) { 80 | throw new RuntimeException(sprintf("An error occurred while downloading the file: %s", $e->getMessage()), $e->getCode(), $e); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/FFMpeg.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use FFMpeg\Exception\ExceptionInterface; 15 | use FFMpeg\FFMpeg as BFFMpeg; 16 | use FFMpeg\FFProbe; 17 | use FFMpeg\Media\Video; 18 | use Psr\Log\LoggerInterface; 19 | use Streaming\Clouds\Cloud; 20 | use Streaming\Exception\RuntimeException; 21 | 22 | 23 | /** @mixin BFFMpeg*/ 24 | class FFMpeg 25 | { 26 | /** @var BFFMpeg */ 27 | private $ffmpeg; 28 | 29 | /** 30 | * @param $ffmpeg 31 | */ 32 | public function __construct(BFFMpeg $ffmpeg) 33 | { 34 | $this->ffmpeg = $ffmpeg; 35 | } 36 | 37 | /** 38 | * @param string $path 39 | * @param bool $is_tmp 40 | * @return Media 41 | */ 42 | public function open(string $path, bool $is_tmp = false): Media 43 | { 44 | try { 45 | return new Media($this->ffmpeg->open($path), $is_tmp); 46 | } catch (ExceptionInterface $e) { 47 | if ($is_tmp) { 48 | sleep(.5); 49 | File::remove($path); 50 | } 51 | throw new RuntimeException("An error occurred while opening the file: " . $e->getMessage(), $e->getCode(), $e); 52 | } 53 | } 54 | 55 | /** 56 | * @param array $cloud 57 | * @param string|null $save_to 58 | * @return Media 59 | */ 60 | public function openFromCloud(array $cloud, string $save_to = null): Media 61 | { 62 | return call_user_func_array([$this, 'open'], Cloud::download($cloud, $save_to)); 63 | } 64 | 65 | /** 66 | * @param string $video 67 | * @param string|null $audio 68 | * @param array $options 69 | * @param bool $screen 70 | * @return Media 71 | */ 72 | public function capture(string $video, string $audio = null, array $options = [], $screen = false): Media 73 | { 74 | list($path, $option) = (new Capture($video, $audio, $screen))->getOptions(); 75 | return $this->customInput($path, array_merge($option, $options)); 76 | } 77 | 78 | /** 79 | * @param string $path 80 | * @param array $options 81 | * @return Media 82 | */ 83 | public function customInput(string $path, array $options = []): Media 84 | { 85 | return new Media(new Video($path, $this->getFFMpegDriver(), $this->getFFProbe()), false, $options); 86 | } 87 | 88 | /** 89 | * @param $method 90 | * @param $parameters 91 | * @return mixed 92 | */ 93 | public function __call($method, $parameters) 94 | { 95 | return call_user_func_array([$this->ffmpeg, $method], $parameters); 96 | } 97 | 98 | /** 99 | * @param array $config 100 | * @param LoggerInterface $logger 101 | * @param FFProbe|null $probe 102 | * @return FFMpeg 103 | */ 104 | public static function create(array $config = [], LoggerInterface $logger = null, FFProbe $probe = null) 105 | { 106 | return new static(BFFMpeg::create($config, $logger, $probe)); 107 | } 108 | } -------------------------------------------------------------------------------- /src/Filters/DASHFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming\Filters; 14 | 15 | 16 | use Streaming\StreamInterface; 17 | use Streaming\Representation; 18 | use Streaming\Utiles; 19 | 20 | class DASHFilter extends FormatFilter 21 | { 22 | /** @var \Streaming\DASH */ 23 | private $dash; 24 | 25 | /** 26 | * @param Representation $rep 27 | * @param int $key 28 | * @return array 29 | */ 30 | private function getAudioBitrate(Representation $rep, int $key): array 31 | { 32 | return $rep->getAudioKiloBitrate() ? ["-b:a:" . $key, $rep->getAudioKiloBitrate() . "k"] : []; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | private function streams(): array 39 | { 40 | $streams = []; 41 | foreach ($this->dash->getRepresentations() as $key => $rep) { 42 | $streams = array_merge( 43 | $streams, 44 | Utiles::arrayToFFmpegOpt([ 45 | 'map' => 0, 46 | "s:v:$key" => $rep->size2string(), 47 | "b:v:$key" => $rep->getKiloBitrate() . "k" 48 | ]), 49 | $this->getAudioBitrate($rep, $key) 50 | ); 51 | } 52 | 53 | return $streams; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | private function getAdaptions(): array 60 | { 61 | return $this->dash->getAdaption() ? ['-adaptation_sets', $this->dash->getAdaption()] : []; 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | private function init(): array 68 | { 69 | $name = $this->dash->pathInfo(PATHINFO_FILENAME); 70 | 71 | $init = [ 72 | "use_timeline" => 1, 73 | "use_template" => 1, 74 | "init_seg_name" => $name . '_init_$RepresentationID$.$ext$', 75 | "media_seg_name" => $name . '_chunk_$RepresentationID$_$Number%05d$.$ext$', 76 | "seg_duration" => $this->dash->getSegDuration(), 77 | "hls_playlist" => (int)$this->dash->isGenerateHlsPlaylist(), 78 | "f" => "dash", 79 | ]; 80 | 81 | return array_merge( 82 | Utiles::arrayToFFmpegOpt($init), 83 | $this->getAdaptions(), 84 | Utiles::arrayToFFmpegOpt($this->dash->getAdditionalParams()) 85 | ); 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | private function getArgs(): array 92 | { 93 | return array_merge( 94 | $this->init(), 95 | $this->streams(), 96 | ['-strict', $this->dash->getStrict()] 97 | ); 98 | } 99 | 100 | /** 101 | * @param StreamInterface $dash 102 | */ 103 | public function streamFilter(StreamInterface $dash): void 104 | { 105 | $this->dash = $dash; 106 | 107 | $this->filter = array_merge( 108 | $this->getFormatOptions($dash->getFormat()), 109 | $this->getArgs() 110 | ); 111 | } 112 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@aminyazdanpanah.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/HLSPlaylist.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | 15 | use FFMpeg\Exception\ExceptionInterface; 16 | 17 | class HLSPlaylist 18 | { 19 | /** @var HLS */ 20 | private $hls; 21 | 22 | private const DEFAULT_AUDIO_BITRATE = 0; //131072; 23 | 24 | /** 25 | * HLSPlaylist constructor. 26 | * @param HLS $hls 27 | */ 28 | public function __construct(HLS $hls) 29 | { 30 | $this->hls = $hls; 31 | } 32 | 33 | /** 34 | * @param Representation $rep 35 | * @return string 36 | */ 37 | private function segmentPath(Representation $rep): string 38 | { 39 | return $this->hls->pathInfo(PATHINFO_FILENAME) . "_" . $rep->getHeight() . "p.m3u8"; 40 | } 41 | 42 | /** 43 | * @param Representation $rep 44 | * @return string 45 | */ 46 | private function streamInfo(Representation $rep): string 47 | { 48 | $tag = '#EXT-X-STREAM-INF:'; 49 | $params = array_merge( 50 | [ 51 | "BANDWIDTH" => $rep->getKiloBitrate() * 1024 + $this->getAudioBitrate($rep), 52 | "RESOLUTION" => $rep->size2string(), 53 | "NAME" => "\"" . $rep->getHeight() . "\"" 54 | ], 55 | $rep->getHlsStreamInfo() 56 | ); 57 | Utiles::concatKeyValue($params, "="); 58 | 59 | return $tag . implode(",", $params); 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | private function getVersion(): string 66 | { 67 | $version = $this->hls->getHlsSegmentType() === "fmp4" ? 7 : 3; 68 | return "#EXT-X-VERSION:" . $version; 69 | } 70 | 71 | /** 72 | * @param array $description 73 | * @return string 74 | */ 75 | private function contents(array $description): string 76 | { 77 | $content = array_merge(["#EXTM3U", $this->getVersion()], $description); 78 | 79 | foreach ($this->hls->getRepresentations() as $rep) { 80 | array_push($content, $this->streamInfo($rep), $this->segmentPath($rep)); 81 | } 82 | 83 | return implode(PHP_EOL, $content); 84 | } 85 | 86 | /** 87 | * @param string $filename 88 | * @param array $description 89 | */ 90 | public function save(string $filename, array $description): void 91 | { 92 | File::put($filename, $this->contents(($description))); 93 | } 94 | 95 | /** 96 | * @param Representation $rep 97 | * @return int 98 | */ 99 | private function getAudioBitrate(Representation $rep): int 100 | { 101 | return $rep->getAudioKiloBitrate() ? $rep->getAudioKiloBitrate() * 1024 : $this->getOriginalAudioBitrate(); 102 | } 103 | 104 | /** 105 | * @return int 106 | */ 107 | private function getOriginalAudioBitrate(): int 108 | { 109 | try { 110 | $audios = $this->hls->getMedia()->getStreams()->audios(); 111 | 112 | if (!$audios->count()){ 113 | return static::DEFAULT_AUDIO_BITRATE; 114 | } 115 | 116 | return $audios->first()->get('bit_rate', static::DEFAULT_AUDIO_BITRATE); 117 | } catch (ExceptionInterface $e) { 118 | return static::DEFAULT_AUDIO_BITRATE; 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Clouds/GoogleCloudStorage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | namespace Streaming\Clouds; 12 | 13 | use Streaming\Exception\InvalidArgumentException; 14 | use Streaming\Exception\RuntimeException; 15 | 16 | class GoogleCloudStorage implements CloudInterface 17 | { 18 | /** @var \Google\Cloud\Storage\StorageClient*/ 19 | private $storage; 20 | /** 21 | * GoogleCloudStorage constructor. 22 | * @param array $config 23 | */ 24 | public function __construct(array $config) 25 | { 26 | if(!class_exists('\Google\Cloud\Storage\StorageClient')){ 27 | throw new RuntimeException('Google\Cloud\Storage\StorageClient not found. make sure the package is installed: composer require google/cloud-storage'); 28 | } 29 | 30 | try { 31 | $this->storage = new \Google\Cloud\Storage\StorageClient($config); 32 | } catch (\Exception $e) { 33 | throw new InvalidArgumentException(sprintf("Invalid inputs:\n %s", $e->getMessage()), $e->getCode(), $e); 34 | } 35 | } 36 | 37 | /** 38 | * @param string $dir 39 | * @param array $options 40 | */ 41 | public function uploadDirectory(string $dir, array $options): void 42 | { 43 | $bucket = $this->getBucket($options); 44 | $cloud_filename = dirname($options['filename'] ?? ''); 45 | 46 | unset($options['bucket'], $options['user_project'], $options['filename']); 47 | 48 | try { 49 | foreach (scandir($dir) as $file) { 50 | $path = $dir . DIRECTORY_SEPARATOR . $file; 51 | $name = $cloud_filename ? implode('/', [$cloud_filename, $file]) : $file; 52 | $options = array_merge($options, ['name' => $name]); 53 | 54 | if (is_file($path)) { 55 | $bucket->upload(fopen($path, 'r'), $options); 56 | } 57 | } 58 | } catch (\Exception $e) { 59 | throw new RuntimeException(sprintf("An error occurred while uploading files: %s", $e->getMessage()), $e->getCode(), $e); 60 | } 61 | } 62 | 63 | /** 64 | * @param string $save_to 65 | * @param array $options 66 | */ 67 | public function download(string $save_to, array $options): void 68 | { 69 | $bucket = $this->getBucket($options); 70 | 71 | if(!isset($options['bucket'])){ 72 | throw new InvalidArgumentException("You need to set the bucket name in the option array"); 73 | } 74 | $name = $options['object_name']; 75 | unset($options['bucket'], $options['user_project'], $options['object_name']); 76 | 77 | try { 78 | $bucket->object($name, $options)->downloadToFile($save_to); 79 | } catch (\Exception $e) { 80 | throw new RuntimeException(sprintf("An error occurred while downloading the file: %s", $e->getMessage()), $e->getCode(), $e); 81 | } 82 | } 83 | 84 | /** 85 | * @param array $options 86 | * @return \Google\Cloud\Storage\Bucket 87 | */ 88 | private function getBucket(array $options): \Google\Cloud\Storage\Bucket 89 | { 90 | if(!isset($options['bucket'])){ 91 | throw new InvalidArgumentException("You need to set the bucket name in the option array"); 92 | } 93 | try{ 94 | return $this->storage->bucket($options['bucket'], $options['user_project'] ?? false); 95 | }catch (\Exception $e){ 96 | throw new RuntimeException(sprintf("An error occurred while opening the bucket: %s", $e->getMessage()), $e->getCode(), $e); 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/Representation.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use FFMpeg\Coordinate\AspectRatio; 15 | use FFMpeg\Coordinate\Dimension; 16 | use Streaming\Exception\InvalidArgumentException; 17 | 18 | class Representation implements RepresentationInterface 19 | { 20 | /** @var int $kiloBitrate video kilo bitrate */ 21 | private $kiloBitrate; 22 | 23 | /** @var int $audioKiloBitrate audio kilo bitrate */ 24 | private $audioKiloBitrate; 25 | 26 | /** @var Dimension $size size of representation */ 27 | private $size; 28 | 29 | /** @var array $hls_stream_info hls stream info */ 30 | private $hls_stream_info = []; 31 | 32 | /** 33 | * @return string | null 34 | */ 35 | public function size2string(): ?string 36 | { 37 | return !is_null($this->size) ? implode("x", [$this->getWidth(), $this->getHeight()]) : null; 38 | } 39 | 40 | /** 41 | * @param $width 42 | * @param $height 43 | * @return Representation 44 | * @throws InvalidArgumentException 45 | */ 46 | public function setResize(int $width, int $height): Representation 47 | { 48 | $this->setSize(new Dimension($width, $height)); 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return int 54 | */ 55 | public function getKiloBitrate(): int 56 | { 57 | return $this->kiloBitrate; 58 | } 59 | 60 | /** 61 | * @return int|null 62 | */ 63 | public function getAudioKiloBitrate() 64 | { 65 | return $this->audioKiloBitrate; 66 | } 67 | 68 | /** 69 | * Sets the video kiloBitrate value. 70 | * 71 | * @param integer $kiloBitrate 72 | * @return Representation 73 | * @throws InvalidArgumentException 74 | */ 75 | public function setKiloBitrate(int $kiloBitrate): Representation 76 | { 77 | if ($kiloBitrate < 1) { 78 | throw new InvalidArgumentException('Invalid kilo bit rate value'); 79 | } 80 | 81 | $this->kiloBitrate = (int)$kiloBitrate; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Sets the video kiloBitrate value. 87 | * 88 | * @param integer $audioKiloBitrate 89 | * @return Representation 90 | * @throws InvalidArgumentException 91 | */ 92 | public function setAudioKiloBitrate(int $audioKiloBitrate): Representation 93 | { 94 | $this->audioKiloBitrate = $audioKiloBitrate; 95 | return $this; 96 | } 97 | 98 | /** 99 | * @return int 100 | */ 101 | public function getWidth(): int 102 | { 103 | return $this->size->getWidth(); 104 | } 105 | 106 | /** 107 | * @return int 108 | */ 109 | public function getHeight(): int 110 | { 111 | return $this->size->getHeight(); 112 | } 113 | 114 | /** 115 | * @return AspectRatio 116 | */ 117 | public function getRatio(): AspectRatio 118 | { 119 | return $this->size->getRatio(); 120 | } 121 | 122 | /** 123 | * @param array $hls_stream_info 124 | * @return Representation 125 | */ 126 | public function setHlsStreamInfo(array $hls_stream_info): Representation 127 | { 128 | $this->hls_stream_info = $hls_stream_info; 129 | return $this; 130 | } 131 | 132 | /** 133 | * @return array 134 | */ 135 | public function getHlsStreamInfo(): array 136 | { 137 | return $this->hls_stream_info; 138 | } 139 | 140 | /** 141 | * @param Dimension $size 142 | * @return Representation 143 | */ 144 | public function setSize(Dimension $size): Representation 145 | { 146 | $this->size = $size; 147 | return $this; 148 | } 149 | 150 | /** 151 | * @return Dimension 152 | */ 153 | public function getSize(): Dimension 154 | { 155 | return $this->size; 156 | } 157 | } -------------------------------------------------------------------------------- /tests/HLSTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Tests\FFMpegStreaming; 13 | 14 | use Streaming\Format\StreamFormat; 15 | use Streaming\HLS; 16 | use Streaming\Stream; 17 | use Streaming\Representation; 18 | 19 | class HLSTest extends TestCase 20 | { 21 | public function testHLSClass() 22 | { 23 | $this->assertInstanceOf(Stream::class, $this->getHLS()); 24 | } 25 | 26 | public function testFormat() 27 | { 28 | $hls = $this->getHLS(); 29 | $hls->X264(); 30 | 31 | $this->assertInstanceOf(StreamFormat::class, $hls->getFormat()); 32 | } 33 | 34 | public function testAutoRepresentations() 35 | { 36 | $hls = $this->getHLS(); 37 | $hls->X264() 38 | ->autoGenerateRepresentations(); 39 | 40 | $representations = $hls->getRepresentations()->all(); 41 | 42 | $this->assertIsArray($representations); 43 | $this->assertInstanceOf(Representation::class, current($representations)); 44 | 45 | $this->assertEquals('256x144', $representations[0]->size2string()); 46 | $this->assertEquals('426x240', $representations[1]->size2string()); 47 | $this->assertEquals('640x360', $representations[2]->size2string()); 48 | 49 | $this->assertEquals(103, $representations[0]->getKiloBitrate()); 50 | $this->assertEquals(138, $representations[1]->getKiloBitrate()); 51 | $this->assertEquals(207, $representations[2]->getKiloBitrate()); 52 | } 53 | 54 | public function testSetHlsTime() 55 | { 56 | $hls = $this->getHLS(); 57 | $hls->setHlsTime(10); 58 | 59 | $this->assertEquals(10, $hls->getHlsTime()); 60 | } 61 | 62 | public function testSetHlsAllowCache() 63 | { 64 | $hls = $this->getHLS(); 65 | $hls->setHlsAllowCache(false); 66 | 67 | $this->assertFalse($hls->isHlsAllowCache()); 68 | } 69 | 70 | public function testSave() 71 | { 72 | $rep_1 = (new Representation())->setKiloBitrate(200)->setResize(640, 360); 73 | $rep_2 = (new Representation())->setKiloBitrate(100)->setResize(480, 270); 74 | 75 | $hls = $this->getHLS(); 76 | $hls->X264() 77 | ->addRepresentations([$rep_1, $rep_2]) 78 | ->save($this->srcDir . '/hls/test.m3u8'); 79 | 80 | $this->assertInstanceOf(Representation::class, $rep_1); 81 | $this->assertEquals($rep_1->getKiloBitrate(), 200); 82 | $this->assertEquals($rep_2->size2string(), "480x270"); 83 | $this->assertFileExists($this->srcDir . '/hls/test.m3u8'); 84 | } 85 | 86 | public function testEncryptedHLS() 87 | { 88 | $this->creatKeyInfoFile(); 89 | 90 | $hls = $this->getHLS(); 91 | $hls->X264() 92 | ->setHlsKeyInfoFile($this->srcDir . '/enc.keyinfo') 93 | ->autoGenerateRepresentations() 94 | ->save($this->srcDir . '/enc_hls/test.m3u8'); 95 | 96 | 97 | $this->assertFileExists($this->srcDir . '/enc_hls/test.m3u8'); 98 | } 99 | 100 | public function testRandomEncryptedHLS() 101 | { 102 | $url = "https://www.aminyazdanpanah.com/keys/test.key"; 103 | $path = $this->srcDir . DIRECTORY_SEPARATOR . "test2.key"; 104 | 105 | $hls = $this->getHLS(); 106 | $hls->encryption($path, $url) 107 | ->X264() 108 | ->autoGenerateRepresentations() 109 | ->save($this->srcDir . '/enc_random_hls/test.m3u8'); 110 | 111 | $this->assertFileExists($this->srcDir . '/enc_random_hls/test.m3u8'); 112 | } 113 | 114 | private function getHLS() 115 | { 116 | return new HLS($this->getVideo()); 117 | } 118 | 119 | private function creatKeyInfoFile() 120 | { 121 | $contents[] = 'http://www.aminyazdanpanah.com/key/enc.key'; 122 | $contents[] = $this->srcDir . '/enc.key'; 123 | $contents[] = '17e15e333e4347e31017c5415adde26f'; 124 | 125 | file_put_contents($this->srcDir . '/enc.keyinfo', implode(PHP_EOL, $contents)); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use Streaming\Exception\RuntimeException; 17 | use Symfony\Component\Filesystem\Exception\IOExceptionInterface; 18 | use Symfony\Component\Filesystem\Filesystem; 19 | 20 | /** 21 | * File constructor. 22 | * It is all about files 23 | */ 24 | 25 | class File 26 | { 27 | /** 28 | * @param $dirname 29 | * @param int $mode 30 | */ 31 | public static function makeDir(string $dirname, int $mode = 0777): void 32 | { 33 | static::filesystem('mkdir', [$dirname, $mode]); 34 | } 35 | 36 | /** 37 | * @param $dir 38 | * @return int|null 39 | */ 40 | public static function directorySize(string $dir): int 41 | { 42 | if (is_dir($dir)) { 43 | $size = 0; 44 | foreach (scandir($dir) as $file) { 45 | if (in_array($file, [".", ".."])) continue; 46 | $filename = $dir . DIRECTORY_SEPARATOR . $file; 47 | $size += is_file($filename) ? filesize($filename) : static::directorySize($filename); 48 | } 49 | return $size; 50 | } 51 | 52 | return 0; 53 | } 54 | 55 | /** 56 | * @param $path 57 | * @param $content 58 | * @param bool $force 59 | * @return void 60 | */ 61 | public static function put($path, $content, $force = true): void 62 | { 63 | if (file_exists($path) && !$force) { 64 | throw new RuntimeException("File Already Exists"); 65 | } 66 | 67 | if (false === @file_put_contents($path, $content)) { 68 | throw new RuntimeException("Unable to save the file"); 69 | } 70 | } 71 | 72 | /** 73 | * @param string $prefix 74 | * @return string 75 | */ 76 | public static function tmp($prefix = 'pfvs.file_'): string 77 | { 78 | for ($i = 0; $i < 10; ++$i) { 79 | $tmpFile = static::tmpDirPath() . '/' . basename($prefix) . uniqid(mt_rand()); 80 | $handle = @fopen($tmpFile, 'x+'); 81 | 82 | if (false === $handle) { 83 | continue; 84 | } 85 | 86 | @fclose($handle); 87 | 88 | return $tmpFile; 89 | } 90 | 91 | throw new RuntimeException("A temporary file could not be created."); 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public static function tmpDir(): string 98 | { 99 | static::makeDir($tmp_dir = static::tmpDirPath() . DIRECTORY_SEPARATOR . uniqid() . DIRECTORY_SEPARATOR); 100 | return $tmp_dir; 101 | } 102 | 103 | /** 104 | * clear all tmp files 105 | */ 106 | public static function cleanTmpFiles(): void 107 | { 108 | static::remove(static::tmpDirPath()); 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | private static function tmpDirPath(): string 115 | { 116 | static::makeDir($tmp_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "php_ffmpeg_video_streaming"); 117 | return $tmp_path; 118 | } 119 | 120 | /** 121 | * @param string $src 122 | * @param string $dst 123 | */ 124 | public static function move(string $src, string $dst): void 125 | { 126 | static::filesystem('mirror', [$src, $dst]); 127 | static::remove($src); 128 | } 129 | 130 | /** 131 | * @param string $src 132 | * @param string $dst 133 | * @param bool $force 134 | */ 135 | public static function copy(string $src, string $dst, bool $force = true): void 136 | { 137 | static::filesystem('copy', [$src, $dst, $force]); 138 | } 139 | 140 | /** 141 | * @param $dir 142 | */ 143 | public static function remove(string $dir): void 144 | { 145 | static::filesystem('remove', [$dir]); 146 | } 147 | 148 | /** 149 | * @param string $method 150 | * @param array $params 151 | * @return mixed 152 | */ 153 | private static function filesystem(string $method, array $params) 154 | { 155 | try { 156 | return \call_user_func_array([new Filesystem, $method], $params); 157 | } catch (IOExceptionInterface $e) { 158 | throw new RuntimeException("Failed action" . $e->getPath(), $e->getCode(), $e); 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /src/HLSKeyInfo.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use FFMpeg\Driver\FFMpegDriver; 17 | use Streaming\Exception\RuntimeException; 18 | 19 | class HLSKeyInfo 20 | { 21 | /** @var string */ 22 | private $path; 23 | 24 | /** @var string */ 25 | private $c_path; 26 | 27 | /** @var string */ 28 | private $url; 29 | 30 | /** @var string */ 31 | private $c_url; 32 | 33 | /** @var string */ 34 | private $key_info_path; 35 | 36 | /** @var string */ 37 | private $suffix = ""; 38 | 39 | /** @var int */ 40 | private $length = 16; 41 | 42 | /** @var array */ 43 | private $segments = []; 44 | 45 | /** 46 | * HLSKeyInfo constructor. 47 | * @param string $path 48 | * @param string $url 49 | */ 50 | public function __construct(string $path, string $url) 51 | { 52 | $this->path = $this->c_path = $path; 53 | $this->url = $this->c_url = $url; 54 | File::makeDir(dirname($path)); 55 | $this->key_info_path = File::tmp(); 56 | } 57 | 58 | /** 59 | * @param string $path 60 | * @param string $url 61 | * @return HLSKeyInfo 62 | */ 63 | public static function create(string $path, string $url): HLSKeyInfo 64 | { 65 | return new static($path, $url); 66 | } 67 | 68 | /** 69 | * Generate a encryption key 70 | */ 71 | public function createKey(): void 72 | { 73 | if (!extension_loaded('openssl')) { 74 | throw new RuntimeException('OpenSSL is not installed.'); 75 | } 76 | 77 | File::put($this->path, openssl_random_pseudo_bytes($this->length)); 78 | } 79 | 80 | /** 81 | * update or generate a key info file 82 | */ 83 | public function createKeyInfo(): void 84 | { 85 | $content = implode( 86 | PHP_EOL, 87 | [ 88 | $this->url, 89 | $this->path, 90 | bin2hex(openssl_random_pseudo_bytes($this->length)) 91 | ] 92 | ); 93 | File::put($this->key_info_path, $content); 94 | } 95 | 96 | /** 97 | * @param FFMpegDriver $driver 98 | * @param int $period 99 | * @param string $needle 100 | */ 101 | public function rotateKey(FFMpegDriver $driver, int $period, string $needle): void 102 | { 103 | call_user_func_array([$driver->listen(new FFMpegListener), 'on'], ['listen', $this->call($needle, $period)]); 104 | } 105 | 106 | /** 107 | * @return void 108 | */ 109 | public function generate(): void 110 | { 111 | $this->createKey(); 112 | $this->createKeyInfo(); 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function __toString() 119 | { 120 | $this->generate(); 121 | return $this->key_info_path; 122 | } 123 | 124 | /** 125 | * @param string $key_info_path 126 | * @return HLSKeyInfo 127 | */ 128 | public function setKeyInfoPath(string $key_info_path): HLSKeyInfo 129 | { 130 | $this->key_info_path = $key_info_path; 131 | return $this; 132 | } 133 | 134 | /** 135 | * @param int $length 136 | * @return HLSKeyInfo 137 | */ 138 | public function setLength(int $length): HLSKeyInfo 139 | { 140 | $this->length = $length; 141 | return $this; 142 | } 143 | 144 | /** 145 | * @param string $suffix 146 | * @return HLSKeyInfo 147 | */ 148 | public function setSuffix(string $suffix): HLSKeyInfo 149 | { 150 | $this->suffix = $suffix; 151 | return $this; 152 | } 153 | 154 | /** 155 | * update the suffix of paths 156 | */ 157 | public function updateSuffix(): void 158 | { 159 | $suffix = uniqid("_") . $this->suffix; 160 | 161 | $this->path = $this->c_path . $suffix; 162 | $this->url = $this->c_url . $suffix; 163 | } 164 | 165 | /** 166 | * check if a new segment is created or not 167 | * @param string $needle 168 | * @param int $period 169 | * @return callable 170 | */ 171 | private function call(string $needle, int $period): callable 172 | { 173 | return function ($line) use ($needle, $period) { 174 | if (false !== strpos($line, $needle) && !in_array($line, $this->segments)) { 175 | array_push($this->segments, $line); 176 | if (0 === count($this->segments) % $period) { 177 | $this->updateSuffix(); 178 | $this->generate(); 179 | } 180 | } 181 | }; 182 | } 183 | 184 | /** 185 | * @return array 186 | */ 187 | public function getSegments(): array 188 | { 189 | return $this->segments; 190 | } 191 | 192 | /** 193 | * @return string 194 | */ 195 | public function getKeyInfoPath(): string 196 | { 197 | return $this->key_info_path; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use FFMpeg\Exception\ExceptionInterface; 15 | use Streaming\Clouds\Cloud; 16 | use Streaming\Exception\InvalidArgumentException; 17 | use Streaming\Exception\RuntimeException; 18 | use Streaming\Filters\StreamFilterInterface; 19 | use Streaming\Traits\Formats; 20 | 21 | 22 | abstract class Stream implements StreamInterface 23 | { 24 | use Formats; 25 | 26 | /** @var Media */ 27 | private $media; 28 | 29 | /** @var string */ 30 | protected $path; 31 | 32 | /** @var string */ 33 | private $tmp_dir = ''; 34 | 35 | /** 36 | * Stream constructor. 37 | * @param Media $media 38 | */ 39 | public function __construct(Media $media) 40 | { 41 | $this->media = $media; 42 | $this->path = $media->getPathfile(); 43 | } 44 | 45 | /** 46 | * @return Media 47 | */ 48 | public function getMedia(): Media 49 | { 50 | return $this->media; 51 | } 52 | 53 | /** 54 | * @return bool 55 | */ 56 | public function isTmpDir(): bool 57 | { 58 | return (bool)$this->tmp_dir; 59 | } 60 | 61 | /** 62 | * @param int $option 63 | * @return string 64 | */ 65 | public function pathInfo(int $option): string 66 | { 67 | return pathinfo($this->path, $option); 68 | } 69 | 70 | /** 71 | * @param string|null $path 72 | */ 73 | private function moveTmp(?string $path): void 74 | { 75 | if ($this->isTmpDir() && !is_null($path)) { 76 | File::move($this->tmp_dir, dirname($path)); 77 | $this->path = $path; 78 | $this->tmp_dir = ''; 79 | } 80 | } 81 | 82 | /** 83 | * @param array $clouds 84 | * @param string $path 85 | */ 86 | private function clouds(array $clouds, ?string $path): void 87 | { 88 | if (!empty($clouds)) { 89 | Cloud::uploadDirectory($clouds, $this->tmp_dir); 90 | $this->moveTmp($path); 91 | } 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | protected function getFilePath(): string 98 | { 99 | return str_replace( 100 | "\\", 101 | "/", 102 | $this->pathInfo(PATHINFO_DIRNAME) . "/" . $this->pathInfo(PATHINFO_FILENAME) 103 | ); 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | abstract protected function getPath(): string; 110 | 111 | /** 112 | * @return StreamFilterInterface 113 | */ 114 | abstract protected function getFilter(): StreamFilterInterface; 115 | 116 | /** 117 | * Run FFmpeg to package media content 118 | */ 119 | private function run(): void 120 | { 121 | $this->media->addFilter($this->getFilter()); 122 | 123 | $commands = (new CommandBuilder($this->media, $this->getFormat()))->build($this->getFormat(), $this->getPath()); 124 | $pass = $this->format->getPasses(); 125 | $listeners = $this->format->createProgressListener($this->media->baseMedia(), $this->media->getFFProbe(), 1, $pass); 126 | 127 | try { 128 | $this->media->getFFMpegDriver()->command($commands, false, $listeners); 129 | } catch (ExceptionInterface $e) { 130 | throw new RuntimeException("An error occurred while saving files: " . $e->getMessage(), $e->getCode(), $e); 131 | } 132 | } 133 | 134 | /** 135 | * @param $path 136 | * @param $clouds 137 | */ 138 | private function paths(?string $path, array $clouds): void 139 | { 140 | if (!empty($clouds)) { 141 | $this->tmp_dir = File::tmpDir(); 142 | $this->path = $this->tmp_dir . basename($clouds['options']['filename'] ?? $path ?? $this->path); 143 | } elseif (!is_null($path)) { 144 | if (strlen($path) > PHP_MAXPATHLEN) { 145 | throw new InvalidArgumentException("The path is too long"); 146 | } 147 | 148 | File::makeDir(dirname($path)); 149 | $this->path = $path; 150 | } elseif ($this->media->isTmp()) { 151 | throw new InvalidArgumentException("You need to specify a path. It is not possible to save to a tmp directory"); 152 | } 153 | } 154 | 155 | /** 156 | * @param string $path 157 | * @param array $clouds 158 | * @return mixed 159 | */ 160 | public function save(string $path = null, array $clouds = []): Stream 161 | { 162 | $this->paths($path, $clouds); 163 | $this->run(); 164 | $this->clouds($clouds, $path); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * @param string $url 171 | */ 172 | public function live(string $url): void 173 | { 174 | $this->path = $url; 175 | $this->run(); 176 | } 177 | 178 | /** 179 | * @return Metadata 180 | */ 181 | public function metadata(): Metadata 182 | { 183 | return new Metadata($this); 184 | } 185 | 186 | /** 187 | * clear tmp files 188 | */ 189 | public function __destruct() 190 | { 191 | // make sure that FFmpeg process has benn terminated 192 | sleep(1); 193 | File::remove($this->tmp_dir); 194 | 195 | if ($this->media->isTmp()) { 196 | File::remove($this->media->getPathfile()); 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /src/Filters/HLSFilter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming\Filters; 13 | 14 | use Streaming\StreamInterface; 15 | use Streaming\File; 16 | use Streaming\Representation; 17 | use Streaming\Utiles; 18 | 19 | class HLSFilter extends FormatFilter 20 | { 21 | /** @var \Streaming\HLS */ 22 | private $hls; 23 | 24 | /** @var string */ 25 | private $dirname; 26 | 27 | /** @var string */ 28 | private $filename; 29 | 30 | /** @var string */ 31 | private $seg_sub_dir; 32 | 33 | /** @var string */ 34 | private $base_url; 35 | 36 | /** @var string */ 37 | private $seg_filename; 38 | 39 | /** 40 | * @param Representation $rep 41 | * @param bool $not_last 42 | * @return array 43 | */ 44 | private function playlistPath(Representation $rep, bool $not_last): array 45 | { 46 | return $not_last ? [$this->dirname . "/" . $this->filename . "_" . $rep->getHeight() . "p.m3u8"] : []; 47 | } 48 | 49 | /** 50 | * @param Representation $rep 51 | * @return array 52 | */ 53 | private function getAudioBitrate(Representation $rep): array 54 | { 55 | return $rep->getAudioKiloBitrate() ? ["b:a" => $rep->getAudioKiloBitrate() . "k"] : []; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | private function getBaseURL(): array 62 | { 63 | return $this->base_url ? ["hls_base_url" => $this->base_url] : []; 64 | } 65 | 66 | private function flags(): array 67 | { 68 | return !empty($this->hls->getFlags()) ? ["hls_flags" => implode("+", $this->hls->getFlags())] : []; 69 | } 70 | 71 | /** 72 | * @return array 73 | */ 74 | private function getKeyInfo(): array 75 | { 76 | return $this->hls->getHlsKeyInfoFile() ? ["hls_key_info_file" => $this->hls->getHlsKeyInfoFile()] : []; 77 | } 78 | 79 | /** 80 | * @param Representation $rep 81 | * @return string 82 | */ 83 | private function getInitFilename(Representation $rep): string 84 | { 85 | return $this->seg_sub_dir . $this->filename . "_" . $rep->getHeight() ."p_". $this->hls->getHlsFmp4InitFilename(); 86 | } 87 | 88 | /** 89 | * @param Representation $rep 90 | * @return string 91 | */ 92 | private function getSegmentFilename(Representation $rep): string 93 | { 94 | $ext = ($this->hls->getHlsSegmentType() === "fmp4") ? "m4s" : "ts"; 95 | return $this->seg_filename . "_" . $rep->getHeight() . "p_%04d." . $ext; 96 | } 97 | 98 | /** 99 | * @param Representation $rep 100 | * @return array 101 | */ 102 | private function initArgs(Representation $rep): array 103 | { 104 | $init = [ 105 | "hls_list_size" => $this->hls->getHlsListSize(), 106 | "hls_time" => $this->hls->getHlsTime(), 107 | "hls_allow_cache" => (int)$this->hls->isHlsAllowCache(), 108 | "hls_segment_type" => $this->hls->getHlsSegmentType(), 109 | "hls_fmp4_init_filename" => $this->getInitFilename($rep), 110 | "hls_segment_filename" => $this->getSegmentFilename($rep), 111 | "s:v" => $rep->size2string(), 112 | "b:v" => $rep->getKiloBitrate() . "k" 113 | ]; 114 | 115 | return array_merge($init, 116 | $this->getAudioBitrate($rep), 117 | $this->getBaseURL(), 118 | $this->flags(), 119 | $this->getKeyInfo()); 120 | } 121 | 122 | /** 123 | * @param Representation $rep 124 | * @param bool $not_last 125 | */ 126 | private function getArgs(Representation $rep, bool $not_last): void 127 | { 128 | $this->filter = array_merge( 129 | $this->filter, 130 | $this->getFormatOptions($this->hls->getFormat()), 131 | Utiles::arrayToFFmpegOpt($this->initArgs($rep)), 132 | Utiles::arrayToFFmpegOpt($this->hls->getAdditionalParams()), 133 | ["-strict", $this->hls->getStrict()], 134 | $this->playlistPath($rep, $not_last) 135 | ); 136 | } 137 | 138 | /** 139 | * set segments paths 140 | */ 141 | private function segmentPaths() 142 | { 143 | if ($this->hls->getSegSubDirectory()) { 144 | File::makeDir($this->dirname . "/" . $this->hls->getSegSubDirectory() . "/"); 145 | } 146 | 147 | $base = Utiles::appendSlash($this->hls->getHlsBaseUrl()); 148 | 149 | $this->seg_sub_dir = Utiles::appendSlash($this->hls->getSegSubDirectory()); 150 | $this->seg_filename = $this->dirname . "/" . $this->seg_sub_dir . $this->filename; 151 | $this->base_url = $base . $this->seg_sub_dir; 152 | } 153 | 154 | /** 155 | * set paths 156 | */ 157 | private function setPaths(): void 158 | { 159 | $this->dirname = str_replace("\\", "/", $this->hls->pathInfo(PATHINFO_DIRNAME)); 160 | $this->filename = $this->hls->pathInfo(PATHINFO_FILENAME); 161 | $this->segmentPaths(); 162 | } 163 | 164 | /** 165 | * @param StreamInterface $stream 166 | * @return void 167 | */ 168 | public function streamFilter(StreamInterface $stream): void 169 | { 170 | $this->hls = $stream; 171 | $this->setPaths(); 172 | $reps = $this->hls->getRepresentations(); 173 | 174 | foreach ($reps as $key => $rep) { 175 | $this->getArgs($rep, $reps->end() !== $rep); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /src/Metadata.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | namespace Streaming; 14 | 15 | 16 | use FFMpeg\FFProbe\DataMapping\Format; 17 | use FFMpeg\FFProbe\DataMapping\Stream as VideoStream; 18 | use FFMpeg\FFProbe\DataMapping\StreamCollection; 19 | use Streaming\Exception\InvalidArgumentException; 20 | 21 | class Metadata 22 | { 23 | /** @var Stream */ 24 | private $stream; 25 | 26 | /** @var \FFMpeg\FFProbe\DataMapping\Format */ 27 | private $format; 28 | 29 | /** @var \FFMpeg\FFProbe\DataMapping\StreamCollection */ 30 | private $video_streams; 31 | 32 | /** 33 | * Metadata constructor. 34 | * @param Stream $stream 35 | */ 36 | public function __construct(Stream $stream) 37 | { 38 | $this->stream = $stream; 39 | $this->format = $stream->getMedia()->getFormat(); 40 | $this->video_streams = $stream->getMedia()->getStreams(); 41 | } 42 | 43 | /** 44 | * @return \FFMpeg\FFProbe\DataMapping\Format 45 | */ 46 | public function getFormat(): Format 47 | { 48 | return $this->format; 49 | } 50 | 51 | /** 52 | * @return \FFMpeg\FFProbe\DataMapping\StreamCollection 53 | */ 54 | public function getVideoStreams(): StreamCollection 55 | { 56 | return $this->video_streams; 57 | } 58 | 59 | /** 60 | * @param VideoStream $stream 61 | * @return array 62 | */ 63 | private function streamToArray(VideoStream $stream): array 64 | { 65 | return $stream->all(); 66 | } 67 | 68 | /** 69 | * @return mixed 70 | */ 71 | private function getVideoMetadata(): array 72 | { 73 | return [ 74 | 'format' => $this->getFormat()->all(), 75 | 'streams' => array_map([$this, 'streamToArray'], $this->getVideoStreams()->all()) 76 | ]; 77 | } 78 | 79 | /** 80 | * @param Representation $rep 81 | * @return array 82 | */ 83 | private function repToArray(Representation $rep): array 84 | { 85 | return [ 86 | "dimension" => strtoupper($rep->size2string()), 87 | "video_kilo_bitrate" => $rep->getKiloBitrate(), 88 | "audio_kilo_bitrate" => $rep->getAudioKiloBitrate() ?? "Not specified" 89 | ]; 90 | } 91 | 92 | /** 93 | * @return array 94 | */ 95 | private function getResolutions(): array 96 | { 97 | if (!method_exists($this->stream, 'getRepresentations')) { 98 | return []; 99 | } 100 | 101 | return array_map([$this, 'repToArray'], $this->stream->getRepresentations()->all()); 102 | } 103 | 104 | 105 | /** 106 | * @return array 107 | */ 108 | public function getStreamsMetadata(): array 109 | { 110 | $dirname = $this->stream->pathInfo(PATHINFO_DIRNAME); 111 | $basename = $this->stream->pathInfo(PATHINFO_BASENAME); 112 | $filename = $dirname . DIRECTORY_SEPARATOR . $basename; 113 | 114 | $technique = explode("\\", get_class($this->stream)); 115 | $format = explode("\\", get_class($this->stream->getFormat())); 116 | 117 | $metadata = [ 118 | "filename" => $filename, 119 | "size_of_stream_dir" => File::directorySize($dirname), 120 | "created_at" => file_exists($filename) ? date("Y-m-d H:i:s", filemtime($filename)) : 'The file has been deleted', 121 | "resolutions" => $this->getResolutions(), 122 | "format" => end($format), 123 | "streaming_technique" => end($technique) 124 | ]; 125 | 126 | if ($this->stream instanceof DASH) { 127 | $metadata = array_merge($metadata, ["seg_duration" => $this->stream->getSegDuration()]); 128 | } elseif ($this->stream instanceof HLS) { 129 | $metadata = array_merge( 130 | $metadata, 131 | [ 132 | "hls_time" => (int)$this->stream->getHlsTime(), 133 | "hls_cache" => (bool)$this->stream->isHlsAllowCache(), 134 | "encrypted_hls" => (bool)$this->stream->getHlsKeyInfoFile(), 135 | "ts_sub_directory" => $this->stream->getSegSubDirectory(), 136 | "base_url" => $this->stream->getHlsBaseUrl() 137 | ] 138 | ); 139 | } 140 | 141 | return $metadata; 142 | } 143 | 144 | /** 145 | * @return array 146 | */ 147 | public function get(): array 148 | { 149 | return [ 150 | "video" => $this->getVideoMetadata(), 151 | "stream" => $this->getStreamsMetadata() 152 | ]; 153 | } 154 | 155 | /** 156 | * @param null $opts 157 | * @return string 158 | */ 159 | public function getJson($opts = null): string 160 | { 161 | return json_encode($this->get(), $opts ?? JSON_PRETTY_PRINT); 162 | } 163 | 164 | /** 165 | * @param string $filename 166 | * @param int $opts 167 | * @return string 168 | */ 169 | public function saveAsJson(string $filename = null, int $opts = null): string 170 | { 171 | if (is_null($filename)) { 172 | if ($this->stream->isTmpDir()) { 173 | throw new InvalidArgumentException("It is a temp directory! It is not possible to save it"); 174 | } 175 | 176 | $name = uniqid(($this->stream->pathInfo(PATHINFO_FILENAME) ?? "meta") . "-") . ".json"; 177 | $filename = $this->stream->pathInfo(PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . $name; 178 | } 179 | File::put($filename, $this->getJson($opts)); 180 | 181 | return $filename; 182 | } 183 | 184 | /** 185 | * @param string|null $save_to 186 | * @param int|null $opts 187 | * @return array 188 | */ 189 | public function export(string $save_to = null, int $opts = null): array 190 | { 191 | return array_merge($this->get(), ['filename' => $this->saveAsJson($save_to, $opts)]); 192 | } 193 | } -------------------------------------------------------------------------------- /src/AutoReps.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use FFMpeg\Coordinate\Dimension; 15 | use FFMpeg\Exception\ExceptionInterface; 16 | use FFMpeg\Format\VideoInterface; 17 | use Streaming\Exception\InvalidArgumentException; 18 | use Streaming\Exception\RuntimeException; 19 | use Traversable; 20 | 21 | 22 | class AutoReps implements \IteratorAggregate 23 | { 24 | /** @var \FFMpeg\FFProbe\DataMapping\Stream $video */ 25 | private $video; 26 | 27 | /** @var \FFMpeg\FFProbe\DataMapping\Format $format */ 28 | private $original_format; 29 | 30 | /** 31 | * regular video's heights 32 | * 33 | * @var array side_values 34 | */ 35 | private $sides = [144, 240, 360, 480, 720, 1080, 1440, 2160]; 36 | 37 | /** @var array $k_bitrate */ 38 | private $k_bitrate; 39 | 40 | /** @var bool */ 41 | private $sort = true; 42 | 43 | /** @const VideoInterface */ 44 | private $format; 45 | 46 | /** 47 | * AutoReps constructor. 48 | * @param Media $media 49 | * @param VideoInterface $format 50 | * @param array|null $sides 51 | * @param array|null $k_bitrate 52 | */ 53 | public function __construct(Media $media, VideoInterface $format, array $sides = null, array $k_bitrate = null) 54 | { 55 | $this->format = $format; 56 | $this->video = $media->getStreams()->videos()->first(); 57 | $this->original_format = $media->getFormat(); 58 | $this->sides($sides, $k_bitrate); 59 | $this->kiloBitrate($k_bitrate); 60 | } 61 | 62 | /** 63 | * Set sort order for reps 64 | * @param bool $sort 65 | */ 66 | public function sort(bool $sort) 67 | { 68 | $this->sort = $sort; 69 | } 70 | 71 | /** 72 | * @return Dimension 73 | */ 74 | private function getDimensions(): Dimension 75 | { 76 | try { 77 | return $this->video->getDimensions(); 78 | } catch (ExceptionInterface $e) { 79 | throw new RuntimeException("Unable to extract dimensions.: " . $e->getMessage(), $e->getCode(), $e); 80 | } 81 | } 82 | 83 | /** 84 | * @return mixed 85 | * @throws InvalidArgumentException 86 | */ 87 | private function getKiloBitRate(): int 88 | { 89 | if (!$this->video->has('bit_rate')) { 90 | if (!$this->original_format->has('bit_rate')) { 91 | throw new InvalidArgumentException("Unable to extract bitrate."); 92 | } 93 | 94 | return intval(($this->original_format->get('bit_rate') / 1024) * .9); 95 | } 96 | 97 | return intval($this->video->get('bit_rate') / 1024); 98 | } 99 | 100 | /** 101 | * @param array|null $k_bitrate_values 102 | * @TODO: fix #79 103 | */ 104 | private function kiloBitrate(?array $k_bitrate_values): void 105 | { 106 | $k_bit_rates = []; 107 | $count_sides = count($this->sides); 108 | 109 | if (!is_null($k_bitrate_values)) { 110 | if ($count_sides !== count($k_bitrate_values)) { 111 | throw new InvalidArgumentException("The count of side value array must be the same as the count of kilo bitrate array"); 112 | } 113 | $this->k_bitrate = $k_bitrate_values; 114 | return; 115 | } 116 | 117 | $k_bitrate_value = $this->getKiloBitRate(); 118 | $divided_by = 1.5; 119 | 120 | while ($count_sides) { 121 | $k_bit_rates[] = (($k_bitrate = intval($k_bitrate_value / $divided_by)) < 64) ? 64 : $k_bitrate; 122 | $divided_by += .5; 123 | $count_sides--; 124 | } 125 | 126 | $this->k_bitrate = array_reverse($k_bit_rates); 127 | } 128 | 129 | /** 130 | * @param int $height 131 | * @return bool 132 | */ 133 | private function sideFilter(int $height): bool 134 | { 135 | return $height < $this->getDimensions()->getHeight(); 136 | } 137 | 138 | /** 139 | * @param array|null $sides 140 | * @param array|null $k_bitrate 141 | */ 142 | private function sides(?array $sides, ?array $k_bitrate): void 143 | { 144 | if (!is_null($sides) && is_null($k_bitrate)) { 145 | sort($sides); 146 | } 147 | 148 | $this->sides = array_values(array_filter($sides ?? $this->sides, [$this, 'sideFilter'])); 149 | } 150 | 151 | /** 152 | * @param int $value 153 | * @param string $side 154 | * @return int 155 | */ 156 | private function computeSide(int $value, string $side): int 157 | { 158 | $ratio = clone $this->getDimensions()->getRatio(); 159 | return call_user_func_array([$ratio, 'calculate' . $side], [$value, $this->format->getModulus()]); 160 | } 161 | 162 | /** 163 | * @param $width 164 | * @param $k_bitrate 165 | * @param $height 166 | * @return Representation 167 | * @throws InvalidArgumentException 168 | */ 169 | private function addRep($k_bitrate, $width, $height): Representation 170 | { 171 | return (new Representation)->setKiloBitrate($k_bitrate)->setResize($width, $height); 172 | } 173 | 174 | /** 175 | * @param array $reps 176 | */ 177 | private function sortReps(array &$reps): void 178 | { 179 | usort($reps, function (Representation $rep1, Representation $rep2) { 180 | $ascending = $rep1->getKiloBitrate() - $rep2->getKiloBitrate(); 181 | return $this->sort ? $ascending : -($ascending); 182 | }); 183 | } 184 | 185 | /** 186 | * @param bool $sort 187 | * @return array 188 | */ 189 | public function getCalculatedReps(bool $sort = false): array 190 | { 191 | $reps = []; 192 | foreach ($this->sides as $key => $height) { 193 | array_push($reps, $this->addRep($this->k_bitrate[$key], $this->computeSide($height, 'Width'), $height)); 194 | } 195 | 196 | if ($sort) { 197 | $this->sortReps($reps); 198 | } 199 | 200 | return $reps; 201 | } 202 | 203 | /** 204 | * @return Representation 205 | */ 206 | public function getOriginalRep(): Representation 207 | { 208 | $dimension = $this->getDimensions(); 209 | $width = $this->computeSide($dimension->getHeight(), 'Width'); 210 | $height = $this->computeSide($dimension->getWidth(), 'Height'); 211 | 212 | return $this->addRep($this->getKiloBitRate(), $width, $height); 213 | } 214 | 215 | /** 216 | * @return array 217 | */ 218 | public function getSides(): array 219 | { 220 | return $this->sides; 221 | } 222 | 223 | /** 224 | * @return array 225 | */ 226 | public function getKBitrate(): array 227 | { 228 | return $this->k_bitrate; 229 | } 230 | 231 | /** 232 | * Retrieve an external iterator reps 233 | * @return \Traversable An instance of an object implementing Iterator or Traversable 234 | */ 235 | public function getIterator(): Traversable 236 | { 237 | $reps = $this->getCalculatedReps(); 238 | array_push($reps, $this->getOriginalRep()); 239 | $this->sortReps($reps); 240 | 241 | return new \ArrayIterator($reps); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/HLSSubtitle.php: -------------------------------------------------------------------------------- 1 | path = $path; 65 | $this->language_name = $language_name; 66 | $this->language_code = $language_code; 67 | 68 | if(!in_array(pathinfo($path, PATHINFO_EXTENSION), static::SUPPORTED_EXTS)){ 69 | throw new InvalidArgumentException(sprintf("Unsupported input! Only %s files are acceptable.", implode(",", static::SUPPORTED_EXTS))); 70 | } 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | public function getPath(): string 77 | { 78 | return $this->path; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getLanguageName(): string 85 | { 86 | return $this->language_name; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getLanguageCode(): string 93 | { 94 | return $this->language_code; 95 | } 96 | /** 97 | * @return string|null 98 | */ 99 | public function getUri(): string 100 | { 101 | return $this->uri ?? $this->getBaseName(); 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | private function getFileName(): string 108 | { 109 | return pathinfo($this->path, PATHINFO_FILENAME); 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | private function getBaseName(): string 116 | { 117 | return pathinfo($this->path, PATHINFO_BASENAME); 118 | } 119 | 120 | /** 121 | * @param string|null $uri 122 | */ 123 | public function setUri(string $uri): void 124 | { 125 | $this->uri = $uri; 126 | } 127 | 128 | /** 129 | * @return bool 130 | */ 131 | public function isDefault(): bool 132 | { 133 | return $this->default; 134 | } 135 | 136 | /** 137 | * @param bool $default 138 | */ 139 | public function default(bool $default = true): void 140 | { 141 | $this->default = $default; 142 | } 143 | 144 | /** 145 | * @return bool 146 | */ 147 | public function isAutoSelect(): bool 148 | { 149 | return $this->auto_select; 150 | } 151 | 152 | /** 153 | * @param bool $auto_select 154 | */ 155 | public function autoSelect(bool $auto_select = true): void 156 | { 157 | $this->auto_select = $auto_select; 158 | } 159 | 160 | /** 161 | * @return bool 162 | */ 163 | public function isForce(): bool 164 | { 165 | return $this->force; 166 | } 167 | 168 | /** 169 | * @param bool $force 170 | */ 171 | public function force(bool $force = true): void 172 | { 173 | $this->force = $force; 174 | } 175 | 176 | 177 | /** 178 | * @param int $hls_version 179 | */ 180 | public function setHlsVersion(int $hls_version): void 181 | { 182 | $this->hls_version = $hls_version; 183 | } 184 | 185 | /** 186 | * @param int $media_sequence 187 | */ 188 | public function setMediaSequence(int $media_sequence): void 189 | { 190 | $this->media_sequence = $media_sequence; 191 | } 192 | 193 | /** 194 | * @param string $media_type 195 | */ 196 | public function setMediaType(string $media_type): void 197 | { 198 | $this->media_type = $media_type; 199 | } 200 | 201 | /** 202 | * @return string 203 | */ 204 | public function getGroupId(): string 205 | { 206 | return $this->group_id; 207 | } 208 | 209 | /** 210 | * @param string $group_id 211 | */ 212 | public function setGroupId(string $group_id): void 213 | { 214 | $this->group_id = $group_id; 215 | } 216 | 217 | /** 218 | * @return string 219 | */ 220 | public function getM3u8Uri(): string 221 | { 222 | return $this->m3u8_uri; 223 | } 224 | 225 | /** 226 | * @param string $m3u8_uri 227 | */ 228 | public function setM3u8Uri(string $m3u8_uri): void 229 | { 230 | $this->m3u8_uri = $m3u8_uri; 231 | } 232 | 233 | /** 234 | * @return string 235 | */ 236 | public function __toString() 237 | { 238 | $s_ext = "#EXT-X-MEDIA:"; 239 | 240 | $ext = [ 241 | "TYPE" => "SUBTITLES", 242 | "GROUP-ID" => "\"" . $this->group_id . "\"", 243 | "NAME" => "\"" . $this->language_name . "\"", 244 | "DEFAULT" => Utiles::convertBooleanToYesNo($this->isDefault()), 245 | "AUTOSELECT" => Utiles::convertBooleanToYesNo($this->isAutoSelect()), 246 | "FORCED" => Utiles::convertBooleanToYesNo($this->isForce()), 247 | "LANGUAGE" => "\"" . $this->language_code . "\"", 248 | "URI" => "\"" . $this->getM3u8Uri(). "\"", 249 | ]; 250 | Utiles::concatKeyValue($ext, "="); 251 | 252 | return $s_ext . implode(",", $ext); 253 | } 254 | 255 | public function generateM3U8File(string $path, float $duration, array $description = [], array $info = []): void 256 | { 257 | $ext_x = array_merge($description, [ 258 | "#EXT-X-TARGETDURATION" => intval($duration), 259 | "#EXT-X-VERSION" => $this->hls_version, 260 | "#EXT-X-MEDIA-SEQUENCE" => $this->media_sequence, 261 | "#EXT-X-PLAYLIST-TYPE" => $this->media_type, 262 | "#EXTINF" => implode(",", array_merge([number_format($duration, 1, '.', '')], $info)) 263 | ]); 264 | 265 | Utiles::concatKeyValue($ext_x, ":"); 266 | File::put($path, implode(PHP_EOL, array_merge([static::S_TAG], $ext_x, [$this->getUri(), static::E_TAG]))); 267 | 268 | if(!$this->m3u8_uri){ 269 | $this->setM3u8Uri(pathinfo($path, PATHINFO_BASENAME)); 270 | } 271 | 272 | if(!$this->uri){ 273 | File::copy($this->path, implode(DIRECTORY_SEPARATOR, [dirname($path), $this->getBaseName()])); 274 | } 275 | } 276 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📼 PHP FFmpeg - Video Streaming 2 | [![Total Downloads](https://img.shields.io/packagist/dt/aminyazdanpanah/php-ffmpeg-video-streaming.svg?style=flat)](https://packagist.org/packages/aminyazdanpanah/php-ffmpeg-video-streaming) 3 | 4 |

5 | 6 | This package utilizes **[FFmpeg](https://ffmpeg.org)** to bundle media content for online streaming, including DASH and 7 | HLS. Additionally, it provides the capability to implement **[DRM](https://en.wikipedia.org/wiki/Digital_rights_management)** for HLS packaging. The program offers a range of 8 | options to open files from cloud storage and save files to cloud storage as well. 9 | 10 | ## Documentation 11 | 12 | **[Full Documentation](https://www.quasarstream.com/op/php/ffmpeg-streaming/)** is available describing all features 13 | and components. 14 | 15 | ## Basic Usage 16 | 17 | ```php 18 | use Streaming\Representation; 19 | 20 | $r_360p = (new Representation)->setKiloBitrate(276)->setResize(640, 360); 21 | $r_480p = (new Representation)->setKiloBitrate(750)->setResize(854, 480); 22 | $r_720p = (new Representation)->setKiloBitrate(2048)->setResize(1280, 720); 23 | 24 | $video->hls() 25 | ->x264() 26 | ->addRepresentations([$r_360p, $r_480p, $r_720p]) 27 | ->save(); 28 | ``` 29 | 30 | ## Get from Basic and Pro packages for Video Streaming 31 | 32 |

33 | 34 | Our platform empowers businesses to expand their reach globally by delivering exceptional video streaming experiences. Enjoy unmatched reliability, scalability, and high-definition quality across a diverse range of devices, ensuring your content captivates audiences worldwide. 35 | 36 | 37 | ### Plans 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 150 | 151 | 152 |
Features / PlansBasicPro
Authentication
Access-control list (ACL)
Video On-Demand (HLS and DASH)
HLS Encryption(Single key and key rotation)
Video Quality Settings: Manually Choose from 144p to 4k or auto mode
Real-Time Progress Monitoring: progress bar to display the live upload and transcoding progress
Dark and light theme
Live Streaming: From Browser Webcam, IP Cameras, Live Streaming Software⛔️
Bespoke player design: Crafted to perfectly align with your brand identity and user preferences.⛔️
Add Subtitles and Audios: add different subtitle and audio files to stream⛔️
Monetization: Subscriptons/pay-per-view/ads⛔️
Advanced Analytics: Views/Watched hours/Visited countries and more⛔️
Robust DRM Systems: Widevine, FairPlay Streaming and PlayReady⛔️
Social Media Integration: Like, Comment, Share and embed videos⛔️
Cloud-based CDN: Accelerates content delivery worldwide through integration with major cloud storage providers such as Amazon S3, Google Cloud Storage, and Microsoft Azure.⛔️
Tailored features: We can integrate any specific functionality you require into your platform.⛔️
Support3 MonthsCustomizable
Online Demo See Online Demo Book Free Demo
Get GET CONTACT US
148 | We tailor OTT platforms to exact client specifications, offering flexible and affordable pricing. 149 |
153 |
154 | 155 | ## Contributors 156 | 157 | Your contribution is crucial to our success, regardless of its size. We appreciate your support and encourage you to 158 | read our **[CONTRIBUTING](https://github.com/quasarstream/php-ffmpeg-video-streaming/blob/master/CONTRIBUTING.md)** 159 | guide for detailed instructions on how to get involved. Together, we can make a significant impact. 160 | 161 | 162 | 163 | 164 | 165 | Made with [contrib.rocks](https://contrib.rocks). 166 | 167 | ## License 168 | 169 | The MIT License (MIT). See **[License File](https://github.com/quasarstream/php-ffmpeg-video-streaming/blob/master/LICENSE)** for more 170 | information. 171 | 172 | -------------------------------------------------------------------------------- /src/HLS.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Streaming; 13 | 14 | use Streaming\Filters\HLSFilter; 15 | use Streaming\Filters\StreamFilterInterface; 16 | 17 | class HLS extends Streaming 18 | { 19 | 20 | /** @var string */ 21 | private $hls_time = 10; 22 | 23 | /** @var bool */ 24 | private $hls_allow_cache = true; 25 | 26 | /** @var string */ 27 | private $hls_key_info_file = ""; 28 | 29 | /** @var string */ 30 | private $seg_sub_directory = ""; 31 | 32 | /** @var string */ 33 | private $hls_base_url = ""; 34 | 35 | /** @var int */ 36 | private $hls_list_size = 0; 37 | 38 | /** @var bool */ 39 | private $tmp_key_info_file = false; 40 | 41 | /** @var string */ 42 | public $master_playlist; 43 | 44 | /** @var string */ 45 | private $hls_segment_type = 'mpegts'; 46 | 47 | /** @var string */ 48 | private $hls_fmp4_init_filename = "init.mp4"; 49 | 50 | /** @var array */ 51 | private $stream_des = []; 52 | 53 | /** @var array */ 54 | private $flags = []; 55 | 56 | /** @var array */ 57 | private $subtitles = []; 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getSegSubDirectory(): string 63 | { 64 | return $this->seg_sub_directory; 65 | } 66 | 67 | /** 68 | * @param string $seg_sub_directory 69 | * @return HLS 70 | */ 71 | public function setSegSubDirectory(string $seg_sub_directory) 72 | { 73 | $this->seg_sub_directory = $seg_sub_directory; 74 | return $this; 75 | } 76 | 77 | /** 78 | * @param string $hls_time 79 | * @return HLS 80 | */ 81 | public function setHlsTime(string $hls_time): HLS 82 | { 83 | $this->hls_time = $hls_time; 84 | return $this; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function getHlsTime(): string 91 | { 92 | return $this->hls_time; 93 | } 94 | 95 | /** 96 | * @param bool $hls_allow_cache 97 | * @return HLS 98 | */ 99 | public function setHlsAllowCache(bool $hls_allow_cache): HLS 100 | { 101 | $this->hls_allow_cache = $hls_allow_cache; 102 | return $this; 103 | } 104 | 105 | /** 106 | * @return bool 107 | */ 108 | public function isHlsAllowCache(): bool 109 | { 110 | return $this->hls_allow_cache; 111 | } 112 | 113 | /** 114 | * @param string $hls_key_info_file 115 | * @return HLS 116 | */ 117 | public function setHlsKeyInfoFile(string $hls_key_info_file): HLS 118 | { 119 | $this->hls_key_info_file = $hls_key_info_file; 120 | return $this; 121 | } 122 | 123 | /** 124 | * @param string $save_to 125 | * @param string $url 126 | * @param int $key_rotation_period 127 | * @param string $search 128 | * @param int $length 129 | * @return HLS 130 | */ 131 | public function encryption(string $save_to, string $url, int $key_rotation_period = 0, string $search = ".ts' for writing", int $length = 16): HLS 132 | { 133 | $key_info = HLSKeyInfo::create($save_to, $url); 134 | $key_info->setLength($length); 135 | 136 | if ($key_rotation_period > 0) { 137 | $key_info->rotateKey($this->getMedia()->getFFMpegDriver(), $key_rotation_period, $search); 138 | array_push($this->flags, HLSFlag::PERIODIC_REKEY); 139 | } 140 | 141 | $this->setHlsKeyInfoFile((string)$key_info); 142 | $this->tmp_key_info_file = true; 143 | 144 | return $this; 145 | } 146 | 147 | public function subtitle(HLSSubtitle $subtitle) 148 | { 149 | array_push($this->subtitles, $subtitle); 150 | return $this; 151 | } 152 | 153 | /** 154 | * @param array $subtitles 155 | * @return HLS 156 | */ 157 | public function subtitles(array $subtitles): HLS 158 | { 159 | array_walk($subtitles, [$this, 'subtitle']); 160 | return $this; 161 | } 162 | 163 | /** 164 | * @return string 165 | */ 166 | public function getHlsKeyInfoFile(): string 167 | { 168 | return $this->hls_key_info_file; 169 | } 170 | 171 | /** 172 | * @param string $hls_base_url 173 | * @return HLS 174 | */ 175 | public function setHlsBaseUrl(string $hls_base_url): HLS 176 | { 177 | $this->hls_base_url = $hls_base_url; 178 | return $this; 179 | } 180 | 181 | /** 182 | * @return string 183 | */ 184 | public function getHlsBaseUrl(): string 185 | { 186 | return $this->hls_base_url; 187 | } 188 | 189 | /** 190 | * @param int $hls_list_size 191 | * @return HLS 192 | */ 193 | public function setHlsListSize(int $hls_list_size): HLS 194 | { 195 | $this->hls_list_size = $hls_list_size; 196 | return $this; 197 | } 198 | 199 | /** 200 | * @return int 201 | */ 202 | public function getHlsListSize(): int 203 | { 204 | return $this->hls_list_size; 205 | } 206 | 207 | /** 208 | * @param string $master_playlist 209 | * @param array $stream_des 210 | * @return HLS 211 | */ 212 | public function setMasterPlaylist(string $master_playlist, array $stream_des = []): HLS 213 | { 214 | $this->master_playlist = $master_playlist; 215 | $this->stream_des = $stream_des; 216 | 217 | return $this; 218 | } 219 | 220 | /** 221 | * @return HLS 222 | */ 223 | public function fragmentedMP4(): HLS 224 | { 225 | $this->setHlsSegmentType("fmp4"); 226 | return $this; 227 | } 228 | 229 | /** 230 | * @param string $hls_segment_type 231 | * @return HLS 232 | */ 233 | public function setHlsSegmentType(string $hls_segment_type): HLS 234 | { 235 | $this->hls_segment_type = $hls_segment_type; 236 | return $this; 237 | } 238 | 239 | /** 240 | * @return string 241 | */ 242 | public function getHlsSegmentType(): string 243 | { 244 | return $this->hls_segment_type; 245 | } 246 | 247 | /** 248 | * @param string $hls_fmp4_init_filename 249 | * @return HLS 250 | */ 251 | public function setHlsFmp4InitFilename(string $hls_fmp4_init_filename): HLS 252 | { 253 | $this->hls_fmp4_init_filename = $hls_fmp4_init_filename; 254 | return $this; 255 | } 256 | 257 | /** 258 | * @return string 259 | */ 260 | public function getHlsFmp4InitFilename(): string 261 | { 262 | return $this->hls_fmp4_init_filename; 263 | } 264 | 265 | /** 266 | * @param array $flags 267 | * @return HLS 268 | */ 269 | public function setFlags(array $flags): HLS 270 | { 271 | $this->flags = array_merge($this->flags, $flags); 272 | return $this; 273 | } 274 | 275 | /** 276 | * @return array 277 | */ 278 | public function getFlags(): array 279 | { 280 | return $this->flags; 281 | } 282 | 283 | /** 284 | * @return HLSFilter 285 | */ 286 | protected function getFilter(): StreamFilterInterface 287 | { 288 | return new HLSFilter($this); 289 | } 290 | 291 | /** 292 | * @return string 293 | */ 294 | protected function getPath(): string 295 | { 296 | $path = $this->getFilePath(); 297 | $reps = $this->getRepresentations(); 298 | 299 | if(!empty($this->subtitles)){ 300 | $this->generateSubs($path); 301 | } 302 | 303 | $this->savePlaylist($path . ".m3u8"); 304 | 305 | return $path . "_" . $reps->end()->getHeight() . "p.m3u8"; 306 | } 307 | 308 | /** 309 | * @param $path 310 | */ 311 | public function savePlaylist(string $path): void 312 | { 313 | $mater_playlist = new HLSPlaylist($this); 314 | $mater_playlist->save($this->master_playlist ?? $path, $this->stream_des); 315 | } 316 | 317 | /** 318 | * @param string $path 319 | */ 320 | private function generateSubs(string $path) 321 | { 322 | $this->stream_des = array_merge($this->stream_des, [PHP_EOL]); 323 | 324 | foreach ($this->subtitles as $subtitle) { 325 | if($subtitle instanceof HLSSubtitle){ 326 | $subtitle->generateM3U8File("{$path}_subtitles_{$subtitle->getLanguageCode()}.m3u8", $this->getDuration()); 327 | array_push($this->stream_des, (string)$subtitle); 328 | } 329 | } 330 | array_push($this->stream_des, PHP_EOL); 331 | 332 | $this->getRepresentations()->map(function (Representation $rep){ 333 | return $rep->setHlsStreamInfo(["SUBTITLES" => "\"" . $this->subtitles[0]->getGroupId() . "\""]); 334 | }); 335 | } 336 | 337 | /** 338 | * @return float 339 | */ 340 | private function getDuration():float 341 | { 342 | return $this->getMedia()->getFormat()->get("duration", 0); 343 | } 344 | 345 | /** 346 | * Clear key info file if is a temp file 347 | */ 348 | public function __destruct() 349 | { 350 | if ($this->tmp_key_info_file) { 351 | File::remove($this->getHlsKeyInfoFile()); 352 | } 353 | 354 | parent::__destruct(); 355 | } 356 | } --------------------------------------------------------------------------------