├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── run-tests.yml ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── config.php └── src ├── Drivers ├── InteractsWithFilters.php ├── InteractsWithMediaStreams.php ├── PHPFFMpeg.php └── UnknownDurationException.php ├── Exporters ├── EncodingException.php ├── EncryptsHLSSegments.php ├── HLSExporter.php ├── HLSPlaylistGenerator.php ├── HLSVideoFilters.php ├── HandlesAdvancedMedia.php ├── HandlesConcatenation.php ├── HandlesFrames.php ├── HandlesTimelapse.php ├── HasProgressListener.php ├── MediaExporter.php ├── NoFormatException.php ├── PlaylistGenerator.php └── VTTPreviewThumbnailsGenerator.php ├── FFMpeg ├── AdvancedMedia.php ├── AdvancedOutputMapping.php ├── AudioMedia.php ├── CopyFormat.php ├── CopyVideoFormat.php ├── FFProbe.php ├── ImageFormat.php ├── InteractsWithBeforeSavingCallbacks.php ├── InteractsWithHttpHeaders.php ├── InteractsWithInputPath.php ├── LegacyFilterMapping.php ├── NullFormat.php ├── ProgressListenerDecorator.php ├── RebuildsCommands.php ├── StdListener.php ├── VideoMedia.php └── VideoProgressListenerDecorator.php ├── Filesystem ├── Disk.php ├── HasInputOptions.php ├── Media.php ├── MediaCollection.php ├── MediaOnNetwork.php └── TemporaryDirectories.php ├── Filters ├── TileFactory.php ├── TileFilter.php ├── WatermarkFactory.php └── WatermarkFilter.php ├── Http └── DynamicHLSPlaylist.php ├── MediaOpener.php └── Support ├── FFMpeg.php ├── MediaOpenerFactory.php ├── ProcessOutput.php ├── ServiceProvider.php └── StreamParser.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [pascalbaljet] 2 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | concurrency: ci-${{ github.ref }} 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | os: [ubuntu-20.04] 17 | php: [8.4, 8.3, 8.2, 8.1] 18 | laravel: ['10.*', '11.*', '12.*'] 19 | ffmpeg: [5.0, 4.4] 20 | dependency-version: [prefer-lowest, prefer-stable] 21 | include: 22 | - laravel: 10.* 23 | testbench: 8.* 24 | - laravel: 11.* 25 | testbench: 9.* 26 | - laravel: 12.* 27 | testbench: 10.* 28 | exclude: 29 | - laravel: 11.* 30 | php: 8.1 31 | - laravel: 10.* 32 | php: 8.4 33 | - laravel: 12.* 34 | php: 8.1 35 | 36 | name: ${{ matrix.os }} - P${{ matrix.php }} - L${{ matrix.laravel }} - FF${{ matrix.ffmpeg }} - ${{ matrix.dependency-version }} 37 | 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | 42 | - name: Setup PHP 43 | uses: shivammathur/setup-php@v2 44 | with: 45 | php-version: ${{ matrix.php }} 46 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql, fileinfo 47 | coverage: none 48 | 49 | - name: Install FFmpeg 50 | uses: Iamshankhadeep/setup-ffmpeg@ffmpeg-5.0-20220119 51 | with: 52 | version: ${{ matrix.ffmpeg }} 53 | id: setup-ffmpeg 54 | 55 | - name: Install dependencies 56 | run: | 57 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 58 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 59 | 60 | - name: Cache dependencies 61 | uses: actions/cache@v4 62 | with: 63 | path: ~/.composer/cache/files 64 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}-dep-${{ matrix.dependency-version }} 65 | 66 | - name: Execute tests 67 | run: vendor/bin/phpunit --order-by random 68 | env: 69 | FFMPEG_TEMPORARY_FILES_ROOT: ${{ github.workspace }} 70 | FFMPEG_TEMPORARY_ENCRYPTED_HLS: /dev/shm 71 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `pbmedia/laravel-ffmpeg` will be documented in this file 4 | 5 | ## 8.1.2 - 2022-05-23 6 | 7 | - Don't resolve driver until needed. 8 | 9 | ## 8.1.1 - 2022-05-13 10 | 11 | - Bugfix for parsing the average frame rate. 12 | 13 | ## 8.1.0 - 2022-05-12 14 | 15 | - You may now specify a separate temporary disk for processing HLS exports. 16 | 17 | ## 8.0.2 - 2022-05-12 18 | 19 | - Fix for getting the previous exception while it doesn't exist. 20 | 21 | ## 8.0.1 - 2022-02-22 22 | 23 | - The configured temporary directory is now passed to the underlying driver. 24 | 25 | ## 8.0.0 - 2022-02-10 26 | 27 | Support for Laravel 9, dropped support for Laravel 8 and earlier. 28 | 29 | ### Upgrading to v8 30 | 31 | * As Laravel 9 has migrated from [Flysystem 1.x to 3.x](https://laravel.com/docs/9.x/upgrade#flysystem-3), this version is not compatible with Laravel 8 or earlier. 32 | * If you're using the [Watermark manipulation](#watermark-manipulation) feature, make sure you upgrade [`spatie/image`](https://github.com/spatie/image) to v2. 33 | * The `set_command_and_error_output_on_exception` configuration key now defaults to `true`, making exceptions more informative. Read more at the [Handling exceptions](#handling-exceptions) section. 34 | * The `enable_logging` configuration key has been replaced by `log_channel` to choose the log channel used when writing messages to the logs. If you still want to disable logging entirely, you may set the new configuration key to `false`. 35 | * The *segment length* and *keyframe interval* of [HLS exports](#HLS) should be `2` or more; less is not supported anymore. 36 | 37 | ## 7.8.1 - 2022-02-10 38 | 39 | ### Added 40 | 41 | - Support for opening uploaded files 42 | 43 | ## 7.8.0 - 2022-02-09 44 | 45 | ### Added 46 | 47 | - Support for the [modernized php-ffmpeg release](https://github.com/PHP-FFMpeg/PHP-FFMpeg/releases/tag/v1.0.0) 48 | 49 | ## 7.7.3 - 2022-02-07 50 | 51 | ### Added 52 | 53 | - Abilty to disable the threads filter from the config (thanks @ibrainventures) 54 | 55 | ## 7.7.2 - 2021-01-12 56 | 57 | ### Fixed 58 | 59 | - Fix for getting the duration of a file opened with the `openUrl` method. 60 | 61 | ## 7.7.1 - 2021-01-03 62 | 63 | ### Fixed 64 | 65 | - Fix for missing `$remaining` and `$rate` values when using the progress handler on exports with multiple inputs/outputs. 66 | 67 | ## 7.7.0 - 2021-12-31 68 | 69 | ### Added 70 | 71 | - Added Tile filter and factory 72 | - Support for exporting frames using the Tile filter 73 | - Bugfix for exporting loops using external disks 74 | 75 | ## 7.6.0 - 2021-12-20 76 | 77 | ### Added 78 | 79 | - Support for PHP 8.1 80 | 81 | ### Removed 82 | 83 | - Support for PHP 7.3 84 | - Support for Laravel 6 and 7 85 | 86 | ## 7.5.12 - 2021-07-05 87 | 88 | ### Added 89 | 90 | - Fix for passing additional parameters to a format when using HLS exports 91 | 92 | ## 7.5.11 - 2021-04-25 93 | 94 | ### Added 95 | 96 | - Added `CopyVideoFormat` format class 97 | 98 | ## 7.5.10 - 2021-03-31 99 | 100 | ### Added 101 | 102 | - Add ability to disable -b:v (thanks @marbocub) 103 | 104 | ## 7.5.9 - 2021-03-19 105 | 106 | ### Fixed 107 | 108 | - Prevent duplicate encryption key listeners 109 | 110 | ## 7.5.8 - 2021-03-17 111 | 112 | ### Fixed 113 | 114 | - Bugfix for creating temporary directories on Windows 115 | - Bugfix for HLS exports with custom framerate 116 | 117 | ## 7.5.7 - 2021-03-08 118 | 119 | ### Fixed 120 | 121 | - Prevent HLS key rotation on non-rotating exports (thanks @marbocub) 122 | 123 | ## 7.5.6 - 2021-03-03 124 | 125 | ### Fixed 126 | 127 | - Bugfix for HLS exports to S3 disks (thanks @chistel) 128 | - Prevent duplicate progress handler when using loops 129 | 130 | ## 7.5.5 - 2021-01-18 131 | 132 | ### Added 133 | 134 | - Added `beforeSaving` method to add callbacks 135 | 136 | ## 7.5.4 - 2021-01-07 137 | 138 | ### Added 139 | 140 | - Added fourth optional argument to the resize method whether or not to force the use of standards ratios 141 | - Improved docs 142 | - Small refactor 143 | 144 | ## 7.5.3 - 2021-01-02 145 | 146 | ### Added 147 | 148 | - Support for custom encryption filename when using non-rotating keys 149 | 150 | ## 7.5.2 - 2021-01-02 151 | 152 | ### Added 153 | 154 | - Support for setting a custom path for temporary directories 155 | - GitHub Actions now runs on Windows in addition to Ubuntu 156 | 157 | ### Fixed 158 | 159 | - HLS Encryption I/O improvements 160 | - Path normalization on Windows, which solves common problems with HLS and watermarks 161 | - Some refactors and documentation improvements 162 | 163 | ## 7.5.1 - 2020-12-24 164 | 165 | ### Added 166 | 167 | - Support for codec in HLS playlist 168 | - Fixed bitrate bug in HLS playlist 169 | 170 | ## 7.5.0 - 2020-12-22 171 | 172 | ### Added 173 | 174 | - Support for PHP 8.0. 175 | - Encrypted HLS. 176 | - New `getProcessOutput` method to analyze media. 177 | - Support for dynamic HLS playlists. 178 | 179 | ### Deprecated 180 | 181 | - Nothing 182 | 183 | ### Fixed 184 | 185 | - Nothing 186 | 187 | ### Removed 188 | 189 | - Support for PHP 7.2 190 | 191 | ## 7.4.1 - 2020-10-26 192 | 193 | ### Added 194 | 195 | - Better exceptions 196 | - dd() improvements 197 | 198 | ### Deprecated 199 | 200 | - Nothing 201 | 202 | ### Fixed 203 | 204 | - Nothing 205 | 206 | ### Removed 207 | 208 | - Nothing 209 | 210 | ## 7.4.0 - 2020-10-25 211 | 212 | ### Added 213 | 214 | - Watermark manipulations 215 | - Dump and die 216 | - Resize filter shortcut 217 | - HLS export with multiple filters per format 218 | 219 | ### Deprecated 220 | 221 | - Nothing 222 | 223 | ### Fixed 224 | 225 | - Nothing 226 | 227 | ### Removed 228 | 229 | - Nothing 230 | 231 | ## 7.3.0 - 2020-10-16 232 | 233 | ### Added 234 | 235 | - Built-in support for watermarks. 236 | 237 | ### Deprecated 238 | 239 | - Nothing 240 | 241 | ### Fixed 242 | 243 | - Nothing 244 | 245 | ### Removed 246 | 247 | - Nothing 248 | 249 | ## 7.2.0 - 2020-09-17 250 | 251 | ### Added 252 | 253 | - Support for inputs from the web 254 | 255 | ### Deprecated 256 | 257 | - Nothing 258 | 259 | ### Fixed 260 | 261 | - Nothing 262 | 263 | ### Removed 264 | 265 | - Nothing 266 | 267 | ## 7.1.0 - 2020-09-04 268 | 269 | ### Added 270 | 271 | - Support for Laravel 8.0 272 | 273 | ### Deprecated 274 | 275 | - Nothing 276 | 277 | ### Fixed 278 | 279 | - Nothing 280 | 281 | ### Removed 282 | 283 | - Nothing 284 | 285 | ## 7.0.5 - 2020-07-04 286 | 287 | ### Added 288 | 289 | - Added `CopyFormat` to export a file without transcoding. 290 | 291 | ### Deprecated 292 | 293 | - Nothing 294 | 295 | ### Fixed 296 | 297 | - Nothing 298 | 299 | ### Removed 300 | 301 | - Nothing 302 | 303 | ## 7.0.4 - 2020-06-03 304 | 305 | ### Added 306 | 307 | - Added an `each` method to the `MediaOpener` 308 | 309 | ### Deprecated 310 | 311 | - Nothing 312 | 313 | ### Fixed 314 | 315 | - Nothing 316 | 317 | ### Removed 318 | 319 | - Nothing 320 | 321 | ## 7.0.3 - 2020-06-01 322 | 323 | ### Added 324 | 325 | - Added a `MediaOpenerFactory` to support pre v7.0 facade 326 | 327 | ### Deprecated 328 | 329 | - Nothing 330 | 331 | ### Fixed 332 | 333 | - Nothing 334 | 335 | ### Removed 336 | 337 | - Nothing 338 | 339 | ## 7.0.2 - 2020-06-01 340 | 341 | ### Added 342 | 343 | - Nothing 344 | 345 | ### Deprecated 346 | 347 | - Nothing 348 | 349 | ### Fixed 350 | 351 | - Audio bugfix for HLS exports with filters 352 | 353 | ### Removed 354 | 355 | - Nothing 356 | 357 | 358 | ## 7.0.1 - 2020-05-28 359 | 360 | ### Added 361 | 362 | - Nothing 363 | 364 | ### Deprecated 365 | 366 | - Nothing 367 | 368 | ### Fixed 369 | 370 | - Fixed HLS playlist creation on Windows hosts 371 | 372 | ### Removed 373 | 374 | - Nothing 375 | 376 | ## 7.0.0 - 2020-05-26 377 | 378 | ### Added 379 | 380 | - Support for both Laravel 6.0 and Laravel 7.0 381 | - Support for multiple inputs/outputs including mapping and complex filters 382 | - Concatenation with transcoding 383 | - Concatenation without transcoding 384 | - Support for image sequences (timelapse) 385 | - Bitrate, framerate and resolution data in HLS playlist 386 | - Execute one job for HLS export instead of one job for each format 387 | - Custom playlist/segment naming pattern for HLS export 388 | - Support for disabling log 389 | 390 | ### Deprecated 391 | 392 | - Nothing 393 | 394 | ### Fixed 395 | 396 | - Improved progress monitoring 397 | - Improved handling of remote filesystems 398 | 399 | ### Removed 400 | 401 | - Nothing 402 | 403 | ## 6.0.0 - 2020-03-03 404 | 405 | ### Added 406 | 407 | - Support for Laravel 7.0 408 | 409 | ### Deprecated 410 | 411 | - Nothing 412 | 413 | ### Fixed 414 | 415 | - Nothing 416 | 417 | ### Removed 418 | 419 | - Support for Laravel 6.0 420 | 421 | ## 5.0.0 - 2019-09-03 422 | 423 | ### Added 424 | 425 | - Support for Laravel 6.0 426 | 427 | ### Deprecated 428 | 429 | - Nothing 430 | 431 | ### Fixed 432 | 433 | - Nothing 434 | 435 | ### Removed 436 | 437 | - Support for PHP 7.1 438 | - Support for Laravel 5.8 439 | 440 | ### Security 441 | 442 | - Nothing 443 | 444 | ## 4.1.0 - 2019-08-28 445 | 446 | ### Added 447 | 448 | - Nothing 449 | 450 | ### Deprecated 451 | 452 | - Nothing 453 | 454 | ### Fixed 455 | 456 | - Lower memory usage when opening remote files 457 | 458 | ### Removed 459 | 460 | - Nothing 461 | 462 | ### Security 463 | 464 | - Nothing 465 | 466 | ## 4.0.1 - 2019-06-17 467 | 468 | ### Added 469 | 470 | - Nothing 471 | 472 | ### Deprecated 473 | 474 | - Nothing 475 | 476 | ### Fixed 477 | 478 | - Support for php-ffmpeg 0.14 479 | 480 | ### Removed 481 | 482 | - Nothing 483 | 484 | ### Security 485 | 486 | - Nothing 487 | 488 | ## 4.0.0 - 2019-02-26 489 | 490 | ### Added 491 | 492 | - Support for Laravel 5.8. 493 | - Support for PHP 7.3. 494 | 495 | ### Deprecated 496 | 497 | - Nothing 498 | 499 | ### Fixed 500 | 501 | - Nothing 502 | 503 | ### Removed 504 | 505 | - Nothing 506 | 507 | ### Security 508 | 509 | - Nothing 510 | 511 | ## 3.0.0 - 2018-09-03 512 | 513 | ### Added 514 | 515 | - Support for Laravel 5.7. 516 | 517 | ### Deprecated 518 | 519 | - Nothing 520 | 521 | ### Fixed 522 | 523 | - Nothing 524 | 525 | ### Removed 526 | 527 | - Nothing 528 | 529 | ### Security 530 | 531 | - Nothing 532 | 533 | ## 2.1.0 - 2018-04-10 534 | 535 | ### Added 536 | 537 | - Option to disable format sorting in HLS exporter. 538 | 539 | ### Deprecated 540 | 541 | - Nothing 542 | 543 | ### Fixed 544 | 545 | - Nothing 546 | 547 | ### Removed 548 | 549 | - Nothing 550 | 551 | ### Security 552 | 553 | - Nothing 554 | 555 | ## 2.0.1 - 2018-02-30 556 | 557 | ### Added 558 | 559 | - Nothing 560 | 561 | ### Deprecated 562 | 563 | - Nothing 564 | 565 | ### Fixed 566 | 567 | - Symfony 4.0 workaround 568 | 569 | ### Removed 570 | 571 | - Nothing 572 | 573 | ### Security 574 | 575 | - Nothing 576 | 577 | ## 2.0.0 - 2018-02-19 578 | 579 | ### Added 580 | 581 | - Support for Laravel 5.6. 582 | 583 | ### Deprecated 584 | 585 | - Nothing 586 | 587 | ### Fixed 588 | 589 | - Nothing 590 | 591 | ### Removed 592 | 593 | - Support for Laravel 5.5 and earlier. 594 | 595 | ### Security 596 | 597 | - Nothing 598 | 599 | ## 1.3.0 - 2017-11-13 600 | 601 | ### Added 602 | 603 | - Support for monitoring the progress of a HLS Export. 604 | 605 | ### Deprecated 606 | 607 | - Nothing 608 | 609 | ### Fixed 610 | 611 | - Some refactoring 612 | 613 | ### Removed 614 | 615 | - Nothing 616 | 617 | ### Security 618 | 619 | - Nothing 620 | 621 | ## 1.2.0 - 2017-11-13 622 | 623 | ### Added 624 | 625 | - Support for adding filters per format in the `HLSPlaylistExporter` class by giving access to the `Media` object through a callback. 626 | 627 | ### Deprecated 628 | 629 | - Nothing 630 | 631 | ### Fixed 632 | 633 | - Some refactoring 634 | 635 | ### Removed 636 | 637 | - Nothing 638 | 639 | ### Security 640 | 641 | - Nothing 642 | 643 | ## 1.1.12 - 2017-09-05 644 | 645 | ### Added 646 | 647 | - Support for Package Discovery in Laravel 5.5. 648 | 649 | ### Deprecated 650 | 651 | - Nothing 652 | 653 | ### Fixed 654 | 655 | - Some refactoring 656 | 657 | ### Removed 658 | 659 | - Nothing 660 | 661 | ### Security 662 | 663 | - Nothing 664 | 665 | ## 1.1.11 - 2017-08-31 666 | 667 | ### Added 668 | 669 | - Added `withVisibility` method to the MediaExporter 670 | 671 | ### Deprecated 672 | 673 | - Nothing 674 | 675 | ### Fixed 676 | 677 | - Some refactoring 678 | 679 | ### Removed 680 | 681 | - Nothing 682 | 683 | ### Security 684 | 685 | - Nothing 686 | 687 | ## 1.1.10 - 2017-08-16 688 | 689 | ### Added 690 | 691 | - Added `getFirstStream()` method to the `Media` class 692 | 693 | ### Deprecated 694 | 695 | - Nothing 696 | 697 | ### Fixed 698 | 699 | - Some refactoring 700 | 701 | ### Removed 702 | 703 | - Nothing 704 | 705 | ### Security 706 | 707 | - Nothing 708 | 709 | ## 1.1.9 - 2017-07-10 710 | 711 | ### Added 712 | 713 | - Support for custom filters in the `Media` class 714 | 715 | ### Deprecated 716 | 717 | - Nothing 718 | 719 | ### Fixed 720 | 721 | - Nothing 722 | 723 | ### Removed 724 | 725 | - Nothing 726 | 727 | ### Security 728 | 729 | - Nothing 730 | 731 | ## 1.1.8 - 2017-05-22 732 | 733 | ### Added 734 | 735 | - `getDurationInMiliseconds` method in Media class 736 | 737 | ### Deprecated 738 | 739 | - Nothing 740 | 741 | ### Fixed 742 | 743 | - Nothing 744 | 745 | ### Removed 746 | 747 | - Nothing 748 | 749 | ### Security 750 | 751 | - Nothing 752 | 753 | ## 1.1.7 - 2017-05-22 754 | 755 | ### Added 756 | 757 | - `fromFilesystem` method in FFMpeg class 758 | 759 | ### Deprecated 760 | 761 | - Nothing 762 | 763 | ### Fixed 764 | 765 | - Fallback to format properties in `getDurationInSeconds` method (Media class) 766 | 767 | ### Removed 768 | 769 | - Nothing 770 | 771 | ### Security 772 | 773 | - Nothing 774 | 775 | ## 1.1.6 - 2017-05-11 776 | 777 | ### Added 778 | 779 | - `cleanupTemporaryFiles` method 780 | 781 | ### Deprecated 782 | 783 | - Nothing 784 | 785 | ### Fixed 786 | 787 | - Nothing 788 | 789 | ### Removed 790 | 791 | - Nothing 792 | 793 | ### Security 794 | 795 | - Nothing 796 | 797 | ## 1.1.5 - 2017-03-20 798 | 799 | ### Added 800 | 801 | - Nothing 802 | 803 | ### Deprecated 804 | 805 | - Nothing 806 | 807 | ### Fixed 808 | 809 | - Bugfix for saving on remote disks 810 | 811 | ### Removed 812 | 813 | - Nothing 814 | 815 | ### Security 816 | 817 | - Nothing 818 | 819 | ## 1.1.4 - 2017-01-29 820 | 821 | ### Added 822 | 823 | - Nothing 824 | 825 | ### Deprecated 826 | 827 | - Nothing 828 | 829 | ### Fixed 830 | 831 | - Support for php-ffmpeg 0.8.0 832 | 833 | ### Removed 834 | 835 | - Nothing 836 | 837 | ### Security 838 | 839 | - Nothing 840 | 841 | ## 1.1.3 - 2017-01-05 842 | 843 | ### Added 844 | 845 | - Nothing 846 | 847 | ### Deprecated 848 | 849 | - Nothing 850 | 851 | ### Fixed 852 | 853 | - HLS segment playlists output path is now relative 854 | 855 | ### Removed 856 | 857 | - Nothing 858 | 859 | ### Security 860 | 861 | - Nothing 862 | 863 | ## 1.1.2 - 2017-01-05 864 | 865 | ### Added 866 | 867 | - Added 'getDurationInSeconds' method to Media class. 868 | 869 | ### Deprecated 870 | 871 | - Nothing 872 | 873 | ### Fixed 874 | 875 | - Nothing 876 | 877 | ### Removed 878 | 879 | - Nothing 880 | 881 | ### Security 882 | 883 | - Nothing 884 | -------------------------------------------------------------------------------- /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 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **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. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Protone Media B.V. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel FFMpeg 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/pbmedia/laravel-ffmpeg.svg?style=flat-square)](https://packagist.org/packages/pbmedia/laravel-ffmpeg) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | ![run-tests](https://github.com/protonemedia/laravel-ffmpeg/workflows/run-tests/badge.svg) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/pbmedia/laravel-ffmpeg.svg?style=flat-square)](https://packagist.org/packages/pbmedia/laravel-ffmpeg) 7 | 8 | This package provides an integration with FFmpeg for Laravel 10. [Laravel's Filesystem](http://laravel.com/docs/9.x/filesystem) handles the storage of the files. 9 | 10 | 11 | ## Sponsor Us 12 | 13 | [](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-ffmpeg) 14 | 15 | ❤️ We proudly support the community by developing Laravel packages and giving them away for free. If this package saves you time or if you're relying on it professionally, please consider [sponsoring the maintenance and development](https://github.com/sponsors/pascalbaljet) and check out our latest premium package: [Inertia Table](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-ffmpeg). Keeping track of issues and pull requests takes time, but we're happy to help! 16 | 17 | 18 | ## Features 19 | * Super easy wrapper around [PHP-FFMpeg](https://github.com/PHP-FFMpeg/PHP-FFMpeg), including support for filters and other advanced features. 20 | * Integration with [Laravel's Filesystem](http://laravel.com/docs/9.x/filesystem), [configuration system](https://laravel.com/docs/9.x/configuration) and [logging handling](https://laravel.com/docs/9.x/errors). 21 | * Compatible with Laravel 10, support for [Package Discovery](https://laravel.com/docs/9.x/packages#package-discovery). 22 | * Built-in support for HLS. 23 | * Built-in support for encrypted HLS (AES-128) and rotating keys (optional). 24 | * Built-in support for concatenation, multiple inputs/outputs, image sequences (timelapse), complex filters (and mapping), frame/thumbnail exports. 25 | * Built-in support for watermarks (positioning and manipulation). 26 | * Built-in support for creating a mosaic/sprite/tile from a video. 27 | * Built-in support for generating *VTT Preview Thumbnail* files. 28 | * Requires PHP 8.1 or higher. 29 | * Tested with FFmpeg 4.4 and 5.0. 30 | 31 | ## Installation 32 | 33 | Verify you have the latest version of FFmpeg installed: 34 | 35 | ```bash 36 | ffmpeg -version 37 | ``` 38 | 39 | You can install the package via composer: 40 | 41 | ```bash 42 | composer require pbmedia/laravel-ffmpeg 43 | ``` 44 | 45 | Add the Service Provider and Facade to your ```app.php``` config file if you're not using Package Discovery. 46 | 47 | ```php 48 | // config/app.php 49 | 50 | 'providers' => [ 51 | ... 52 | ProtoneMedia\LaravelFFMpeg\Support\ServiceProvider::class, 53 | ... 54 | ]; 55 | 56 | 'aliases' => [ 57 | ... 58 | 'FFMpeg' => ProtoneMedia\LaravelFFMpeg\Support\FFMpeg::class 59 | ... 60 | ]; 61 | ``` 62 | 63 | Publish the config file using the artisan CLI tool: 64 | 65 | ```bash 66 | php artisan vendor:publish --provider="ProtoneMedia\LaravelFFMpeg\Support\ServiceProvider" 67 | ``` 68 | 69 | ## Upgrading to v8 70 | 71 | * The `set_command_and_error_output_on_exception` configuration key now defaults to `true`, making exceptions more informative. Read more at the [Handling exceptions](#handling-exceptions) section. 72 | * The `enable_logging` configuration key has been replaced by `log_channel` to choose the log channel used when writing messages to the logs. If you still want to disable logging entirely, you may set the new configuration key to `false`. 73 | * The *segment length* and *keyframe interval* of [HLS exports](#HLS) should be `2` or more; less is not supported anymore. 74 | * As Laravel 9 has migrated from [Flysystem 1.x to 3.x](https://laravel.com/docs/9.x/upgrade#flysystem-3), this version is not compatible with Laravel 8 or earlier. 75 | * If you're using the [Watermark manipulation](#watermark-manipulation) feature, make sure you upgrade [`spatie/image`](https://github.com/spatie/image) to v2. 76 | 77 | ## Upgrading to v7 78 | 79 | * The namespace has changed to `ProtoneMedia\LaravelFFMpeg`, the facade has been renamed to `ProtoneMedia\LaravelFFMpeg\Support\FFMpeg`, and the Service Provider has been renamed to `ProtoneMedia\LaravelFFMpeg\Support\ServiceProvider`. 80 | * Chaining exports are still supported, but you have to reapply filters for each export. 81 | * HLS playlists now include bitrate, framerate and resolution data. The segments also use a new naming pattern ([read more](#using-custom-segment-patterns)). Please verify your exports still work in your player. 82 | * HLS export is now executed as *one* job instead of exporting each format/stream separately. This uses FFMpeg's `map` and `filter_complex` features. It might be sufficient to replace all calls to `addFilter` with `addLegacyFilter`, but some filters should be migrated manually. Please read the [documentation on HLS](#hls) to find out more about adding filters. 83 | 84 | ## Usage 85 | 86 | Convert an audio or video file: 87 | 88 | ```php 89 | FFMpeg::fromDisk('songs') 90 | ->open('yesterday.mp3') 91 | ->export() 92 | ->toDisk('converted_songs') 93 | ->inFormat(new \FFMpeg\Format\Audio\Aac) 94 | ->save('yesterday.aac'); 95 | ``` 96 | 97 | Instead of the ```fromDisk()``` method you can also use the ```fromFilesystem()``` method, where ```$filesystem``` is an instance of ```Illuminate\Contracts\Filesystem\Filesystem```. 98 | 99 | ```php 100 | $media = FFMpeg::fromFilesystem($filesystem)->open('yesterday.mp3'); 101 | ``` 102 | 103 | ### Progress monitoring 104 | 105 | You can monitor the transcoding progress. Use the ```onProgress``` method to provide a callback, which gives you the completed percentage. In previous versions of this package you had to pass the callback to the format object. 106 | 107 | ```php 108 | FFMpeg::open('steve_howe.mp4') 109 | ->export() 110 | ->onProgress(function ($percentage) { 111 | echo "{$percentage}% transcoded"; 112 | }); 113 | ``` 114 | 115 | The callback may also expose `$remaining` (in seconds) and `$rate`: 116 | 117 | ```php 118 | FFMpeg::open('steve_howe.mp4') 119 | ->export() 120 | ->onProgress(function ($percentage, $remaining, $rate) { 121 | echo "{$remaining} seconds left at rate: {$rate}"; 122 | }); 123 | ``` 124 | 125 | ### Opening uploaded files 126 | 127 | You can open uploaded files directly from the `Request` instance. It's probably better to first save the uploaded file in case the request aborts, but if you want to, you can open a `UploadedFile` instance: 128 | 129 | ```php 130 | class UploadVideoController 131 | { 132 | public function __invoke(Request $request) 133 | { 134 | FFMpeg::open($request->file('video')); 135 | } 136 | } 137 | ``` 138 | 139 | ### Open files from the web 140 | 141 | You can open files from the web by using the `openUrl` method. You can specify custom HTTP headers with the optional second parameter: 142 | 143 | ```php 144 | FFMpeg::openUrl('https://videocoursebuilder.com/lesson-1.mp4'); 145 | 146 | FFMpeg::openUrl('https://videocoursebuilder.com/lesson-2.mp4', [ 147 | 'Authorization' => 'Basic YWRtaW46MTIzNA==', 148 | ]); 149 | ``` 150 | 151 | ### Handling exceptions 152 | 153 | When the encoding fails, a `ProtoneMedia\LaravelFFMpeg\Exporters\EncodingException` shall be thrown, which extends the underlying `FFMpeg\Exception\RuntimeException` class. This class has two methods that can help you identify the problem. Using the `getCommand` method, you can get the executed command with all parameters. The `getErrorOutput` method gives you a full output log. 154 | 155 | In previous versions of this package, the message of the exception was always *Encoding failed*. You can downgrade to this message by updating the `set_command_and_error_output_on_exception` configuration key to `false`. 156 | 157 | ```php 158 | try { 159 | FFMpeg::open('yesterday.mp3') 160 | ->export() 161 | ->inFormat(new Aac) 162 | ->save('yesterday.aac'); 163 | } catch (EncodingException $exception) { 164 | $command = $exception->getCommand(); 165 | $errorLog = $exception->getErrorOutput(); 166 | } 167 | ``` 168 | 169 | ### Filters 170 | 171 | You can add filters through a ```Closure``` or by using PHP-FFMpeg's Filter objects: 172 | 173 | ```php 174 | use FFMpeg\Filters\Video\VideoFilters; 175 | 176 | FFMpeg::fromDisk('videos') 177 | ->open('steve_howe.mp4') 178 | ->addFilter(function (VideoFilters $filters) { 179 | $filters->resize(new \FFMpeg\Coordinate\Dimension(640, 480)); 180 | }) 181 | ->export() 182 | ->toDisk('converted_videos') 183 | ->inFormat(new \FFMpeg\Format\Video\X264) 184 | ->save('small_steve.mkv'); 185 | 186 | // or 187 | 188 | $start = \FFMpeg\Coordinate\TimeCode::fromSeconds(5) 189 | $clipFilter = new \FFMpeg\Filters\Video\ClipFilter($start); 190 | 191 | FFMpeg::fromDisk('videos') 192 | ->open('steve_howe.mp4') 193 | ->addFilter($clipFilter) 194 | ->export() 195 | ->toDisk('converted_videos') 196 | ->inFormat(new \FFMpeg\Format\Video\X264) 197 | ->save('short_steve.mkv'); 198 | ``` 199 | 200 | You can also call the `addFilter` method *after* the `export` method: 201 | 202 | ```php 203 | use FFMpeg\Filters\Video\VideoFilters; 204 | 205 | FFMpeg::fromDisk('videos') 206 | ->open('steve_howe.mp4') 207 | ->export() 208 | ->toDisk('converted_videos') 209 | ->inFormat(new \FFMpeg\Format\Video\X264) 210 | ->addFilter(function (VideoFilters $filters) { 211 | $filters->resize(new \FFMpeg\Coordinate\Dimension(640, 480)); 212 | }) 213 | ->save('small_steve.mkv'); 214 | ``` 215 | 216 | #### Resizing 217 | 218 | Since resizing is a common operation, we've added a dedicated method for it: 219 | 220 | ```php 221 | FFMpeg::open('steve_howe.mp4') 222 | ->export() 223 | ->inFormat(new \FFMpeg\Format\Video\X264) 224 | ->resize(640, 480) 225 | ->save('steve_howe_resized.mp4'); 226 | ``` 227 | The first argument is the width, and the second argument the height. The optional third argument is the mode. You can choose between `fit` (default), `inset`, `width` or `height`. The optional fourth argument is a boolean whether or not to force the use of standards ratios. You can find about these modes in the `FFMpeg\Filters\Video\ResizeFilter` class. 228 | 229 | ### Custom filters 230 | 231 | Sometimes you don't want to use the built-in filters. You can apply your own filter by providing a set of options. This can be an array or multiple strings as arguments: 232 | 233 | ```php 234 | FFMpeg::fromDisk('videos') 235 | ->open('steve_howe.mp4') 236 | ->addFilter(['-itsoffset', 1]); 237 | 238 | // or 239 | 240 | FFMpeg::fromDisk('videos') 241 | ->open('steve_howe.mp4') 242 | ->addFilter('-itsoffset', 1); 243 | ``` 244 | 245 | ### Watermark filter 246 | 247 | You can easily add a watermark using the `addWatermark` method. With the `WatermarkFactory`, you can open your watermark file from a specific disk, just like opening an audio or video file. When you discard the `fromDisk` method, it uses the default disk specified in the `filesystems.php` configuration file. 248 | 249 | After opening your watermark file, you can position it with the `top`, `right`, `bottom`, and `left` methods. The first parameter of these methods is the offset, which is optional and can be negative. 250 | 251 | ```php 252 | use ProtoneMedia\LaravelFFMpeg\Filters\WatermarkFactory; 253 | 254 | FFMpeg::fromDisk('videos') 255 | ->open('steve_howe.mp4') 256 | ->addWatermark(function(WatermarkFactory $watermark) { 257 | $watermark->fromDisk('local') 258 | ->open('logo.png') 259 | ->right(25) 260 | ->bottom(25); 261 | }); 262 | ``` 263 | 264 | Instead of using the position methods, you can also use the `horizontalAlignment` and `verticalAlignment` methods. 265 | 266 | For horizontal alignment, you can use the `WatermarkFactory::LEFT`, `WatermarkFactory::CENTER` and `WatermarkFactory::RIGHT` constants. For vertical alignment, you can use the `WatermarkFactory::TOP`, `WatermarkFactory::CENTER` and `WatermarkFactory::BOTTOM` constants. Both methods take an optional second parameter, which is the offset. 267 | 268 | ```php 269 | FFMpeg::open('steve_howe.mp4') 270 | ->addWatermark(function(WatermarkFactory $watermark) { 271 | $watermark->open('logo.png') 272 | ->horizontalAlignment(WatermarkFactory::LEFT, 25) 273 | ->verticalAlignment(WatermarkFactory::TOP, 25); 274 | }); 275 | ``` 276 | 277 | The `WatermarkFactory` also supports opening files from the web with the `openUrl` method. It supports custom HTTP headers as well. 278 | 279 | ```php 280 | FFMpeg::open('steve_howe.mp4') 281 | ->addWatermark(function(WatermarkFactory $watermark) { 282 | $watermark->openUrl('https://videocoursebuilder.com/logo.png'); 283 | 284 | // or 285 | 286 | $watermark->openUrl('https://videocoursebuilder.com/logo.png', [ 287 | 'Authorization' => 'Basic YWRtaW46MTIzNA==', 288 | ]); 289 | }); 290 | ``` 291 | 292 | If you want more control over the GET request, you can pass in an optional third parameter, which gives you the Curl resource. 293 | 294 | ```php 295 | $watermark->openUrl('https://videocoursebuilder.com/logo.png', [ 296 | 'Authorization' => 'Basic YWRtaW46MTIzNA==', 297 | ], function($curl) { 298 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); 299 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); 300 | }); 301 | ``` 302 | 303 | #### Watermark manipulation 304 | 305 | This package can manipulate watermarks by using [Spatie's Image package](https://github.com/spatie/image). To get started, install the package with Composer: 306 | 307 | ```bash 308 | composer require spatie/image 309 | ``` 310 | 311 | Now you can chain one more manipulation methods on the `WatermarkFactory` instance: 312 | 313 | ```php 314 | FFMpeg::open('steve_howe.mp4') 315 | ->addWatermark(function(WatermarkFactory $watermark) { 316 | $watermark->open('logo.png') 317 | ->right(25) 318 | ->bottom(25) 319 | ->width(100) 320 | ->height(100) 321 | ->greyscale(); 322 | }); 323 | ``` 324 | 325 | Check out [the documentation](https://spatie.be/docs/image/v1/introduction) for all available methods. 326 | 327 | ### Export without transcoding 328 | 329 | This package comes with a `ProtoneMedia\LaravelFFMpeg\FFMpeg\CopyFormat` class that allows you to export a file without transcoding the streams. You might want to use this to use another container: 330 | 331 | ```php 332 | use ProtoneMedia\LaravelFFMpeg\FFMpeg\CopyFormat; 333 | 334 | FFMpeg::open('video.mp4') 335 | ->export() 336 | ->inFormat(new CopyFormat) 337 | ->save('video.mkv'); 338 | ``` 339 | 340 | ### Chain multiple convertions 341 | 342 | ```php 343 | // The 'fromDisk()' method is not required, the file will now 344 | // be opened from the default 'disk', as specified in 345 | // the config file. 346 | 347 | FFMpeg::open('my_movie.mov') 348 | 349 | // export to FTP, converted in WMV 350 | ->export() 351 | ->toDisk('ftp') 352 | ->inFormat(new \FFMpeg\Format\Video\WMV) 353 | ->save('my_movie.wmv') 354 | 355 | // export to Amazon S3, converted in X264 356 | ->export() 357 | ->toDisk('s3') 358 | ->inFormat(new \FFMpeg\Format\Video\X264) 359 | ->save('my_movie.mkv'); 360 | 361 | // you could even discard the 'toDisk()' method, 362 | // now the converted file will be saved to 363 | // the same disk as the source! 364 | ->export() 365 | ->inFormat(new FFMpeg\Format\Video\WebM) 366 | ->save('my_movie.webm') 367 | 368 | // optionally you could set the visibility 369 | // of the exported file 370 | ->export() 371 | ->inFormat(new FFMpeg\Format\Video\WebM) 372 | ->withVisibility('public') 373 | ->save('my_movie.webm') 374 | ``` 375 | 376 | ### Export a frame from a video 377 | 378 | ```php 379 | FFMpeg::fromDisk('videos') 380 | ->open('steve_howe.mp4') 381 | ->getFrameFromSeconds(10) 382 | ->export() 383 | ->toDisk('thumnails') 384 | ->save('FrameAt10sec.png'); 385 | 386 | // Instead of the 'getFrameFromSeconds()' method, you could 387 | // also use the 'getFrameFromString()' or the 388 | // 'getFrameFromTimecode()' methods: 389 | 390 | $media = FFMpeg::open('steve_howe.mp4'); 391 | $frame = $media->getFrameFromString('00:00:13.37'); 392 | 393 | // or 394 | 395 | $timecode = new FFMpeg\Coordinate\TimeCode(...); 396 | $frame = $media->getFrameFromTimecode($timecode); 397 | ``` 398 | 399 | You can also get the raw contents of the frame instead of saving it to the filesystem: 400 | 401 | ```php 402 | $contents = FFMpeg::open('video.mp4') 403 | ->getFrameFromSeconds(2) 404 | ->export() 405 | ->getFrameContents(); 406 | ``` 407 | 408 | ### Export multiple frames at once 409 | 410 | There is a `TileFilter` that powers the [Tile-feature](#creates-tiles-of-frames). To make exporting multiple frames faster and simpler, we leveraged this feature to add some helper methods. For example, you may use the `exportFramesByInterval` method to export frames by a fixed interval. Alternatively, you may pass the number of frames you want to export to the `exportFramesByAmount` method, which will then calculate the interval based on the duration of the video. 411 | 412 | ```php 413 | FFMpeg::open('video.mp4') 414 | ->exportFramesByInterval(2) 415 | ->save('thumb_%05d.jpg'); 416 | ``` 417 | 418 | Both methods accept an optional second and third argument to specify to width and height of the frames. Instead of passing both the width and height, you may also pass just one of them. FFmpeg will respect the aspect ratio of the source. 419 | 420 | ```php 421 | FFMpeg::open('video.mp4') 422 | ->exportFramesByAmount(10, 320, 180) 423 | ->save('thumb_%05d.png'); 424 | ``` 425 | 426 | Both methods accept an optional fourth argument to specify the quality of the image when you're exporting to a lossy format like JPEG. The range for JPEG is `2-31`, with `2` being the best quality and `31` being the worst. 427 | 428 | ```php 429 | FFMpeg::open('video.mp4') 430 | ->exportFramesByInterval(2, 640, 360, 5) 431 | ->save('thumb_%05d.jpg'); 432 | ``` 433 | 434 | ### Creates tiles of frames 435 | 436 | You can create tiles from a video. You may call the `exportTile` method to specify how your tiles should be generated. In the example below, each generated image consists of a 3x5 grid (thus containing 15 frames) and each frame is 160x90 pixels. A frame will be taken every 5 seconds from the video. Instead of passing both the width and height, you may also pass just one of them. FFmpeg will respect the aspect ratio of the source. 437 | 438 | ```php 439 | use ProtoneMedia\LaravelFFMpeg\Filters\TileFactory; 440 | 441 | FFMpeg::open('steve_howe.mp4') 442 | ->exportTile(function (TileFactory $factory) { 443 | $factory->interval(5) 444 | ->scale(160, 90) 445 | ->grid(3, 5); 446 | }) 447 | ->save('tile_%05d.jpg'); 448 | ``` 449 | 450 | Instead of passing both the width and height, you may also pass just one of them like `scale(160)` or `scale(null, 90)`. The aspect ratio will be respected. The `TileFactory` has `margin`, `padding`, `width`, and `height` methods as well. There's also a `quality` method to specify the quality when exporting to a lossy format like JPEG. The range for JPEG is `2-31`, with `2` being the best quality and `31` being the worst. 451 | 452 | This package can also generate a WebVTT file to add *Preview Thumbnails* to your video player. This is supported out-of-the-box by [JW player](https://support.jwplayer.com/articles/how-to-add-preview-thumbnails) and there are community-driven plugins for Video.js available as well. You may call the `generateVTT` method on the `TileFactory` with the desired filename: 453 | 454 | ```php 455 | FFMpeg::open('steve_howe.mp4') 456 | ->exportTile(function (TileFactory $factory) { 457 | $factory->interval(10) 458 | ->scale(320, 180) 459 | ->grid(5, 5) 460 | ->generateVTT('thumbnails.vtt'); 461 | }) 462 | ->save('tile_%05d.jpg'); 463 | ``` 464 | 465 | ### Multiple exports using loops 466 | 467 | Chaining multiple conversions works because the `save` method of the `MediaExporter` returns a fresh instance of the `MediaOpener`. You can use this to loop through items, for example, to exports multiple frames from one video: 468 | 469 | ```php 470 | $mediaOpener = FFMpeg::open('video.mp4'); 471 | 472 | foreach ([5, 15, 25] as $key => $seconds) { 473 | $mediaOpener = $mediaOpener->getFrameFromSeconds($seconds) 474 | ->export() 475 | ->save("thumb_{$key}.png"); 476 | } 477 | ``` 478 | 479 | The `MediaOpener` comes with an `each` method as well. The example above could be refactored like this: 480 | 481 | ```php 482 | FFMpeg::open('video.mp4')->each([5, 15, 25], function ($ffmpeg, $seconds, $key) { 483 | $ffmpeg->getFrameFromSeconds($seconds)->export()->save("thumb_{$key}.png"); 484 | }); 485 | ``` 486 | 487 | ### Create a timelapse 488 | 489 | You can create a timelapse from a sequence of images by using the `asTimelapseWithFramerate` method on the exporter 490 | 491 | ```php 492 | FFMpeg::open('feature_%04d.png') 493 | ->export() 494 | ->asTimelapseWithFramerate(1) 495 | ->inFormat(new X264) 496 | ->save('timelapse.mp4'); 497 | ``` 498 | 499 | ### Multiple inputs 500 | 501 | You can open multiple inputs, even from different disks. This uses FFMpeg's `map` and `filter_complex` features. You can open multiple files by chaining the `open` method of by using an array. You can mix inputs from different disks. 502 | 503 | ```php 504 | FFMpeg::open('video1.mp4')->open('video2.mp4'); 505 | 506 | FFMpeg::open(['video1.mp4', 'video2.mp4']); 507 | 508 | FFMpeg::fromDisk('uploads') 509 | ->open('video1.mp4') 510 | ->fromDisk('archive') 511 | ->open('video2.mp4'); 512 | ``` 513 | 514 | When you open multiple inputs, you have to add mappings so FFMpeg knows how to route them. This package provides a `addFormatOutputMapping` method, which takes three parameters: the format, the output, and the output labels of the `-filter_complex` part. 515 | 516 | The output (2nd argument) should be an instanceof `ProtoneMedia\LaravelFFMpeg\Filesystem\Media`. You can instantiate with the `make` method, call it with the name of the disk and the path (see example). 517 | 518 | Check out this example, which maps separate video and audio inputs into one output. 519 | 520 | ```php 521 | FFMpeg::fromDisk('local') 522 | ->open(['video.mp4', 'audio.m4a']) 523 | ->export() 524 | ->addFormatOutputMapping(new X264, Media::make('local', 'new_video.mp4'), ['0:v', '1:a']) 525 | ->save(); 526 | ``` 527 | 528 | This is an example [from the underlying library](https://github.com/PHP-FFMpeg/PHP-FFMpeg#base-usage): 529 | 530 | ```php 531 | // This code takes 2 input videos, stacks they horizontally in 1 output video and 532 | // adds to this new video the audio from the first video. (It is impossible 533 | // with a simple filter graph that has only 1 input and only 1 output). 534 | 535 | FFMpeg::fromDisk('local') 536 | ->open(['video.mp4', 'video2.mp4']) 537 | ->export() 538 | ->addFilter('[0:v][1:v]', 'hstack', '[v]') // $in, $parameters, $out 539 | ->addFormatOutputMapping(new X264, Media::make('local', 'stacked_video.mp4'), ['0:a', '[v]']) 540 | ->save(); 541 | ``` 542 | 543 | Just like single inputs, you can also pass a callback to the `addFilter` method. This will give you an instance of `\FFMpeg\Filters\AdvancedMedia\ComplexFilters`: 544 | 545 | ```php 546 | use FFMpeg\Filters\AdvancedMedia\ComplexFilters; 547 | 548 | FFMpeg::open(['video.mp4', 'video2.mp4']) 549 | ->export() 550 | ->addFilter(function(ComplexFilters $filters) { 551 | // $filters->watermark(...); 552 | }); 553 | ``` 554 | 555 | Opening files from the web works similarly. You can pass an array of URLs to the `openUrl` method, optionally with custom HTTP headers. 556 | 557 | ```php 558 | FFMpeg::openUrl([ 559 | 'https://videocoursebuilder.com/lesson-3.mp4', 560 | 'https://videocoursebuilder.com/lesson-4.mp4', 561 | ]); 562 | 563 | FFMpeg::openUrl([ 564 | 'https://videocoursebuilder.com/lesson-3.mp4', 565 | 'https://videocoursebuilder.com/lesson-4.mp4', 566 | ], [ 567 | 'Authorization' => 'Basic YWRtaW46MTIzNA==', 568 | ]); 569 | ``` 570 | 571 | If you want to use another set of HTTP headers for each URL, you can chain the `openUrl` method: 572 | 573 | ```php 574 | FFMpeg::openUrl('https://videocoursebuilder.com/lesson-5.mp4', [ 575 | 'Authorization' => 'Basic YWRtaW46MTIzNA==', 576 | ])->openUrl('https://videocoursebuilder.com/lesson-6.mp4', [ 577 | 'Authorization' => 'Basic bmltZGE6NDMyMQ==', 578 | ]); 579 | ``` 580 | 581 | ### Concat files without transcoding 582 | 583 | ```php 584 | FFMpeg::fromDisk('local') 585 | ->open(['video.mp4', 'video2.mp4']) 586 | ->export() 587 | ->concatWithoutTranscoding() 588 | ->save('concat.mp4'); 589 | ``` 590 | 591 | ### Concat files with transcoding 592 | 593 | ```php 594 | FFMpeg::fromDisk('local') 595 | ->open(['video.mp4', 'video2.mp4']) 596 | ->export() 597 | ->inFormat(new X264) 598 | ->concatWithTranscoding($hasVideo = true, $hasAudio = true) 599 | ->save('concat.mp4'); 600 | ``` 601 | 602 | ### Determinate duration 603 | 604 | With the ```Media``` class you can determinate the duration of a file: 605 | 606 | ```php 607 | $media = FFMpeg::open('wwdc_2006.mp4'); 608 | 609 | $durationInSeconds = $media->getDurationInSeconds(); // returns an int 610 | $durationInMiliseconds = $media->getDurationInMiliseconds(); // returns a float 611 | ``` 612 | 613 | ### Handling remote disks 614 | 615 | When opening or saving files from or to a remote disk, temporary files will be created on your server. After you're done exporting or processing these files, you could clean them up by calling the ```cleanupTemporaryFiles()``` method: 616 | 617 | ```php 618 | FFMpeg::cleanupTemporaryFiles(); 619 | ``` 620 | 621 | By default, the root of the temporary directories is evaluated by PHP's `sys_get_temp_dir()` method, but you can modify it by setting the `temporary_files_root` configuration key to a custom path. 622 | 623 | ## HLS 624 | 625 | You can create a M3U8 playlist to do [HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming). 626 | 627 | ```php 628 | $lowBitrate = (new X264)->setKiloBitrate(250); 629 | $midBitrate = (new X264)->setKiloBitrate(500); 630 | $highBitrate = (new X264)->setKiloBitrate(1000); 631 | 632 | FFMpeg::fromDisk('videos') 633 | ->open('steve_howe.mp4') 634 | ->exportForHLS() 635 | ->setSegmentLength(10) // optional 636 | ->setKeyFrameInterval(48) // optional 637 | ->addFormat($lowBitrate) 638 | ->addFormat($midBitrate) 639 | ->addFormat($highBitrate) 640 | ->save('adaptive_steve.m3u8'); 641 | ``` 642 | 643 | The ```addFormat``` method of the HLS exporter takes an optional second parameter which can be a callback method. This allows you to add different filters per format. First, check out the *Multiple inputs* section to understand how complex filters are handled. 644 | 645 | You can use the `addFilter` method to add a complex filter (see `$lowBitrate` example). Since the `scale` filter is used a lot, there is a helper method (see `$midBitrate` example). You can also use a callable to get access to the `ComplexFilters` instance. The package provides the `$in` and `$out` arguments so you don't have to worry about it (see `$highBitrate` example). 646 | 647 | HLS export is built using FFMpeg's `map` and `filter_complex` features. This is a breaking change from earlier versions (1.x - 6.x) which performed a single export for each format. If you're upgrading, replace the `addFilter` calls with `addLegacyFilter` calls and verify the result (see `$superBitrate` example). Not all filters will work this way and some need to be upgraded manually. 648 | 649 | ```php 650 | $lowBitrate = (new X264)->setKiloBitrate(250); 651 | $midBitrate = (new X264)->setKiloBitrate(500); 652 | $highBitrate = (new X264)->setKiloBitrate(1000); 653 | $superBitrate = (new X264)->setKiloBitrate(1500); 654 | 655 | FFMpeg::open('steve_howe.mp4') 656 | ->exportForHLS() 657 | ->addFormat($lowBitrate, function($media) { 658 | $media->addFilter('scale=640:480'); 659 | }) 660 | ->addFormat($midBitrate, function($media) { 661 | $media->scale(960, 720); 662 | }) 663 | ->addFormat($highBitrate, function ($media) { 664 | $media->addFilter(function ($filters, $in, $out) { 665 | $filters->custom($in, 'scale=1920:1200', $out); // $in, $parameters, $out 666 | }); 667 | }) 668 | ->addFormat($superBitrate, function($media) { 669 | $media->addLegacyFilter(function ($filters) { 670 | $filters->resize(new \FFMpeg\Coordinate\Dimension(2560, 1920)); 671 | }); 672 | }) 673 | ->save('adaptive_steve.m3u8'); 674 | ``` 675 | 676 | ### Using custom segment patterns 677 | 678 | You can use a custom pattern to name the segments and playlists. The `useSegmentFilenameGenerator` gives you 5 arguments. The first, second and third argument provide information about the basename of the export, the format of the current iteration and the key of the current iteration. The fourth argument is a callback you should call with your *segments* pattern. The fifth argument is a callback you should call with your *playlist* pattern. Note that this is not the name of the primary playlist, but the name of the playlist of each format. 679 | 680 | ```php 681 | FFMpeg::fromDisk('videos') 682 | ->open('steve_howe.mp4') 683 | ->exportForHLS() 684 | ->useSegmentFilenameGenerator(function ($name, $format, $key, callable $segments, callable $playlist) { 685 | $segments("{$name}-{$format->getKiloBitrate()}-{$key}-%03d.ts"); 686 | $playlist("{$name}-{$format->getKiloBitrate()}-{$key}.m3u8"); 687 | }); 688 | ``` 689 | 690 | ### Encrypted HLS 691 | 692 | You can encrypt each HLS segment using AES-128 encryption. To do this, call the `withEncryptionKey` method on the HLS exporter with a key. We provide a `generateEncryptionKey` helper method on the `HLSExporter` class to generate a key. Make sure you store the key well, as the exported result is worthless without the key. By default, the filename of the key is `secret.key`, but you can change that with the optional second parameter of the `withEncryptionKey` method. 693 | 694 | ```php 695 | use ProtoneMedia\LaravelFFMpeg\Exporters\HLSExporter; 696 | 697 | $encryptionKey = HLSExporter::generateEncryptionKey(); 698 | 699 | FFMpeg::open('steve_howe.mp4') 700 | ->exportForHLS() 701 | ->withEncryptionKey($encryptionKey) 702 | ->addFormat($lowBitrate) 703 | ->addFormat($midBitrate) 704 | ->addFormat($highBitrate) 705 | ->save('adaptive_steve.m3u8'); 706 | ``` 707 | 708 | To secure your HLS export even further, you can rotate the key on each exported segment. By doing so, it will generate multiple keys that you'll need to store. Use the `withRotatingEncryptionKey` method to enable this feature and provide a callback that implements the storage of the keys. 709 | 710 | ```php 711 | FFMpeg::open('steve_howe.mp4') 712 | ->exportForHLS() 713 | ->withRotatingEncryptionKey(function ($filename, $contents) { 714 | $videoId = 1; 715 | 716 | // use this callback to store the encryption keys 717 | 718 | Storage::disk('secrets')->put($videoId . '/' . $filename, $contents); 719 | 720 | // or... 721 | 722 | DB::table('hls_secrets')->insert([ 723 | 'video_id' => $videoId, 724 | 'filename' => $filename, 725 | 'contents' => $contents, 726 | ]); 727 | }) 728 | ->addFormat($lowBitrate) 729 | ->addFormat($midBitrate) 730 | ->addFormat($highBitrate) 731 | ->save('adaptive_steve.m3u8'); 732 | ``` 733 | 734 | The `withRotatingEncryptionKey` method has an optional second argument to set the number of segments that use the same key. This defaults to `1`. 735 | 736 | ```php 737 | FFMpeg::open('steve_howe.mp4') 738 | ->exportForHLS() 739 | ->withRotatingEncryptionKey($callable, 10); 740 | ``` 741 | 742 | Some filesystems, especially on cheap and slow VPSs, are not fast enough to handle the rotating key. This may lead to encoding exceptions, like `No key URI specified in key info file`. One possible solution is to use a different storage for the keys, which you can specify using the `temporary_files_encrypted_hls` configuration key. On UNIX-based systems, you may use a `tmpfs` filesystem to increase read/write speeds: 743 | 744 | ```php 745 | // config/laravel-ffmpeg.php 746 | 747 | return [ 748 | 749 | 'temporary_files_encrypted_hls' => '/dev/shm' 750 | 751 | ]; 752 | ``` 753 | 754 | ### Protecting your HLS encryption keys 755 | 756 | To make working with encrypted HLS even better, we've added a `DynamicHLSPlaylist` class that modifies playlists on-the-fly and specifically for your application. This way, you can add your authentication and authorization logic. As we're using a plain Laravel controller, you can use features like [Gates](https://laravel.com/docs/master/authorization#gates) and [Middleware](https://laravel.com/docs/master/middleware#introduction). 757 | 758 | In this example, we've saved the HLS export to the `public` disk, and we've stored the encryption keys to the `secrets` disk, which isn't publicly available. As the browser can't access the encryption keys, it won't play the video. Each playlist has paths to the encryption keys, and we need to modify those paths to point to an accessible endpoint. 759 | 760 | This implementation consists of two routes. One that responses with an encryption key and one that responses with a modified playlist. The first route (`video.key`) is relatively simple, and this is where you should add your additional logic. 761 | 762 | The second route (`video.playlist`) uses the `DynamicHLSPlaylist` class. Call the `dynamicHLSPlaylist` method on the `FFMpeg` facade, and similar to opening media files, you can open a playlist utilizing the `fromDisk` and `open` methods. Then you must provide three callbacks. Each of them gives you a relative path and expects a full path in return. As the `DynamicHLSPlaylist` class implements the `Illuminate\Contracts\Support\Responsable` interface, you can return the instance. 763 | 764 | The first callback (KeyUrlResolver) gives you the relative path to an encryption key. The second callback (MediaUrlResolver) gives you the relative path to a media segment (.ts files). The third callback (PlaylistUrlResolver) gives you the relative path to a playlist. 765 | 766 | Now instead of using `Storage::disk('public')->url('adaptive_steve.m3u8')` to get the full url to your primary playlist, you can use `route('video.playlist', ['playlist' => 'adaptive_steve.m3u8'])`. The `DynamicHLSPlaylist` class takes care of all the paths and urls. 767 | 768 | ```php 769 | Route::get('/video/secret/{key}', function ($key) { 770 | return Storage::disk('secrets')->download($key); 771 | })->name('video.key'); 772 | 773 | Route::get('/video/{playlist}', function ($playlist) { 774 | return FFMpeg::dynamicHLSPlaylist() 775 | ->fromDisk('public') 776 | ->open($playlist) 777 | ->setKeyUrlResolver(function ($key) { 778 | return route('video.key', ['key' => $key]); 779 | }) 780 | ->setMediaUrlResolver(function ($mediaFilename) { 781 | return Storage::disk('public')->url($mediaFilename); 782 | }) 783 | ->setPlaylistUrlResolver(function ($playlistFilename) { 784 | return route('video.playlist', ['playlist' => $playlistFilename]); 785 | }); 786 | })->name('video.playlist'); 787 | ``` 788 | 789 | ### Live Coding Session 790 | 791 | Here you can find a Live Coding Session about HLS encryption: 792 | 793 | [https://www.youtube.com/watch?v=WlbzWoAcez4](https://www.youtube.com/watch?v=WlbzWoAcez4) 794 | 795 | ## Process Output 796 | 797 | You can get the raw process output by calling the `getProcessOutput` method. Though the use-case is limited, you can use it to analyze a file (for example, with the `volumedetect` filter). It returns a `ProtoneMedia\LaravelFFMpeg\Support\ProcessOutput` class that has three methods: `all`, `errors` and `output`. Each method returns an array with the corresponding lines. 798 | 799 | ```php 800 | $processOutput = FFMpeg::open('video.mp4') 801 | ->export() 802 | ->addFilter(['-filter:a', 'volumedetect', '-f', 'null']) 803 | ->getProcessOutput(); 804 | 805 | $processOutput->all(); 806 | $processOutput->errors(); 807 | $processOutput->out(); 808 | ``` 809 | 810 | ## Advanced 811 | 812 | The Media object you get when you 'open' a file, actually holds the Media object that belongs to the [underlying driver](https://github.com/PHP-FFMpeg/PHP-FFMpeg). It handles dynamic method calls as you can see [here](https://github.com/pascalbaljetmedia/laravel-ffmpeg/blob/master/src/Media.php#L114-L117). This way all methods of the underlying driver are still available to you. 813 | 814 | ```php 815 | // This gives you an instance of ProtoneMedia\LaravelFFMpeg\MediaOpener 816 | $media = FFMpeg::fromDisk('videos')->open('video.mp4'); 817 | 818 | // The 'getStreams' method will be called on the underlying Media object since 819 | // it doesn't exists on this object. 820 | $codec = $media->getVideoStream()->get('codec_name'); 821 | ``` 822 | 823 | If you want direct access to the underlying object, call the object as a function (invoke): 824 | 825 | ```php 826 | // This gives you an instance of ProtoneMedia\LaravelFFMpeg\MediaOpener 827 | $media = FFMpeg::fromDisk('videos')->open('video.mp4'); 828 | 829 | // This gives you an instance of FFMpeg\Media\MediaTypeInterface 830 | $baseMedia = $media(); 831 | ``` 832 | 833 | ## Experimental 834 | 835 | The [progress listener](#progress-monitoring) exposes the transcoded percentage, but the underlying package also has an internal `AbstractProgressListener` that exposes the current pass and the current time. Though the use-case is limited, you might want to get access to this listener instance. You can do this by decorating the format with the `ProgressListenerDecorator`. This feature is highly experimental, so be sure the test this thoroughly before using it in production. 836 | 837 | ```php 838 | use FFMpeg\Format\ProgressListener\AbstractProgressListener; 839 | use ProtoneMedia\LaravelFFMpeg\FFMpeg\ProgressListenerDecorator; 840 | 841 | $format = new \FFMpeg\Format\Video\X264; 842 | $decoratedFormat = ProgressListenerDecorator::decorate($format); 843 | 844 | FFMpeg::open('video.mp4') 845 | ->export() 846 | ->inFormat($decoratedFormat) 847 | ->onProgress(function () use ($decoratedFormat) { 848 | $listeners = $decoratedFormat->getListeners(); // array of listeners 849 | 850 | $listener = $listeners[0]; // instance of AbstractProgressListener 851 | 852 | $listener->getCurrentPass(); 853 | $listener->getTotalPass(); 854 | $listener->getCurrentTime(); 855 | }) 856 | ->save('new_video.mp4'); 857 | ``` 858 | 859 | Since we can't get rid of some of the underlying options, you can interact with the final FFmpeg command by adding a callback to the exporter. You can add one or more callbacks by using the `beforeSaving` method: 860 | 861 | ```php 862 | FFMpeg::open('video.mp4') 863 | ->export() 864 | ->inFormat(new X264) 865 | ->beforeSaving(function ($commands) { 866 | $commands[] = '-hello'; 867 | 868 | return $commands; 869 | }) 870 | ->save('concat.mp4'); 871 | ``` 872 | 873 | *Note: this does not work with concatenation and frame exports* 874 | 875 | ## Example app 876 | 877 | Here's a blog post that will help you get started with this package: 878 | 879 | https://protone.media/en/blog/how-to-use-ffmpeg-in-your-laravel-projects 880 | 881 | ## Using Video.js to play HLS in any browser 882 | 883 | Here's a 20-minute overview how to get started with Video.js. It covers including Video.js from a CDN, importing it as an ES6 module with Laravel Mix (Webpack) and building a reusable Vue.js component. 884 | 885 | [https://www.youtube.com/watch?v=nA1Jy8BPjys](https://www.youtube.com/watch?v=nA1Jy8BPjys) 886 | 887 | ## Wiki 888 | 889 | * [Custom filters](https://github.com/protonemedia/laravel-ffmpeg/wiki/Custom-filters) 890 | * [FFmpeg failed to execute command](https://github.com/protonemedia/laravel-ffmpeg/wiki/FFmpeg-failed-to-execute-command) 891 | * [Get the dimensions of a Video file](https://github.com/protonemedia/laravel-ffmpeg/wiki/Get-the-dimensions-of-a-Video-file) 892 | * [Monitoring the transcoding progress](https://github.com/protonemedia/laravel-ffmpeg/wiki/Monitoring-the-transcoding-progress) 893 | * [Unable to load FFProbe](https://github.com/protonemedia/laravel-ffmpeg/wiki/Unable-to-load-FFProbe) 894 | 895 | ## Changelog 896 | 897 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 898 | 899 | ## Testing 900 | 901 | ```bash 902 | $ composer test 903 | ``` 904 | 905 | ## Contributing 906 | 907 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 908 | 909 | ## Other Laravel packages 910 | 911 | * [`Inertia Table`](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-ffmpeg): The Ultimate Table for Inertia.js with built-in Query Builder. 912 | * [`Laravel Blade On Demand`](https://github.com/protonemedia/laravel-blade-on-demand): Laravel package to compile Blade templates in memory. 913 | * [`Laravel Cross Eloquent Search`](https://github.com/protonemedia/laravel-cross-eloquent-search): Laravel package to search through multiple Eloquent models. 914 | * [`Laravel Eloquent Scope as Select`](https://github.com/protonemedia/laravel-eloquent-scope-as-select): Stop duplicating your Eloquent query scopes and constraints in PHP. This package lets you re-use your query scopes and constraints by adding them as a subquery. 915 | * [`Laravel MinIO Testing Tools`](https://github.com/protonemedia/laravel-minio-testing-tools): Run your tests against a MinIO S3 server. 916 | * [`Laravel Mixins`](https://github.com/protonemedia/laravel-mixins): A collection of Laravel goodies. 917 | * [`Laravel Paddle`](https://github.com/protonemedia/laravel-paddle): Paddle.com API integration for Laravel with support for webhooks/events. 918 | * [`Laravel Task Runner`](https://github.com/protonemedia/laravel-task-runner): Write Shell scripts like Blade Components and run them locally or on a remote server. 919 | * [`Laravel Verify New Email`](https://github.com/protonemedia/laravel-verify-new-email): This package adds support for verifying new email addresses: when a user updates its email address, it won't replace the old one until the new one is verified. 920 | * [`Laravel XSS Protection`](https://github.com/protonemedia/laravel-xss-protection): Laravel Middleware to protect your app against Cross-site scripting (XSS). It sanitizes request input, and it can sanatize Blade echo statements. 921 | 922 | ## Security 923 | 924 | If you discover any security-related issues, please email code@protone.media instead of using the issue tracker. Please do not email any questions, open an issue if you have a question. 925 | 926 | ## Credits 927 | 928 | - [Pascal Baljet](https://github.com/pascalbaljet) 929 | - [All Contributors](../../contributors) 930 | 931 | ## License 932 | 933 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 934 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pbmedia/laravel-ffmpeg", 3 | "description": "FFMpeg for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-ffmpeg", 7 | "ffmpeg", 8 | "protonemedia", 9 | "protone media" 10 | ], 11 | "homepage": "https://github.com/protonemedia/laravel-ffmpeg", 12 | "license": "MIT", 13 | "type": "library", 14 | "authors": [ 15 | { 16 | "name": "Pascal Baljet", 17 | "email": "pascal@protone.media", 18 | "homepage": "https://protone.media", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1|^8.2|^8.3|^8.4", 24 | "illuminate/contracts": "^10.0|^11.0|^12.0", 25 | "php-ffmpeg/php-ffmpeg": "^1.2", 26 | "ramsey/collection": "^2.0" 27 | }, 28 | "require-dev": { 29 | "league/flysystem-memory": "^3.10", 30 | "mockery/mockery": "^1.4.4", 31 | "nesbot/carbon": "^2.66|^3.0", 32 | "orchestra/testbench": "^8.0|^9.0|^10.0", 33 | "phpunit/phpunit": "^10.4|^11.5.3", 34 | "spatie/image": "^2.2|^3.3", 35 | "spatie/phpunit-snapshot-assertions": "^5.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "ProtoneMedia\\LaravelFFMpeg\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "ProtoneMedia\\LaravelFFMpeg\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "vendor/bin/phpunit", 49 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 50 | }, 51 | "config": { 52 | "sort-packages": true 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "ProtoneMedia\\LaravelFFMpeg\\Support\\ServiceProvider" 60 | ], 61 | "aliases": { 62 | "FFMpeg": "ProtoneMedia\\LaravelFFMpeg\\Support\\FFMpeg" 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'binaries' => env('FFMPEG_BINARIES', 'ffmpeg'), 6 | 7 | 'threads' => 12, // set to false to disable the default 'threads' filter 8 | ], 9 | 10 | 'ffprobe' => [ 11 | 'binaries' => env('FFPROBE_BINARIES', 'ffprobe'), 12 | ], 13 | 14 | 'timeout' => 3600, 15 | 16 | 'log_channel' => env('LOG_CHANNEL', 'stack'), // set to false to completely disable logging 17 | 18 | 'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()), 19 | 20 | 'temporary_files_encrypted_hls' => env('FFMPEG_TEMPORARY_ENCRYPTED_HLS', env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir())), 21 | ]; 22 | -------------------------------------------------------------------------------- /src/Drivers/InteractsWithFilters.php: -------------------------------------------------------------------------------- 1 | media->getFiltersCollection()); 29 | } 30 | 31 | /** 32 | * Helper method to provide multiple ways to add a filter to the underlying 33 | * media object. 34 | */ 35 | public function addFilter(): self 36 | { 37 | $arguments = func_get_args(); 38 | 39 | // to support '[in]filter[out]' complex filters 40 | if ($this->isAdvancedMedia() && count($arguments) === 3) { 41 | $this->media->filters()->custom(...$arguments); 42 | 43 | return $this; 44 | } 45 | 46 | // use a callback to add a filter 47 | if ($arguments[0] instanceof Closure) { 48 | call_user_func_array($arguments[0], [$this->media->filters()]); 49 | 50 | return $this; 51 | } 52 | 53 | // use an object to add a filter 54 | if ($arguments[0] instanceof FilterInterface) { 55 | call_user_func_array([$this->media, 'addFilter'], $arguments); 56 | 57 | return $this; 58 | } 59 | 60 | // use a single array with parameters to define a filter 61 | if (is_array($arguments[0])) { 62 | $this->media->addFilter(new SimpleFilter($arguments[0])); 63 | 64 | return $this; 65 | } 66 | 67 | // use all function arguments as a filter 68 | $this->media->addFilter(new SimpleFilter($arguments)); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Calls the callable with a WatermarkFactory instance and 75 | * adds the freshly generated WatermarkFilter. 76 | */ 77 | public function addWatermark(callable $withWatermarkFactory): self 78 | { 79 | $withWatermarkFactory( 80 | $watermarkFactory = new WatermarkFactory 81 | ); 82 | 83 | return $this->addFilter($watermarkFactory->get()); 84 | } 85 | 86 | /** 87 | * Shortcut for adding a Resize filter. 88 | * 89 | * @param int $width 90 | * @param int $height 91 | * @param string $mode 92 | * @param bool $forceStandards 93 | */ 94 | public function resize($width, $height, $mode = ResizeFilter::RESIZEMODE_FIT, $forceStandards = true): self 95 | { 96 | $dimension = new Dimension($width, $height); 97 | 98 | $filter = new ResizeFilter($dimension, $mode, $forceStandards); 99 | 100 | return $this->addFilter($filter); 101 | } 102 | 103 | /** 104 | * Maps the arguments into a 'LegacyFilterMapping' instance and 105 | * pushed it to the 'pendingComplexFilters' collection. These 106 | * filters will be applied later on by the MediaExporter. 107 | */ 108 | public function addFilterAsComplexFilter($in, $out, ...$arguments): self 109 | { 110 | $this->pendingComplexFilters->push(new LegacyFilterMapping( 111 | $in, 112 | $out, 113 | ...$arguments 114 | )); 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Getter for the pending complex filters. 121 | */ 122 | public function getPendingComplexFilters(): Collection 123 | { 124 | return $this->pendingComplexFilters; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Drivers/InteractsWithMediaStreams.php: -------------------------------------------------------------------------------- 1 | isAdvancedMedia()) { 18 | return iterator_to_array($this->media->getStreams()); 19 | } 20 | 21 | return $this->mediaCollection->map(function ($media) { 22 | return $this->fresh()->open(MediaCollection::make([$media]))->getStreams(); 23 | })->collapse()->all(); 24 | } 25 | 26 | /** 27 | * Gets the duration of the media from the first stream or from the format. 28 | */ 29 | public function getDurationInMiliseconds(): int 30 | { 31 | $stream = Arr::first($this->getStreams()); 32 | 33 | if ($stream->has('duration')) { 34 | return intval(round($stream->get('duration') * 1000)); 35 | } 36 | 37 | $format = $this->getFormat(); 38 | 39 | if ($format->has('duration')) { 40 | $duration = $format->get('duration'); 41 | 42 | if (! blank($duration)) { 43 | return $format->get('duration') * 1000; 44 | } 45 | } 46 | 47 | $duration = $this->extractDurationFromStream($this->getVideoStream() ?? $this->getAudioStream()); 48 | 49 | if ($duration !== null) { 50 | return $duration; 51 | } 52 | 53 | throw new UnknownDurationException('Could not determine the duration of the media.'); 54 | } 55 | 56 | public function getDurationInSeconds(): int 57 | { 58 | return round($this->getDurationInMiliseconds() / 1000); 59 | } 60 | 61 | /** 62 | * Gets the first audio streams of the media. 63 | */ 64 | public function getAudioStream(): ?Stream 65 | { 66 | return Arr::first($this->getStreams(), function (Stream $stream) { 67 | return $stream->isAudio(); 68 | }); 69 | } 70 | 71 | /** 72 | * Gets the first video streams of the media. 73 | */ 74 | public function getVideoStream(): ?Stream 75 | { 76 | return Arr::first($this->getStreams(), function (Stream $stream) { 77 | return $stream->isVideo(); 78 | }); 79 | } 80 | 81 | /** 82 | * Extract video duration when it's not a standard property. 83 | */ 84 | public function extractDurationFromStream(Stream $stream): ?int 85 | { 86 | $duration = $this->findDuration($stream->all()); 87 | 88 | if ($duration === null) { 89 | return null; 90 | } 91 | 92 | return $this->formatDuration($duration) * 1000; 93 | } 94 | 95 | /** 96 | * Recursively search for the duration key. 97 | */ 98 | public function findDuration(array $array): ?string 99 | { 100 | foreach ($array as $key => $value) { 101 | if (is_array($value)) { 102 | if (! is_null($duration = $this->findDuration($value))) { 103 | return $duration; 104 | } 105 | } 106 | 107 | if (strtolower($key) === 'duration') { 108 | return (string) $value; 109 | } 110 | } 111 | 112 | return null; 113 | } 114 | 115 | /** 116 | * Convert duration string to seconds. 117 | */ 118 | public function formatDuration(string $duration): float 119 | { 120 | $parts = array_map('floatval', explode(':', $duration)); 121 | $count = count($parts); 122 | 123 | return match ($count) { 124 | 2 => $parts[0] * 60 + $parts[1], 125 | 3 => $parts[0] * 3600 + $parts[1] * 60 + $parts[2], 126 | default => 0.0, 127 | }; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Drivers/PHPFFMpeg.php: -------------------------------------------------------------------------------- 1 | ffmpeg = $ffmpeg; 65 | $this->pendingComplexFilters = new Collection; 66 | } 67 | 68 | /** 69 | * Returns a fresh instance of itself with only the underlying FFMpeg instance. 70 | */ 71 | public function fresh(): self 72 | { 73 | return new static($this->ffmpeg); 74 | } 75 | 76 | public function get(): AbstractMediaType 77 | { 78 | return $this->media; 79 | } 80 | 81 | private function isAdvancedMedia(): bool 82 | { 83 | return $this->get() instanceof BaseAdvancedMedia; 84 | } 85 | 86 | public function isFrame(): bool 87 | { 88 | return $this->get() instanceof Frame; 89 | } 90 | 91 | public function isConcat(): bool 92 | { 93 | return $this->get() instanceof Concat; 94 | } 95 | 96 | public function isVideo(): bool 97 | { 98 | return $this->get() instanceof Video; 99 | } 100 | 101 | public function getMediaCollection(): MediaCollection 102 | { 103 | return $this->mediaCollection; 104 | } 105 | 106 | /** 107 | * Opens the MediaCollection if it's not been instanciated yet. 108 | */ 109 | public function open(MediaCollection $mediaCollection): self 110 | { 111 | if ($this->media) { 112 | return $this; 113 | } 114 | 115 | $this->mediaCollection = $mediaCollection; 116 | 117 | if ($mediaCollection->count() === 1 && ! $this->forceAdvanced) { 118 | $media = Arr::first($mediaCollection->collection()); 119 | 120 | $this->ffmpeg->setFFProbe( 121 | FFProbe::make($this->ffmpeg->getFFProbe())->setMedia($media) 122 | ); 123 | 124 | $ffmpegMedia = $this->ffmpeg->open($media->getLocalPath()); 125 | 126 | // this should be refactored to a factory... 127 | if ($ffmpegMedia instanceof Video) { 128 | $this->media = VideoMedia::make($ffmpegMedia); 129 | } elseif ($ffmpegMedia instanceof Audio) { 130 | $this->media = AudioMedia::make($ffmpegMedia); 131 | } else { 132 | $this->media = $ffmpegMedia; 133 | } 134 | 135 | if (method_exists($this->media, 'setHeaders')) { 136 | $this->media->setHeaders(Arr::first($mediaCollection->getHeaders()) ?: []); 137 | } 138 | } else { 139 | $ffmpegMedia = $this->ffmpeg->openAdvanced($mediaCollection->getLocalPaths()); 140 | 141 | $this->media = AdvancedMedia::make($ffmpegMedia) 142 | ->setHeaders($mediaCollection->getHeaders()); 143 | } 144 | 145 | return $this; 146 | } 147 | 148 | public function frame(TimeCode $timecode) 149 | { 150 | if (! $this->isVideo()) { 151 | throw new Exception('Opened media is not a video file.'); 152 | } 153 | 154 | $this->media = $this->media->frame($timecode); 155 | 156 | return $this; 157 | } 158 | 159 | public function concatWithoutTranscoding() 160 | { 161 | $localPaths = $this->mediaCollection->getLocalPaths(); 162 | 163 | $this->media = $this->ffmpeg->open(Arr::first($localPaths)) 164 | ->concat($localPaths); 165 | 166 | return $this; 167 | } 168 | 169 | /** 170 | * Force 'openAdvanced' when opening the MediaCollection 171 | */ 172 | public function openAdvanced(MediaCollection $mediaCollection): self 173 | { 174 | $this->forceAdvanced = true; 175 | 176 | return $this->open($mediaCollection); 177 | } 178 | 179 | /** 180 | * Returns the FFMpegDriver of the underlying library. 181 | */ 182 | private function getFFMpegDriver(): FFMpegDriver 183 | { 184 | return $this->get()->getFFMpegDriver(); 185 | } 186 | 187 | /** 188 | * Add a Listener to the underlying library. 189 | */ 190 | public function addListener(ListenerInterface $listener): self 191 | { 192 | $this->getFFMpegDriver()->listen($listener); 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Remove the Listener from the underlying library. 199 | */ 200 | public function removeListener(ListenerInterface $listener): self 201 | { 202 | $this->getFFMpegDriver()->unlisten($listener); 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * Adds a callable to the callbacks array. 209 | */ 210 | public function beforeSaving(callable $callback): self 211 | { 212 | $this->beforeSavingCallbacks[] = $callback; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Set the callbacks on the Media. 219 | */ 220 | public function applyBeforeSavingCallbacks(): self 221 | { 222 | $media = $this->get(); 223 | 224 | if (method_exists($media, 'setBeforeSavingCallbacks')) { 225 | $media->setBeforeSavingCallbacks($this->beforeSavingCallbacks); 226 | } 227 | 228 | return $this; 229 | } 230 | 231 | /** 232 | * Add an event handler to the underlying library. 233 | */ 234 | public function onEvent(string $event, callable $callback): self 235 | { 236 | $this->getFFMpegDriver()->on($event, $callback); 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Returns the underlying media object itself. 243 | */ 244 | public function __invoke(): AbstractMediaType 245 | { 246 | return $this->get(); 247 | } 248 | 249 | /** 250 | * Forwards the call to the underling media object and returns the result 251 | * if it's something different than the media object itself. 252 | */ 253 | public function __call($method, $arguments) 254 | { 255 | $result = $this->forwardCallTo($media = $this->get(), $method, $arguments); 256 | 257 | return ($result === $media) ? $this : $result; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Drivers/UnknownDurationException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 14 | $runtimeException->getCode(), 15 | $runtimeException->getPrevious() 16 | ), function (self $exception) { 17 | if (config('laravel-ffmpeg.set_command_and_error_output_on_exception', true)) { 18 | $exception->message = $exception->getAlchemyException()?->getMessage() ?: ''; 19 | } 20 | }); 21 | } 22 | 23 | public function getCommand(): ?string 24 | { 25 | return $this->getAlchemyException()?->getCommand(); 26 | } 27 | 28 | public function getErrorOutput(): ?string 29 | { 30 | return $this->getAlchemyException()?->getErrorOutput(); 31 | } 32 | 33 | public function getAlchemyException(): ?ExecutionFailureException 34 | { 35 | return $this->getPrevious(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exporters/EncryptsHLSSegments.php: -------------------------------------------------------------------------------- 1 | encryptionKey = $key; 107 | $this->encryptionIV = bin2hex(static::generateEncryptionKey()); 108 | 109 | $this->encryptionKeyFilename = $filename; 110 | $this->encryptionSecretsRoot = (new TemporaryDirectories( 111 | config('laravel-ffmpeg.temporary_files_encrypted_hls', sys_get_temp_dir()) 112 | ))->create(); 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Enables encryption with rotating keys. The callable will receive every new 119 | * key and the integer sets the number of segments that can 120 | * use the same key. 121 | */ 122 | public function withRotatingEncryptionKey(Closure $callback, int $segmentsPerKey = 1): self 123 | { 124 | $this->rotateEncryptiongKey = true; 125 | $this->onNewEncryptionKey = $callback; 126 | $this->segmentsPerKey = $segmentsPerKey; 127 | 128 | return $this->withEncryptionKey(null, null); 129 | } 130 | 131 | /** 132 | * Rotates the key and returns the absolute path to the info file. This method 133 | * should be executed as fast as possible, or we might be too late for FFmpeg 134 | * opening the next segment. That's why we don't use the Disk-class magic. 135 | */ 136 | private function rotateEncryptionKey(): string 137 | { 138 | if ($this->nextEncryptionFilenameAndKey) { 139 | [$keyFilename, $encryptionKey] = $this->nextEncryptionFilenameAndKey; 140 | } else { 141 | $keyFilename = $this->encryptionKeyFilename ?: static::generateEncryptionKeyFilename(); 142 | $encryptionKey = $this->encryptionKey ?: static::generateEncryptionKey(); 143 | } 144 | 145 | // get the absolute path to the info file and encryption key 146 | $hlsKeyInfoPath = $this->encryptionSecretsRoot.'/'.HLSExporter::HLS_KEY_INFO_FILENAME; 147 | $keyPath = $this->encryptionSecretsRoot.'/'.$keyFilename; 148 | 149 | $normalizedKeyPath = Disk::normalizePath($keyPath); 150 | 151 | // store the encryption key 152 | file_put_contents($keyPath, $encryptionKey); 153 | 154 | // store an info file with a reference to the encryption key and IV 155 | file_put_contents( 156 | $hlsKeyInfoPath, 157 | $normalizedKeyPath.PHP_EOL.$normalizedKeyPath.PHP_EOL.$this->encryptionIV 158 | ); 159 | 160 | // prepare for the next round 161 | if ($this->rotateEncryptiongKey) { 162 | $this->nextEncryptionFilenameAndKey = [ 163 | static::generateEncryptionKeyFilename(), 164 | static::generateEncryptionKey(), 165 | ]; 166 | } 167 | 168 | // call the callback 169 | if ($this->onNewEncryptionKey) { 170 | call_user_func($this->onNewEncryptionKey, $keyFilename, $encryptionKey, $this->listener); 171 | } 172 | 173 | // return the absolute path to the info file 174 | return Disk::normalizePath($hlsKeyInfoPath); 175 | } 176 | 177 | /** 178 | * Returns an array with the encryption parameters. 179 | */ 180 | private function getEncrypedHLSParameters(): array 181 | { 182 | if (! $this->encryptionIV) { 183 | return []; 184 | } 185 | 186 | $keyInfoPath = $this->rotateEncryptionKey(); 187 | $parameters = ['-hls_key_info_file', $keyInfoPath]; 188 | 189 | if ($this->rotateEncryptiongKey) { 190 | $parameters[] = '-hls_flags'; 191 | $parameters[] = 'periodic_rekey'; 192 | } 193 | 194 | return $parameters; 195 | } 196 | 197 | /** 198 | * Adds a listener and handler to rotate the key on 199 | * every new HLS segment. 200 | * 201 | * @return void 202 | */ 203 | private function addHandlerToRotateEncryptionKey() 204 | { 205 | if (! $this->rotateEncryptiongKey) { 206 | return; 207 | } 208 | 209 | $this->listener = new StdListener(HLSExporter::ENCRYPTION_LISTENER); 210 | 211 | $this->addListener($this->listener) 212 | ->onEvent(HLSExporter::ENCRYPTION_LISTENER, function ($line) { 213 | if (! strpos($line, ".keyinfo' for reading")) { 214 | return; 215 | } 216 | 217 | $this->segmentsOpened++; 218 | 219 | if ($this->segmentsOpened % $this->segmentsPerKey === 0) { 220 | $this->rotateEncryptionKey(); 221 | } 222 | }); 223 | } 224 | 225 | /** 226 | * Remove the listener at the end of the export to 227 | * prevent duplicate event handlers. 228 | */ 229 | private function removeHandlerThatRotatesEncryptionKey(): self 230 | { 231 | if ($this->listener) { 232 | $this->listener->removeAllListeners(); 233 | $this->removeListener($this->listener); 234 | $this->listener = null; 235 | 236 | $this->getFFMpegDriver()->removeAllListeners(HLSExporter::ENCRYPTION_LISTENER); 237 | } 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * While encoding, the encryption keys are saved to a temporary directory. 244 | * With this method, we loop through all segment playlists and replace 245 | * the absolute path to the keys to a relative ones. 246 | */ 247 | private function replaceAbsolutePathsHLSEncryption(Collection $playlistMedia): self 248 | { 249 | if (! $this->encryptionSecretsRoot) { 250 | return $this; 251 | } 252 | 253 | $playlistMedia->each(function ($playlistMedia) { 254 | $disk = $playlistMedia->getDisk(); 255 | $path = $playlistMedia->getPath(); 256 | 257 | $prefix = '#EXT-X-KEY:METHOD=AES-128,URI="'; 258 | 259 | $content = str_replace( 260 | $prefix.Disk::normalizePath($this->encryptionSecretsRoot).'/', 261 | $prefix, 262 | $disk->get($path) 263 | ); 264 | 265 | $disk->put($path, $content); 266 | }); 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * Removes the encryption keys from the temporary disk. 273 | */ 274 | private function cleanupHLSEncryption(): self 275 | { 276 | if ($this->encryptionSecretsRoot) { 277 | (new Filesystem)->deleteDirectory($this->encryptionSecretsRoot); 278 | } 279 | 280 | return $this; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Exporters/HLSExporter.php: -------------------------------------------------------------------------------- 1 | segmentLength = max(2, $length); 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Setter for the Key Frame interval 60 | */ 61 | public function setKeyFrameInterval(int $interval): self 62 | { 63 | $this->keyFrameInterval = max(2, $interval); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Method to set a different playlist generator than 70 | * the default HLSPlaylistGenerator. 71 | */ 72 | public function withPlaylistGenerator(PlaylistGenerator $playlistGenerator): self 73 | { 74 | $this->playlistGenerator = $playlistGenerator; 75 | 76 | return $this; 77 | } 78 | 79 | private function getPlaylistGenerator(): PlaylistGenerator 80 | { 81 | return $this->playlistGenerator ??= new HLSPlaylistGenerator; 82 | } 83 | 84 | /** 85 | * Method to not add the #EXT-X-ENDLIST line to the playlist. 86 | */ 87 | public function withoutPlaylistEndLine(): self 88 | { 89 | $playlistGenerator = $this->getPlaylistGenerator(); 90 | 91 | if ($playlistGenerator instanceof HLSPlaylistGenerator) { 92 | $playlistGenerator->withoutEndLine(); 93 | } 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Setter for a callback that generates a segment filename. 100 | */ 101 | public function useSegmentFilenameGenerator(Closure $callback): self 102 | { 103 | $this->segmentFilenameGenerator = $callback; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Returns a default generator if none is set. 110 | */ 111 | private function getSegmentFilenameGenerator(): callable 112 | { 113 | return $this->segmentFilenameGenerator ?: function ($name, $format, $key, $segments, $playlist) { 114 | $bitrate = $this->driver->getVideoStream() 115 | ? $format->getKiloBitrate() 116 | : $format->getAudioKiloBitrate(); 117 | 118 | $segments("{$name}_{$key}_{$bitrate}_%05d.ts"); 119 | $playlist("{$name}_{$key}_{$bitrate}.m3u8"); 120 | }; 121 | } 122 | 123 | /** 124 | * Calls the generator with the path (without extension), format and key. 125 | */ 126 | private function getSegmentPatternAndFormatPlaylistPath(string $baseName, AudioInterface $format, int $key): array 127 | { 128 | $segmentsPattern = null; 129 | $formatPlaylistPath = null; 130 | 131 | call_user_func( 132 | $this->getSegmentFilenameGenerator(), 133 | $baseName, 134 | $format, 135 | $key, 136 | function ($path) use (&$segmentsPattern) { 137 | $segmentsPattern = $path; 138 | }, 139 | function ($path) use (&$formatPlaylistPath) { 140 | $formatPlaylistPath = $path; 141 | } 142 | ); 143 | 144 | return [$segmentsPattern, $formatPlaylistPath]; 145 | } 146 | 147 | /** 148 | * Merges the HLS parameters to the given format. 149 | * 150 | * @param \FFMpeg\Format\Video\DefaultAudio $format 151 | */ 152 | private function addHLSParametersToFormat(DefaultAudio $format, string $segmentsPattern, Disk $disk, int $key): array 153 | { 154 | $format->setAdditionalParameters(array_merge( 155 | $format->getAdditionalParameters() ?: [], 156 | $hlsParameters = [ 157 | '-sc_threshold', 158 | '0', 159 | '-g', 160 | $this->keyFrameInterval, 161 | '-hls_playlist_type', 162 | 'vod', 163 | '-hls_time', 164 | $this->segmentLength, 165 | '-hls_segment_filename', 166 | $disk->makeMedia($segmentsPattern)->getLocalPath(), 167 | '-master_pl_name', 168 | $this->generateTemporarySegmentPlaylistFilename($key), 169 | ], 170 | $this->getEncrypedHLSParameters() 171 | )); 172 | 173 | return $hlsParameters; 174 | } 175 | 176 | /** 177 | * Gives the callback an HLSVideoFilters object that provides addFilter(), 178 | * addLegacyFilter(), addWatermark() and resize() helper methods. It 179 | * returns a mapping for the video and (optional) audio stream. 180 | */ 181 | private function applyFiltersCallback(callable $filtersCallback, int $formatKey): array 182 | { 183 | $filtersCallback( 184 | $hlsVideoFilters = new HLSVideoFilters($this->driver, $formatKey) 185 | ); 186 | 187 | $filterCount = $hlsVideoFilters->count(); 188 | 189 | $outs = [$filterCount ? HLSVideoFilters::glue($formatKey, $filterCount) : '0:v']; 190 | 191 | if ($this->getAudioStream()) { 192 | $outs[] = '0:a'; 193 | } 194 | 195 | return $outs; 196 | } 197 | 198 | /** 199 | * Returns the filename of a segment playlist by its key. We let FFmpeg generate a playlist 200 | * for each added format so we don't have to detect the bitrate and codec ourselves. 201 | * We use this as a reference so when can generate our own main playlist. 202 | */ 203 | public static function generateTemporarySegmentPlaylistFilename(int $key): string 204 | { 205 | return "temporary_segment_playlist_{$key}.m3u8"; 206 | } 207 | 208 | /** 209 | * Loops through each added format and then deletes the temporary 210 | * segment playlist, which we generate manually using the 211 | * HLSPlaylistGenerator. 212 | */ 213 | private function cleanupSegmentPlaylistGuides(Media $media): self 214 | { 215 | $disk = $media->getDisk(); 216 | $directory = $media->getDirectory(); 217 | 218 | $this->pendingFormats->map(function ($formatAndCallback, $key) use ($disk, $directory) { 219 | $disk->delete($directory.static::generateTemporarySegmentPlaylistFilename($key)); 220 | }); 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * Adds a mapping for each added format and automatically handles the mapping 227 | * for filters. Adds a handler to rotate the encryption key (optional). 228 | * Returns a media collection of all segment playlists. 229 | * 230 | * @throws \ProtoneMedia\LaravelFFMpeg\Exporters\NoFormatException 231 | */ 232 | private function prepareSaving(?string $path = null): Collection 233 | { 234 | if (! $this->pendingFormats) { 235 | throw new NoFormatException; 236 | } 237 | 238 | $media = $this->getDisk()->makeMedia($path); 239 | 240 | $baseName = $media->getDirectory().$media->getFilenameWithoutExtension(); 241 | 242 | return $this->pendingFormats->map(function (array $formatAndCallback, $key) use ($baseName) { 243 | [$format, $filtersCallback] = $formatAndCallback; 244 | 245 | [$segmentsPattern, $formatPlaylistPath] = $this->getSegmentPatternAndFormatPlaylistPath( 246 | $baseName, 247 | $format, 248 | $key 249 | ); 250 | 251 | $disk = $this->getDisk()->clone(); 252 | 253 | $this->addHLSParametersToFormat($format, $segmentsPattern, $disk, $key); 254 | 255 | if ($filtersCallback) { 256 | $outs = $this->applyFiltersCallback($filtersCallback, $key); 257 | } 258 | $formatPlaylistOutput = $disk->makeMedia($formatPlaylistPath); 259 | $this->addFormatOutputMapping($format, $formatPlaylistOutput, $outs ?? ['0']); 260 | 261 | return $formatPlaylistOutput; 262 | })->tap(function () { 263 | $this->addHandlerToRotateEncryptionKey(); 264 | }); 265 | } 266 | 267 | /** 268 | * Prepares the saves command but returns the command instead. 269 | * 270 | * @return mixed 271 | */ 272 | public function getCommand(?string $path = null) 273 | { 274 | $this->prepareSaving($path); 275 | 276 | return parent::getCommand(null); 277 | } 278 | 279 | /** 280 | * Runs the export, generates the main playlist, and cleans up the 281 | * segment playlist guides and temporary HLS encryption keys. 282 | * 283 | * @param string $path 284 | */ 285 | public function save(?string $mainPlaylistPath = null): MediaOpener 286 | { 287 | return $this->prepareSaving($mainPlaylistPath)->pipe(function ($segmentPlaylists) use ($mainPlaylistPath) { 288 | $result = parent::save(); 289 | 290 | $playlist = $this->getPlaylistGenerator()->get( 291 | $segmentPlaylists->all(), 292 | $this->driver->fresh() 293 | ); 294 | 295 | $this->getDisk()->put($mainPlaylistPath, $playlist); 296 | 297 | $this->replaceAbsolutePathsHLSEncryption($segmentPlaylists) 298 | ->cleanupSegmentPlaylistGuides($segmentPlaylists->first()) 299 | ->cleanupHLSEncryption() 300 | ->removeHandlerThatRotatesEncryptionKey(); 301 | 302 | return $result; 303 | }); 304 | } 305 | 306 | /** 307 | * Initializes the $pendingFormats property when needed and adds the format 308 | * with the optional callback to add filters. 309 | */ 310 | public function addFormat(FormatInterface $format, ?callable $filtersCallback = null): self 311 | { 312 | if (! $this->pendingFormats) { 313 | $this->pendingFormats = new Collection; 314 | } 315 | 316 | if (! $format instanceof DefaultVideo && $format instanceof DefaultAudio) { 317 | $originalFormat = clone $format; 318 | 319 | $format = new class extends DefaultVideo 320 | { 321 | private array $audioCodecs = []; 322 | 323 | public function setAvailableAudioCodecs(array $audioCodecs) 324 | { 325 | $this->audioCodecs = $audioCodecs; 326 | } 327 | 328 | public function getAvailableAudioCodecs(): array 329 | { 330 | return $this->audioCodecs; 331 | } 332 | 333 | public function supportBFrames() 334 | { 335 | return false; 336 | } 337 | 338 | public function getAvailableVideoCodecs() 339 | { 340 | return []; 341 | } 342 | }; 343 | 344 | $format->setAvailableAudioCodecs($originalFormat->getAvailableAudioCodecs()); 345 | $format->setAudioCodec($originalFormat->getAudioCodec()); 346 | $format->setAudioKiloBitrate($originalFormat->getAudioKiloBitrate()); 347 | 348 | if ($originalFormat->getAudioChannels()) { 349 | $format->setAudioChannels($originalFormat->getAudioChannels()); 350 | } 351 | } 352 | 353 | $this->pendingFormats->push([$format, $filtersCallback]); 354 | 355 | return $this; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/Exporters/HLSPlaylistGenerator.php: -------------------------------------------------------------------------------- 1 | withEndLine = false; 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Return the line from the master playlist that references the given segment playlist. 34 | * 35 | * @param \ProtoneMedia\LaravelFFMpeg\Filesystem\Media $playlistMedia 36 | */ 37 | private function getStreamInfoLine(Media $segmentPlaylistMedia, string $key): string 38 | { 39 | $segmentPlaylist = $segmentPlaylistMedia->getDisk()->get( 40 | $segmentPlaylistMedia->getDirectory().HLSExporter::generateTemporarySegmentPlaylistFilename($key) 41 | ); 42 | 43 | $lines = DynamicHLSPlaylist::parseLines($segmentPlaylist)->filter(); 44 | 45 | return $lines->get($lines->search($segmentPlaylistMedia->getFilename()) - 1); 46 | } 47 | 48 | /** 49 | * Loops through all segment playlists and generates a main playlist. It finds 50 | * the relative paths to the segment playlists and adds the framerate when 51 | * to each playlist. 52 | */ 53 | public function get(array $segmentPlaylists, PHPFFMpeg $driver): string 54 | { 55 | return Collection::make($segmentPlaylists)->map(function (Media $segmentPlaylist, $key) use ($driver) { 56 | $streamInfoLine = $this->getStreamInfoLine($segmentPlaylist, $key); 57 | 58 | $media = (new MediaOpener($segmentPlaylist->getDisk(), $driver)) 59 | ->openWithInputOptions($segmentPlaylist->getPath(), ['-allowed_extensions', 'ALL']); 60 | 61 | if ($media->getVideoStream()) { 62 | if ($frameRate = StreamParser::new($media->getVideoStream())->getFrameRate()) { 63 | $streamInfoLine .= ",FRAME-RATE={$frameRate}"; 64 | } 65 | } 66 | 67 | return [$streamInfoLine, $segmentPlaylist->getFilename()]; 68 | })->collapse() 69 | ->prepend(static::PLAYLIST_START) 70 | ->when($this->withEndLine, fn (Collection $lines) => $lines->push(static::PLAYLIST_END)) 71 | ->implode(PHP_EOL); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Exporters/HLSVideoFilters.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 38 | $this->formatKey = $formatKey; 39 | } 40 | 41 | public function count(): int 42 | { 43 | return $this->filterCount; 44 | } 45 | 46 | private function increaseFilterCount(): self 47 | { 48 | $this->filterCount++; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Generates an input mapping for a new filter. 55 | */ 56 | private function input(): string 57 | { 58 | return $this->filterCount ? static::glue($this->formatKey, $this->filterCount) : '[0]'; 59 | } 60 | 61 | /** 62 | * Generates an output mapping for a new filter. 63 | */ 64 | private function output(): string 65 | { 66 | return static::glue($this->formatKey, $this->filterCount + 1); 67 | } 68 | 69 | /** 70 | * Adds a filter as a complex filter. 71 | */ 72 | public function addLegacyFilter(...$arguments): self 73 | { 74 | $this->driver->addFilterAsComplexFilter($this->input(), $this->output(), ...$arguments); 75 | 76 | return $this->increaseFilterCount(); 77 | } 78 | 79 | /** 80 | * Shortcut for the ResizeFilter. 81 | * 82 | * @param int $width 83 | * @param int $height 84 | * @param string $mode 85 | * @param bool $forceStandards 86 | */ 87 | public function resize($width, $height, $mode = ResizeFilter::RESIZEMODE_FIT, $forceStandards = true): self 88 | { 89 | $dimension = new Dimension($width, $height); 90 | 91 | $filter = new ResizeFilter($dimension, $mode, $forceStandards); 92 | 93 | return $this->addLegacyFilter($filter); 94 | } 95 | 96 | /** 97 | * Shortcut for the WatermarkFactory. 98 | */ 99 | public function addWatermark(callable $withWatermarkFactory): self 100 | { 101 | $withWatermarkFactory($watermarkFactory = new WatermarkFactory); 102 | 103 | return $this->addLegacyFilter($watermarkFactory->get()); 104 | } 105 | 106 | /** 107 | * Adds a scale filter to the video, will be replaced in favor of resize(). 108 | * 109 | * @param int $width 110 | * @param int $height 111 | * 112 | * @deprecated 7.4.0 113 | */ 114 | public function scale($width, $height): self 115 | { 116 | return $this->addFilter("scale={$width}:{$height}"); 117 | } 118 | 119 | /** 120 | * Adds a filter object or a callable to the driver and automatically 121 | * chooses the right input and output mapping. 122 | */ 123 | public function addFilter(...$arguments): self 124 | { 125 | if (count($arguments) === 1 && ! is_callable($arguments[0])) { 126 | $this->driver->addFilter($this->input(), $arguments[0], $this->output()); 127 | } else { 128 | $this->driver->addFilter(function (ComplexFilters $filters) use ($arguments) { 129 | $arguments[0]($filters, $this->input(), $this->output()); 130 | }); 131 | } 132 | 133 | return $this->increaseFilterCount(); 134 | } 135 | 136 | public static function glue($format, $filter): string 137 | { 138 | return "[v{$format}".static::MAPPING_GLUE."{$filter}]"; 139 | } 140 | 141 | public static function beforeGlue($input): string 142 | { 143 | return Str::before($input, static::MAPPING_GLUE); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Exporters/HandlesAdvancedMedia.php: -------------------------------------------------------------------------------- 1 | maps->push( 19 | new AdvancedOutputMapping($outs, $format, $output, $forceDisableAudio, $forceDisableVideo) 20 | ); 21 | 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exporters/HandlesConcatenation.php: -------------------------------------------------------------------------------- 1 | concatWithTranscoding = true; 27 | $this->concatWithVideo = $hasVideo; 28 | $this->concatWithAudio = $hasAudio; 29 | 30 | return $this; 31 | } 32 | 33 | private function addConcatFilterAndMapping(Media $outputMedia) 34 | { 35 | $sources = $this->driver->getMediaCollection()->map(function ($media, $key) { 36 | return "[{$key}]"; 37 | }); 38 | 39 | $concatWithVideo = $this->concatWithVideo ? 1 : 0; 40 | $concatWithAudio = $this->concatWithAudio ? 1 : 0; 41 | 42 | $this->addFilter( 43 | $sources->implode(''), 44 | "concat=n={$sources->count()}:v={$concatWithVideo}:a={$concatWithAudio}", 45 | '[concat]' 46 | )->addFormatOutputMapping($this->format, $outputMedia, ['[concat]']); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exporters/HandlesFrames.php: -------------------------------------------------------------------------------- 1 | mustBeAccurate = true; 20 | 21 | return $this; 22 | } 23 | 24 | public function unaccurate(): self 25 | { 26 | $this->mustBeAccurate = false; 27 | 28 | return $this; 29 | } 30 | 31 | public function getAccuracy(): bool 32 | { 33 | return $this->mustBeAccurate; 34 | } 35 | 36 | public function getFrameContents(): string 37 | { 38 | $this->returnFrameContents = true; 39 | 40 | return $this->save(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exporters/HandlesTimelapse.php: -------------------------------------------------------------------------------- 1 | timelapseFramerate = $framerate; 15 | 16 | return $this; 17 | } 18 | 19 | protected function addTimelapseParametersToFormat() 20 | { 21 | $this->format->setInitialParameters(array_merge( 22 | $this->format->getInitialParameters() ?: [], 23 | ['-framerate', $this->timelapseFramerate, '-f', 'image2'] 24 | )); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exporters/HasProgressListener.php: -------------------------------------------------------------------------------- 1 | onProgressCallback = $callback; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Only calls the callback if the percentage is below 100 and is different 37 | * from the previous emitted percentage. 38 | * 39 | * @return void 40 | */ 41 | private function applyProgressListenerToFormat(EventEmitterInterface $format) 42 | { 43 | $format->removeAllListeners('progress'); 44 | 45 | $format->on('progress', function ($media, $format, $percentage, $remaining = null, $rate = null) { 46 | if ($percentage !== $this->lastPercentage && $percentage < 100) { 47 | $this->lastPercentage = $percentage; 48 | $this->lastRemaining = $remaining ?: $this->lastRemaining; 49 | 50 | call_user_func($this->onProgressCallback, $this->lastPercentage, $this->lastRemaining, $rate); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exporters/MediaExporter.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 61 | 62 | $this->maps = new Collection; 63 | } 64 | 65 | protected function getDisk(): Disk 66 | { 67 | if ($this->toDisk) { 68 | return $this->toDisk; 69 | } 70 | 71 | $media = $this->driver->getMediaCollection(); 72 | 73 | /** @var Disk $disk */ 74 | $disk = $media->first()->getDisk(); 75 | 76 | return $this->toDisk = $disk->clone(); 77 | } 78 | 79 | public function inFormat(FormatInterface $format): self 80 | { 81 | $this->format = $format; 82 | 83 | return $this; 84 | } 85 | 86 | public function toDisk($disk) 87 | { 88 | $this->toDisk = Disk::make($disk); 89 | 90 | return $this; 91 | } 92 | 93 | public function withVisibility(string $visibility) 94 | { 95 | $this->visibility = $visibility; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Calls the callable with a TileFactory instance and 102 | * adds the freshly generated TileFilter. 103 | */ 104 | public function addTileFilter(callable $withTileFactory): self 105 | { 106 | $withTileFactory( 107 | $tileFactory = new TileFactory 108 | ); 109 | 110 | $this->addFilter($filter = $tileFactory->get()); 111 | 112 | if (! $tileFactory->vttOutputPath) { 113 | return $this; 114 | } 115 | 116 | return $this->afterSaving(function (MediaExporter $mediaExporter, Media $outputMedia) use ($filter, $tileFactory) { 117 | $generator = new VTTPreviewThumbnailsGenerator( 118 | $filter, 119 | $mediaExporter->driver->getDurationInSeconds(), 120 | $tileFactory->vttSequnceFilename ?: fn () => $outputMedia->getPath() 121 | ); 122 | 123 | $this->toDisk->put($tileFactory->vttOutputPath, $generator->getContents()); 124 | }); 125 | } 126 | 127 | /** 128 | * Returns the final command, useful for debugging purposes. 129 | * 130 | * @return mixed 131 | */ 132 | public function getCommand(?string $path = null) 133 | { 134 | $media = $this->prepareSaving($path); 135 | 136 | return $this->driver->getFinalCommand( 137 | $this->format ?: new NullFormat, 138 | optional($media)->getLocalPath() ?: '/dev/null' 139 | ); 140 | } 141 | 142 | /** 143 | * Dump the final command and end the script. 144 | * 145 | * @return void 146 | */ 147 | public function dd(?string $path = null) 148 | { 149 | dd($this->getCommand($path)); 150 | } 151 | 152 | /** 153 | * Adds a callable to the callbacks array. 154 | */ 155 | public function afterSaving(callable $callback): self 156 | { 157 | $this->afterSavingCallbacks[] = $callback; 158 | 159 | return $this; 160 | } 161 | 162 | private function prepareSaving(?string $path = null): ?Media 163 | { 164 | $outputMedia = $path ? $this->getDisk()->makeMedia($path) : null; 165 | 166 | if ($this->concatWithTranscoding && $outputMedia) { 167 | $this->addConcatFilterAndMapping($outputMedia); 168 | } 169 | 170 | if ($this->maps->isNotEmpty()) { 171 | $this->driver->getPendingComplexFilters()->each->apply($this->driver, $this->maps); 172 | 173 | $this->maps->map->apply($this->driver->get()); 174 | 175 | return $outputMedia; 176 | } 177 | 178 | if ($this->format && $this->onProgressCallback) { 179 | $this->applyProgressListenerToFormat($this->format); 180 | } 181 | 182 | if ($this->timelapseFramerate > 0) { 183 | $this->addTimelapseParametersToFormat(); 184 | } 185 | 186 | return $outputMedia; 187 | } 188 | 189 | protected function runAfterSavingCallbacks(?Media $outputMedia = null) 190 | { 191 | foreach ($this->afterSavingCallbacks as $key => $callback) { 192 | call_user_func($callback, $this, $outputMedia); 193 | 194 | unset($this->afterSavingCallbacks[$key]); 195 | } 196 | } 197 | 198 | public function save(?string $path = null) 199 | { 200 | $outputMedia = $this->prepareSaving($path); 201 | 202 | $this->driver->applyBeforeSavingCallbacks(); 203 | 204 | if ($this->maps->isNotEmpty()) { 205 | return $this->saveWithMappings(); 206 | } 207 | 208 | try { 209 | if ($this->driver->isConcat() && $outputMedia) { 210 | $this->driver->saveFromSameCodecs($outputMedia->getLocalPath()); 211 | } elseif ($this->driver->isFrame()) { 212 | $data = $this->driver->save( 213 | optional($outputMedia)->getLocalPath(), 214 | $this->getAccuracy(), 215 | $this->returnFrameContents 216 | ); 217 | 218 | if ($this->returnFrameContents) { 219 | $this->runAfterSavingCallbacks($outputMedia); 220 | 221 | return $data; 222 | } 223 | } else { 224 | $this->driver->save( 225 | $this->format ?: new NullFormat, 226 | optional($outputMedia)->getLocalPath() ?: '/dev/null' 227 | ); 228 | } 229 | } catch (RuntimeException $exception) { 230 | throw EncodingException::decorate($exception); 231 | } 232 | 233 | if ($outputMedia) { 234 | $outputMedia->copyAllFromTemporaryDirectory($this->visibility); 235 | $outputMedia->setVisibility($path, $this->visibility); 236 | } 237 | 238 | if ($this->onProgressCallback) { 239 | call_user_func($this->onProgressCallback, 100, 0, 0); 240 | } 241 | 242 | $this->runAfterSavingCallbacks($outputMedia); 243 | 244 | return $this->getMediaOpener(); 245 | } 246 | 247 | public function getProcessOutput(): ProcessOutput 248 | { 249 | return tap(new StdListener, function (StdListener $listener) { 250 | $this->addListener($listener)->save(); 251 | $listener->removeAllListeners(); 252 | $this->removeListener($listener); 253 | })->get(); 254 | } 255 | 256 | private function saveWithMappings(): MediaOpener 257 | { 258 | if ($this->onProgressCallback) { 259 | $this->applyProgressListenerToFormat($this->maps->last()->getFormat()); 260 | } 261 | 262 | try { 263 | $this->driver->save(); 264 | } catch (RuntimeException $exception) { 265 | throw EncodingException::decorate($exception); 266 | } 267 | 268 | if ($this->onProgressCallback) { 269 | call_user_func($this->onProgressCallback, 100, 0, 0); 270 | } 271 | 272 | $this->maps->map->getOutputMedia()->each->copyAllFromTemporaryDirectory($this->visibility); 273 | 274 | return $this->getMediaOpener(); 275 | } 276 | 277 | protected function getMediaOpener(): MediaOpener 278 | { 279 | return new MediaOpener( 280 | $this->driver->getMediaCollection()->last()->getDisk(), 281 | $this->driver, 282 | $this->driver->getMediaCollection() 283 | ); 284 | } 285 | 286 | /** 287 | * Forwards the call to the driver object and returns the result 288 | * if it's something different than the driver object itself. 289 | */ 290 | public function __call($method, $arguments) 291 | { 292 | $result = $this->forwardCallTo($driver = $this->driver, $method, $arguments); 293 | 294 | return ($result === $driver) ? $this : $result; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/Exporters/NoFormatException.php: -------------------------------------------------------------------------------- 1 | tileFilter = $tileFilter; 20 | $this->durationInSeconds = $durationInSeconds; 21 | $this->sequenceFilenameResolver = $sequenceFilenameResolver; 22 | } 23 | 24 | /** 25 | * Returns the x,y,w,h position of the given thumb key. 26 | */ 27 | private function getPositionOnTile(int $thumbKey): string 28 | { 29 | $row = (int) floor($thumbKey / $this->tileFilter->columns); 30 | 31 | $column = ($thumbKey - ($row * $this->tileFilter->columns)) % $this->tileFilter->columns; 32 | 33 | $dimension = $this->tileFilter->getCalculatedDimension(); 34 | 35 | $width = $dimension->getWidth(); 36 | $height = $dimension->getHeight(); 37 | 38 | // base position 39 | $x = $column * $width; 40 | $y = $row * $height; 41 | 42 | // add margin 43 | $x += $this->tileFilter->margin; 44 | $y += $this->tileFilter->margin; 45 | 46 | // add padding 47 | $x += $this->tileFilter->padding * $column; 48 | $y += $this->tileFilter->padding * $row; 49 | 50 | return implode(',', [$x, $y, $width, $height]); 51 | } 52 | 53 | /** 54 | * Returns the formatted timestamp of the given thumb key. 55 | */ 56 | private function getTimestamp(int $thumbKey): string 57 | { 58 | return sprintf( 59 | '%02d:%02d:%02d.000', 60 | ($thumbKey * $this->tileFilter->interval) / 3600, 61 | ($thumbKey * $this->tileFilter->interval) / 60 % 60, 62 | ($thumbKey * $this->tileFilter->interval) % 60 63 | ); 64 | } 65 | 66 | /** 67 | * Generates the WebVTT contents. 68 | */ 69 | public function getContents(): string 70 | { 71 | $thumbsPerTile = $this->tileFilter->rows * $this->tileFilter->columns; 72 | 73 | $totalFiles = ceil( 74 | ($this->durationInSeconds / $this->tileFilter->interval) / $thumbsPerTile 75 | ); 76 | 77 | return Collection::range(1, $totalFiles * $thumbsPerTile) 78 | ->map(function ($thumb) use ($thumbsPerTile) { 79 | $start = $this->getTimestamp($thumb - 1, $this->tileFilter->interval); 80 | $end = $this->getTimestamp($thumb, $this->tileFilter->interval); 81 | 82 | $fileKey = ceil($thumb / $thumbsPerTile); 83 | 84 | $filename = sprintf( 85 | call_user_func($this->sequenceFilenameResolver, $fileKey), 86 | $fileKey 87 | ); 88 | 89 | $positionOnTile = ($thumb - 1) % $thumbsPerTile; 90 | $position = $this->getPositionOnTile($positionOnTile); 91 | 92 | return implode(PHP_EOL, [ 93 | "{$start} --> {$end}", 94 | "{$filename}#xywh={$position}", 95 | ]); 96 | }) 97 | ->prepend('WEBVTT') 98 | ->implode(PHP_EOL.PHP_EOL); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/FFMpeg/AdvancedMedia.php: -------------------------------------------------------------------------------- 1 | getInputs(), $media->getFFMpegDriver(), FFProbe::make($media->getFFProbe())); 19 | } 20 | 21 | /** 22 | * Builds the command using the underlying library and then 23 | * prepends every input with its own set of headers. 24 | * 25 | * @return array 26 | */ 27 | protected function buildCommand() 28 | { 29 | $command = parent::buildCommand(); 30 | 31 | $inputKey = array_search(Arr::first($this->getInputs()), $command) - 1; 32 | 33 | foreach ($this->getInputs() as $key => $path) { 34 | $headers = $this->headers[$key]; 35 | 36 | if (empty($headers)) { 37 | $inputKey += 2; 38 | 39 | continue; 40 | } 41 | 42 | $command = static::mergeBeforeKey($command, $inputKey, static::compileHeaders($headers)); 43 | $inputKey += 4; 44 | } 45 | 46 | $command = $this->rebuildCommandWithCallbacks($command); 47 | 48 | return $command; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/FFMpeg/AdvancedOutputMapping.php: -------------------------------------------------------------------------------- 1 | outs = $outs; 46 | $this->format = $format; 47 | $this->output = $output; 48 | $this->forceDisableAudio = $forceDisableAudio; 49 | $this->forceDisableVideo = $forceDisableVideo; 50 | } 51 | 52 | /** 53 | * Applies the attributes to the format and specifies the video 54 | * bitrate if it's missing. 55 | */ 56 | public function apply(AdvancedMedia $advancedMedia): void 57 | { 58 | if ($this->format instanceof DefaultVideo) { 59 | $parameters = $this->format->getAdditionalParameters() ?: []; 60 | 61 | if (! in_array('-b:v', $parameters) && $this->format->getKiloBitrate() !== 0) { 62 | $parameters = array_merge(['-b:v', $this->format->getKiloBitrate().'k'], $parameters); 63 | } 64 | 65 | $this->format->setAdditionalParameters($parameters); 66 | } 67 | 68 | $advancedMedia->map($this->outs, $this->format, $this->output->getLocalPath(), $this->forceDisableAudio, $this->forceDisableVideo); 69 | } 70 | 71 | public function getFormat(): FormatInterface 72 | { 73 | return $this->format; 74 | } 75 | 76 | public function getOutputMedia(): Media 77 | { 78 | return $this->output; 79 | } 80 | 81 | public function hasOut(string $out): bool 82 | { 83 | return Collection::make($this->outs) 84 | ->map(function ($out) { 85 | return HLSVideoFilters::beforeGlue($out); 86 | }) 87 | ->contains(HLSVideoFilters::beforeGlue($out)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/FFMpeg/AudioMedia.php: -------------------------------------------------------------------------------- 1 | getPathfile(), $audio->getFFMpegDriver(), FFProbe::make($audio->getFFProbe())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/FFMpeg/CopyFormat.php: -------------------------------------------------------------------------------- 1 | audioCodec = 'copy'; 12 | 13 | $this->audioKiloBitrate = null; 14 | } 15 | 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function getExtraParams() 20 | { 21 | return ['-codec', 'copy']; 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function getAvailableAudioCodecs() 28 | { 29 | return ['copy']; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/FFMpeg/CopyVideoFormat.php: -------------------------------------------------------------------------------- 1 | audioCodec = 'copy'; 12 | $this->videoCodec = 'copy'; 13 | 14 | $this->kiloBitrate = 0; 15 | $this->audioKiloBitrate = null; 16 | } 17 | 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | public function getAvailableAudioCodecs() 22 | { 23 | return ['copy']; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getAvailableVideoCodecs() 30 | { 31 | return ['copy']; 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function supportBFrames() 38 | { 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/FFMpeg/FFProbe.php: -------------------------------------------------------------------------------- 1 | media = $media; 19 | 20 | return $this; 21 | } 22 | 23 | /** 24 | * Create a new instance of this class with the instance of the underlying library. 25 | */ 26 | public static function make(FFMpegFFProbe $probe): self 27 | { 28 | if ($probe instanceof FFProbe) { 29 | return $probe; 30 | } 31 | 32 | return new static($probe->getFFProbeDriver(), $probe->getCache()); 33 | } 34 | 35 | private function shouldUseCustomProbe($pathfile): bool 36 | { 37 | if (! $this->media) { 38 | return false; 39 | } 40 | 41 | if ($this->media->getLocalPath() !== $pathfile) { 42 | return false; 43 | } 44 | 45 | if (empty($this->media->getCompiledInputOptions())) { 46 | return false; 47 | } 48 | 49 | if (! $this->getOptionsTester()->has('-show_streams')) { 50 | throw new RuntimeException('This version of ffprobe is too old and does not support `-show_streams` option, please upgrade'); 51 | } 52 | 53 | return true; 54 | } 55 | 56 | /** 57 | * Probes the streams contained in a given file. 58 | * 59 | * @param string $pathfile 60 | * @return \FFMpeg\FFProbe\DataMapping\StreamCollection 61 | * 62 | * @throws \FFMpeg\Exception\InvalidArgumentException 63 | * @throws \FFMpeg\Exception\RuntimeException 64 | */ 65 | public function streams($pathfile) 66 | { 67 | if (! $this->shouldUseCustomProbe($pathfile)) { 68 | return parent::streams($pathfile); 69 | } 70 | 71 | return $this->probeStreams($pathfile, '-show_streams', static::TYPE_STREAMS); 72 | } 73 | 74 | /** 75 | * Probes the format of a given file. 76 | * 77 | * @param string $pathfile 78 | * @return \FFMpeg\FFProbe\DataMapping\Format A Format object 79 | * 80 | * @throws \FFMpeg\Exception\InvalidArgumentException 81 | * @throws \FFMpeg\Exception\RuntimeException 82 | */ 83 | public function format($pathfile) 84 | { 85 | if (! $this->shouldUseCustomProbe($pathfile)) { 86 | return parent::format($pathfile); 87 | } 88 | 89 | return $this->probeStreams($pathfile, '-show_format', static::TYPE_FORMAT); 90 | } 91 | 92 | /** 93 | * This is just copy-paste from FFMpeg\FFProbe... 94 | * It prepends the command with the input options. 95 | */ 96 | private function probeStreams($pathfile, $command, $type, $allowJson = true) 97 | { 98 | $commands = array_merge( 99 | $this->media->getCompiledInputOptions(), 100 | [$pathfile, $command] 101 | ); 102 | 103 | $parseIsToDo = false; 104 | 105 | if ($allowJson && $this->getOptionsTester()->has('-print_format')) { 106 | // allowed in latest PHP-FFmpeg version 107 | $commands[] = '-print_format'; 108 | $commands[] = 'json'; 109 | } elseif ($allowJson && $this->getOptionsTester()->has('-of')) { 110 | // option has changed in avconv 9 111 | $commands[] = '-of'; 112 | $commands[] = 'json'; 113 | } else { 114 | $parseIsToDo = true; 115 | } 116 | 117 | try { 118 | $output = $this->getFFProbeDriver()->command($commands); 119 | } catch (ExecutionFailureException $e) { 120 | throw new RuntimeException(sprintf('Unable to probe %s', $pathfile), $e->getCode(), $e); 121 | } 122 | 123 | if ($parseIsToDo) { 124 | $data = $this->getParser()->parse($type, $output); 125 | } else { 126 | try { 127 | $data = @json_decode($output, true); 128 | 129 | if (json_last_error() !== JSON_ERROR_NONE) { 130 | throw new RuntimeException(sprintf('Unable to parse json %s', $output)); 131 | } 132 | } catch (RuntimeException $e) { 133 | return $this->probeStreams($pathfile, $command, $type, false); 134 | } 135 | } 136 | 137 | return $this->getMapper()->map($type, $data); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/FFMpeg/ImageFormat.php: -------------------------------------------------------------------------------- 1 | kiloBitrate = 0; 12 | $this->audioKiloBitrate = null; 13 | } 14 | 15 | /** 16 | * Gets the kiloBitrate value. 17 | * 18 | * @return int 19 | */ 20 | public function getKiloBitrate() 21 | { 22 | return $this->kiloBitrate; 23 | } 24 | 25 | /** 26 | * Returns the modulus used by the Resizable video. 27 | * 28 | * This used to calculate the target dimensions while maintaining the best 29 | * aspect ratio. 30 | * 31 | * @see http://www.undeadborn.net/tools/rescalculator.php 32 | * 33 | * @return int 34 | */ 35 | public function getModulus() 36 | { 37 | return 0; 38 | } 39 | 40 | /** 41 | * Returns the video codec. 42 | * 43 | * @return string 44 | */ 45 | public function getVideoCodec() 46 | { 47 | return null; 48 | } 49 | 50 | /** 51 | * Returns true if the current format supports B-Frames. 52 | * 53 | * @see https://wikipedia.org/wiki/Video_compression_picture_types 54 | * 55 | * @return bool 56 | */ 57 | public function supportBFrames() 58 | { 59 | return false; 60 | } 61 | 62 | /** 63 | * Returns the list of available video codecs for this format. 64 | * 65 | * @return array 66 | */ 67 | public function getAvailableVideoCodecs() 68 | { 69 | return []; 70 | } 71 | 72 | /** 73 | * Returns the list of additional parameters for this format. 74 | * 75 | * @return array 76 | */ 77 | public function getAdditionalParameters() 78 | { 79 | return ['-f', 'image2']; 80 | } 81 | 82 | /** 83 | * Returns the list of initial parameters for this format. 84 | * 85 | * @return array 86 | */ 87 | public function getInitialParameters() 88 | { 89 | return []; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getExtraParams() 96 | { 97 | return []; 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public function getAvailableAudioCodecs() 104 | { 105 | return []; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/FFMpeg/InteractsWithBeforeSavingCallbacks.php: -------------------------------------------------------------------------------- 1 | beforeSavingCallbacks = $beforeSavingCallbacks; 15 | 16 | return $this; 17 | } 18 | 19 | protected function rebuildCommandWithCallbacks($command) 20 | { 21 | foreach ($this->beforeSavingCallbacks as $key => $callback) { 22 | $newCommand = call_user_func($callback, $command); 23 | 24 | $command = ! is_null($newCommand) ? $newCommand : $command; 25 | 26 | unset($this->beforeSavingCallbacks[$key]); 27 | } 28 | 29 | return $command; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/FFMpeg/InteractsWithHttpHeaders.php: -------------------------------------------------------------------------------- 1 | headers; 19 | } 20 | 21 | public function setHeaders(array $headers = []): self 22 | { 23 | $this->headers = $headers; 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * Maps the headers into a key-value string for FFmpeg. Returns 30 | * an array of arguments to pass into the command. 31 | */ 32 | public static function compileHeaders(array $headers = []): array 33 | { 34 | if (empty($headers)) { 35 | return []; 36 | } 37 | 38 | $headers = Collection::make($headers)->map(function ($value, $key) { 39 | return "{$key}: {$value}"; 40 | })->filter()->push('')->implode("\r\n"); 41 | 42 | return ['-headers', $headers]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/FFMpeg/InteractsWithInputPath.php: -------------------------------------------------------------------------------- 1 | in = $in; 25 | $this->out = $out; 26 | $this->arguments = $arguments; 27 | } 28 | 29 | /** 30 | * Removes all non-numeric characters from the 'in' attribute. 31 | */ 32 | public function normalizeIn(): int 33 | { 34 | return preg_replace('/[^0-9]/', '', HLSVideoFilters::beforeGlue($this->in)); 35 | } 36 | 37 | /** 38 | * Filters the given MediaCollection down to one item by 39 | * guessing the key by the 'in' attribute. 40 | */ 41 | private function singleMediaCollection(MediaCollection $mediaCollection): MediaCollection 42 | { 43 | $media = $mediaCollection->get($this->normalizeIn()) ?: $mediaCollection->first(); 44 | 45 | return MediaCollection::make([$media]); 46 | } 47 | 48 | /** 49 | * Applies the filter to the FFMpeg driver. 50 | */ 51 | public function apply(PHPFFMpeg $driver, Collection $maps): void 52 | { 53 | $freshDriver = $driver->fresh() 54 | ->open($this->singleMediaCollection($driver->getMediaCollection())) 55 | ->addFilter(...$this->arguments); 56 | 57 | $format = $maps->filter->hasOut($this->out)->first()->getFormat(); 58 | 59 | Collection::make($freshDriver->getFilters())->map(function ($filter) use ($freshDriver, $format) { 60 | $parameters = $filter->apply($freshDriver->get(), $format); 61 | 62 | $parameters = Arr::where($parameters, function ($parameter) { 63 | return ! in_array($parameter, ['-vf', '-filter:v', '-filter_complex']); 64 | }); 65 | 66 | return implode(' ', $parameters); 67 | })->each(function ($customCompiledFilter) use ($driver) { 68 | $driver->addFilter($this->in, $customCompiledFilter, $this->out); 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/FFMpeg/NullFormat.php: -------------------------------------------------------------------------------- 1 | audioKiloBitrate = null; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | public function getExtraParams() 18 | { 19 | return []; 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public function getAvailableAudioCodecs() 26 | { 27 | return []; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/FFMpeg/ProgressListenerDecorator.php: -------------------------------------------------------------------------------- 1 | format = $format; 33 | } 34 | 35 | public static function decorate($format) 36 | { 37 | if ($format instanceof VideoInterface) { 38 | return new VideoProgressListenerDecorator($format); 39 | } 40 | 41 | return new static($format); 42 | } 43 | 44 | public function getListeners(): array 45 | { 46 | return $this->listeners; 47 | } 48 | 49 | public function createProgressListener(MediaTypeInterface $media, FFProbe $ffprobe, $pass, $total, $duration = 0) 50 | { 51 | return tap($this->format->createProgressListener(...func_get_args()), function (array $listeners) { 52 | $this->listeners = array_merge($this->listeners, $listeners); 53 | }); 54 | } 55 | 56 | public function on($event, callable $listener) 57 | { 58 | return $this->format->on(...func_get_args()); 59 | } 60 | 61 | public function once($event, callable $listener) 62 | { 63 | return $this->format->once(...func_get_args()); 64 | } 65 | 66 | public function removeListener($event, callable $listener) 67 | { 68 | return $this->format->removeListener(...func_get_args()); 69 | } 70 | 71 | public function removeAllListeners($event = null) 72 | { 73 | return $this->format->removeAllListeners(...func_get_args()); 74 | } 75 | 76 | public function listeners($event = null) 77 | { 78 | return $this->format->listeners(...func_get_args()); 79 | } 80 | 81 | public function emit($event, array $arguments = []) 82 | { 83 | return $this->format->emit(...func_get_args()); 84 | } 85 | 86 | public function getPasses() 87 | { 88 | return $this->format->getPasses(...func_get_args()); 89 | } 90 | 91 | public function getExtraParams() 92 | { 93 | return $this->format->getExtraParams(...func_get_args()); 94 | } 95 | 96 | public function getAudioKiloBitrate() 97 | { 98 | return $this->format->getAudioKiloBitrate(...func_get_args()); 99 | } 100 | 101 | public function getAudioChannels() 102 | { 103 | return $this->format->getAudioChannels(...func_get_args()); 104 | } 105 | 106 | public function getAudioCodec() 107 | { 108 | return $this->format->getAudioCodec(...func_get_args()); 109 | } 110 | 111 | public function getAvailableAudioCodecs() 112 | { 113 | return $this->format->getAvailableAudioCodecs(...func_get_args()); 114 | } 115 | 116 | public function __get($key) 117 | { 118 | return $this->format->{$key}; 119 | } 120 | 121 | public function __call($method, $arguments) 122 | { 123 | return $this->forwardCallTo($this->format, $method, $arguments); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/FFMpeg/RebuildsCommands.php: -------------------------------------------------------------------------------- 1 | rebuildCommandWithHeaders($command); 25 | $command = $this->rebuildCommandWithCallbacks($command); 26 | 27 | return $command; 28 | } 29 | 30 | private function rebuildCommandWithHeaders($command) 31 | { 32 | if (empty($this->headers)) { 33 | return $command; 34 | } 35 | 36 | return Collection::make($command)->map(function ($command) { 37 | return static::mergeBeforePathInput( 38 | $command, 39 | $this->getPathfile(), 40 | static::compileHeaders($this->headers) 41 | ); 42 | })->all(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/FFMpeg/StdListener.php: -------------------------------------------------------------------------------- 1 | [], 28 | Process::ERR => [], 29 | Process::OUT => [], 30 | ]; 31 | 32 | public function __construct(string $eventName = 'listen') 33 | { 34 | $this->eventName = $eventName; 35 | } 36 | 37 | /** 38 | * Handler for a new line of data. 39 | * 40 | * @param string $type 41 | * @param string $data 42 | * @return void 43 | */ 44 | public function handle($type, $data) 45 | { 46 | $lines = preg_split('/\n|\r\n?/', $data); 47 | 48 | foreach ($lines as $line) { 49 | $this->emit($this->eventName, [$line, $type]); 50 | 51 | $line = trim($line); 52 | 53 | $this->data[$type][] = $line; 54 | 55 | $this->data[static::TYPE_ALL][] = $line; 56 | } 57 | } 58 | 59 | /** 60 | * Returns the collected output lines. 61 | * 62 | * @return array 63 | */ 64 | public function get(): ProcessOutput 65 | { 66 | return new ProcessOutput( 67 | $this->data[static::TYPE_ALL], 68 | $this->data[Process::ERR], 69 | $this->data[Process::OUT] 70 | ); 71 | } 72 | 73 | public function forwardedEvents() 74 | { 75 | return [$this->eventName]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/FFMpeg/VideoMedia.php: -------------------------------------------------------------------------------- 1 | getPathfile(), $video->getFFMpegDriver(), $video->getFFProbe()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/FFMpeg/VideoProgressListenerDecorator.php: -------------------------------------------------------------------------------- 1 | format->getKiloBitrate(...func_get_args()); 12 | } 13 | 14 | public function getModulus() 15 | { 16 | return $this->format->getModulus(...func_get_args()); 17 | } 18 | 19 | public function getVideoCodec() 20 | { 21 | return $this->format->getVideoCodec(...func_get_args()); 22 | } 23 | 24 | public function supportBFrames() 25 | { 26 | return $this->format->supportBFrames(...func_get_args()); 27 | } 28 | 29 | public function getAvailableVideoCodecs() 30 | { 31 | return $this->format->getAvailableVideoCodecs(...func_get_args()); 32 | } 33 | 34 | public function getAdditionalParameters() 35 | { 36 | return $this->format->getAdditionalParameters(...func_get_args()); 37 | } 38 | 39 | public function getInitialParameters() 40 | { 41 | return $this->format->getInitialParameters(...func_get_args()); 42 | } 43 | 44 | public function getAvailableAudioCodecs() 45 | { 46 | return $this->format->getAvailableAudioCodecs(...func_get_args()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Filesystem/Disk.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 38 | } 39 | 40 | /** 41 | * Little helper method to instantiate this class. 42 | */ 43 | public static function make($disk): self 44 | { 45 | if ($disk instanceof self) { 46 | return $disk; 47 | } 48 | 49 | return new static($disk); 50 | } 51 | 52 | public static function makeTemporaryDisk(): self 53 | { 54 | $filesystemAdapter = app('filesystem')->createLocalDriver([ 55 | 'root' => app(TemporaryDirectories::class)->create(), 56 | ]); 57 | 58 | return new static($filesystemAdapter); 59 | } 60 | 61 | /** 62 | * Creates a fresh instance, mostly used to force a new TemporaryDirectory. 63 | */ 64 | public function clone(): self 65 | { 66 | return new Disk($this->disk); 67 | } 68 | 69 | /** 70 | * Creates a new TemporaryDirectory instance if none is set, otherwise 71 | * it returns the current one. 72 | */ 73 | public function getTemporaryDirectory(): string 74 | { 75 | if ($this->temporaryDirectory) { 76 | return $this->temporaryDirectory; 77 | } 78 | 79 | return $this->temporaryDirectory = app(TemporaryDirectories::class)->create(); 80 | } 81 | 82 | public function makeMedia(string $path): Media 83 | { 84 | return Media::make($this, $path); 85 | } 86 | 87 | /** 88 | * Returns the name of the disk. It generates a name if the disk 89 | * is an instance of Flysystem. 90 | */ 91 | public function getName(): string 92 | { 93 | if (is_string($this->disk)) { 94 | return $this->disk; 95 | } 96 | 97 | return get_class($this->getFlysystemAdapter()).'_'.md5(spl_object_id($this->getFlysystemAdapter())); 98 | } 99 | 100 | public function getFilesystemAdapter(): FilesystemAdapter 101 | { 102 | if ($this->filesystemAdapter) { 103 | return $this->filesystemAdapter; 104 | } 105 | 106 | if ($this->disk instanceof Filesystem) { 107 | return $this->filesystemAdapter = $this->disk; 108 | } 109 | 110 | return $this->filesystemAdapter = Storage::disk($this->disk); 111 | } 112 | 113 | private function getFlysystemDriver(): LeagueFilesystem 114 | { 115 | return $this->getFilesystemAdapter()->getDriver(); 116 | } 117 | 118 | private function getFlysystemAdapter(): FlysystemFilesystemAdapter 119 | { 120 | return $this->getFilesystemAdapter()->getAdapter(); 121 | } 122 | 123 | public function isLocalDisk(): bool 124 | { 125 | return $this->getFlysystemAdapter() instanceof LocalFilesystemAdapter; 126 | } 127 | 128 | /** 129 | * Replaces backward slashes into forward slashes. 130 | */ 131 | public static function normalizePath(string $path): string 132 | { 133 | return str_replace('\\', '/', $path); 134 | } 135 | 136 | /** 137 | * Get the full path for the file at the given "short" path. 138 | */ 139 | public function path(string $path): string 140 | { 141 | $path = $this->getFilesystemAdapter()->path($path); 142 | 143 | return $this->isLocalDisk() ? static::normalizePath($path) : $path; 144 | } 145 | 146 | /** 147 | * Forwards all calls to Laravel's FilesystemAdapter which will pass 148 | * dynamic methods call onto Flysystem. 149 | */ 150 | public function __call($method, $parameters) 151 | { 152 | return $this->forwardCallTo($this->getFilesystemAdapter(), $method, $parameters); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Filesystem/HasInputOptions.php: -------------------------------------------------------------------------------- 1 | inputOptions; 15 | } 16 | 17 | public function setInputOptions(array $options = []): self 18 | { 19 | $this->inputOptions = $options; 20 | 21 | return $this; 22 | } 23 | 24 | public function getCompiledInputOptions(): array 25 | { 26 | return $this->getInputOptions(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Filesystem/Media.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 29 | $this->path = $path; 30 | 31 | $this->makeDirectory(); 32 | } 33 | 34 | public static function make($disk, string $path): self 35 | { 36 | return new static(Disk::make($disk), $path); 37 | } 38 | 39 | public function getDisk(): Disk 40 | { 41 | return $this->disk; 42 | } 43 | 44 | public function getPath(): string 45 | { 46 | return $this->path; 47 | } 48 | 49 | public function getDirectory(): ?string 50 | { 51 | $directory = rtrim(pathinfo($this->getPath())['dirname'], DIRECTORY_SEPARATOR); 52 | 53 | if ($directory === '.') { 54 | $directory = ''; 55 | } 56 | 57 | if ($directory) { 58 | $directory .= DIRECTORY_SEPARATOR; 59 | } 60 | 61 | return $directory; 62 | } 63 | 64 | private function makeDirectory(): void 65 | { 66 | $disk = $this->getDisk(); 67 | 68 | if (! $disk->isLocalDisk()) { 69 | $disk = $this->temporaryDirectoryDisk(); 70 | } 71 | 72 | $directory = $this->getDirectory(); 73 | 74 | if ($disk->has($directory)) { 75 | return; 76 | } 77 | 78 | $disk->makeDirectory($directory); 79 | } 80 | 81 | public function getFilenameWithoutExtension(): string 82 | { 83 | return pathinfo($this->getPath())['filename']; 84 | } 85 | 86 | public function getFilename(): string 87 | { 88 | return pathinfo($this->getPath())['basename']; 89 | } 90 | 91 | private function temporaryDirectoryDisk(): Disk 92 | { 93 | return Disk::make($this->temporaryDirectoryAdapter()); 94 | } 95 | 96 | private function temporaryDirectoryAdapter(): FilesystemAdapter 97 | { 98 | if (! $this->temporaryDirectory) { 99 | $this->temporaryDirectory = $this->getDisk()->getTemporaryDirectory(); 100 | } 101 | 102 | return app('filesystem')->createLocalDriver( 103 | ['root' => $this->temporaryDirectory] 104 | ); 105 | } 106 | 107 | public function getLocalPath(): string 108 | { 109 | $disk = $this->getDisk(); 110 | $path = $this->getPath(); 111 | 112 | if ($disk->isLocalDisk()) { 113 | return $disk->path($path); 114 | } 115 | 116 | $temporaryDirectoryDisk = $this->temporaryDirectoryDisk(); 117 | 118 | if ($disk->exists($path) && ! $temporaryDirectoryDisk->exists($path)) { 119 | $temporaryDirectoryDisk->writeStream($path, $disk->readStream($path)); 120 | } 121 | 122 | return $temporaryDirectoryDisk->path($path); 123 | } 124 | 125 | public function copyAllFromTemporaryDirectory(?string $visibility = null) 126 | { 127 | if (! $this->temporaryDirectory) { 128 | return $this; 129 | } 130 | 131 | $temporaryDirectoryDisk = $this->temporaryDirectoryDisk(); 132 | 133 | $destinationAdapater = $this->getDisk()->getFilesystemAdapter(); 134 | 135 | foreach ($temporaryDirectoryDisk->allFiles() as $path) { 136 | $destinationAdapater->writeStream($path, $temporaryDirectoryDisk->readStream($path)); 137 | 138 | if ($visibility) { 139 | $destinationAdapater->setVisibility($path, $visibility); 140 | } 141 | } 142 | 143 | return $this; 144 | } 145 | 146 | public function setVisibility(string $path, ?string $visibility = null) 147 | { 148 | $disk = $this->getDisk(); 149 | 150 | if ($visibility && $disk->isLocalDisk()) { 151 | $disk->setVisibility($path, $visibility); 152 | } 153 | 154 | return $this; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Filesystem/MediaCollection.php: -------------------------------------------------------------------------------- 1 | items = new Collection($items); 23 | } 24 | 25 | public static function make(array $items = []): self 26 | { 27 | return new static($items); 28 | } 29 | 30 | /** 31 | * Returns an array with all locals paths of the Media items. 32 | */ 33 | public function getLocalPaths(): array 34 | { 35 | return $this->items->map->getLocalPath()->all(); 36 | } 37 | 38 | /** 39 | * Returns an array with all headers of the Media items. 40 | */ 41 | public function getHeaders(): array 42 | { 43 | return $this->items->map(function ($media) { 44 | return $media instanceof MediaOnNetwork ? $media->getHeaders() : []; 45 | })->all(); 46 | } 47 | 48 | public function collection(): Collection 49 | { 50 | return $this->items; 51 | } 52 | 53 | /** 54 | * Count the number of items in the collection. 55 | */ 56 | public function count(): int 57 | { 58 | return $this->items->count(); 59 | } 60 | 61 | public function __call($method, $parameters) 62 | { 63 | return $this->forwardCallTo($this->collection(), $method, $parameters); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Filesystem/MediaOnNetwork.php: -------------------------------------------------------------------------------- 1 | path = $path; 21 | $this->headers = $headers; 22 | } 23 | 24 | public static function make(string $path, array $headers = []): self 25 | { 26 | return new static($path, $headers); 27 | } 28 | 29 | public function getPath(): string 30 | { 31 | return $this->path; 32 | } 33 | 34 | public function getDisk(): Disk 35 | { 36 | return Disk::make(config('filesystems.default')); 37 | } 38 | 39 | public function getLocalPath(): string 40 | { 41 | return $this->path; 42 | } 43 | 44 | public function getFilenameWithoutExtension(): string 45 | { 46 | return pathinfo($this->getPath())['filename']; 47 | } 48 | 49 | public function getFilename(): string 50 | { 51 | return pathinfo($this->getPath())['basename']; 52 | } 53 | 54 | public function getCompiledInputOptions(): array 55 | { 56 | return array_merge($this->getInputOptions(), $this->getCompiledHeaders()); 57 | } 58 | 59 | public function getCompiledHeaders(): array 60 | { 61 | return static::compileHeaders($this->getHeaders()); 62 | } 63 | 64 | /** 65 | * Downloads the Media from the internet and stores it in 66 | * a temporary directory. 67 | */ 68 | public function toMedia(?callable $withCurl = null): Media 69 | { 70 | $disk = Disk::makeTemporaryDisk(); 71 | 72 | $curl = curl_init(); 73 | curl_setopt($curl, CURLOPT_URL, $this->path); 74 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 75 | 76 | if (! empty($this->headers)) { 77 | $headers = Collection::make($this->headers)->map(function ($value, $header) { 78 | return "{$header}: {$value}"; 79 | })->all(); 80 | 81 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 82 | } 83 | 84 | if ($withCurl) { 85 | $curl = $withCurl($curl) ?: $curl; 86 | } 87 | 88 | $contents = curl_exec($curl); 89 | curl_close($curl); 90 | 91 | $disk->getFilesystemAdapter()->put( 92 | $filename = $this->getFilename(), 93 | $contents 94 | ); 95 | 96 | return new Media($disk, $filename); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Filesystem/TemporaryDirectories.php: -------------------------------------------------------------------------------- 1 | root = rtrim($root, '/'); 29 | } 30 | 31 | /** 32 | * Returns the full path a of new temporary directory. 33 | */ 34 | public function create(): string 35 | { 36 | $directory = $this->root.'/'.bin2hex(random_bytes(8)); 37 | 38 | mkdir($directory, 0777, true); 39 | 40 | return $this->directories[] = $directory; 41 | } 42 | 43 | /** 44 | * Loop through all directories and delete them. 45 | */ 46 | public function deleteAll(): void 47 | { 48 | $filesystem = new Filesystem; 49 | 50 | foreach ($this->directories as $directory) { 51 | $filesystem->deleteDirectory($directory); 52 | } 53 | 54 | $this->directories = []; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Filters/TileFactory.php: -------------------------------------------------------------------------------- 1 | vttOutputPath = $outputPath; 43 | 44 | $this->vttSequnceFilename = is_string($sequnceFilename) 45 | ? fn () => $sequnceFilename 46 | : $sequnceFilename; 47 | 48 | return $this; 49 | } 50 | 51 | public function interval(float $interval): self 52 | { 53 | $this->interval = $interval; 54 | 55 | return $this; 56 | } 57 | 58 | public function width(int $width): self 59 | { 60 | $this->width = $width; 61 | 62 | return $this; 63 | } 64 | 65 | public function height(int $height): self 66 | { 67 | $this->height = $height; 68 | 69 | return $this; 70 | } 71 | 72 | public function scale(?int $width = null, ?int $height = null): self 73 | { 74 | return $this->width($width ?: -1)->height($height ?: -1); 75 | } 76 | 77 | public function grid(int $columns, int $rows): self 78 | { 79 | $this->columns = $columns; 80 | $this->rows = $rows; 81 | 82 | return $this; 83 | } 84 | 85 | public function padding(int $padding): self 86 | { 87 | $this->padding = $padding; 88 | 89 | return $this; 90 | } 91 | 92 | public function margin(int $margin): self 93 | { 94 | $this->margin = $margin; 95 | 96 | return $this; 97 | } 98 | 99 | public function quality(?int $quality = null): self 100 | { 101 | $this->quality = $quality; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Returns a new instance of the TileFilter. 108 | */ 109 | public function get(): TileFilter 110 | { 111 | return new TileFilter( 112 | $this->interval, 113 | $this->width, 114 | $this->height, 115 | $this->columns, 116 | $this->rows, 117 | $this->padding, 118 | $this->margin, 119 | $this->quality 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Filters/TileFilter.php: -------------------------------------------------------------------------------- 1 | interval = $interval; 49 | $this->width = $width; 50 | $this->height = $height; 51 | $this->columns = $columns; 52 | $this->rows = $rows; 53 | $this->padding = $padding; 54 | $this->margin = $margin; 55 | $this->quality = $quality; 56 | $this->priority = $priority; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function getPriority() 63 | { 64 | return $this->priority; 65 | } 66 | 67 | /** 68 | * Get name of the filter. 69 | * 70 | * @return string 71 | */ 72 | public function getName() 73 | { 74 | return 'thumbnail_sprite'; 75 | } 76 | 77 | /** 78 | * Get minimal version of ffmpeg starting with which this filter is supported. 79 | * 80 | * @return string 81 | */ 82 | public function getMinimalFFMpegVersion() 83 | { 84 | return '4.3'; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function apply(Video $video, VideoInterface $format) 91 | { 92 | return $this->getCommands( 93 | $video->getStreams()->videos()->first() 94 | ); 95 | } 96 | 97 | public function getCalculatedDimension(): Dimension 98 | { 99 | return $this->calculatedDimension ?: new Dimension($this->width, $this->height); 100 | } 101 | 102 | private function calculateDimension(Dimension $streamDimension): Dimension 103 | { 104 | $width = $this->width; 105 | $height = $this->height; 106 | 107 | if ($width > 0 && $height < 1) { 108 | $height = $streamDimension->getRatio()->calculateHeight($width); 109 | } elseif ($height > 0 && $width < 1) { 110 | $width = $streamDimension->getRatio()->calculateWidth($height); 111 | } elseif ($width < 1 && $height < 1) { 112 | $width = $streamDimension->getWidth(); 113 | $height = $streamDimension->getHeight(); 114 | } 115 | 116 | return $this->calculatedDimension = new Dimension($width, $height); 117 | } 118 | 119 | /** 120 | * @return array 121 | */ 122 | protected function getCommands(Stream $stream) 123 | { 124 | $frameRateInterval = null; 125 | 126 | if ($frameRate = StreamParser::new($stream)->getFrameRate()) { 127 | $frameRateInterval = round($frameRate * $this->interval); 128 | } 129 | 130 | $dimension = $this->calculateDimension($stream->getDimensions()); 131 | 132 | $select = $frameRateInterval 133 | ? "select=not(mod(n\,{$frameRateInterval}))" 134 | : "select=not(mod(t\,{$this->interval}))"; 135 | 136 | $commands = [ 137 | '-vsync', 138 | '0', 139 | ]; 140 | 141 | if (! is_null($this->quality)) { 142 | $commands = array_merge($commands, [ 143 | '-qscale:v', 144 | $this->quality, 145 | ]); 146 | } 147 | 148 | $commands = array_merge($commands, [ 149 | '-vf', 150 | "{$select},scale={$dimension->getWidth()}:{$dimension->getHeight()},tile={$this->columns}x{$this->rows}:margin={$this->margin}:padding={$this->padding}", 151 | ]); 152 | 153 | return $commands; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Filters/WatermarkFactory.php: -------------------------------------------------------------------------------- 1 | disk = Disk::make(config('filesystems.default')); 69 | } 70 | 71 | /** 72 | * Set the disk to open files from. 73 | */ 74 | public function fromDisk($disk): self 75 | { 76 | $this->disk = Disk::make($disk); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Instantiates a Media object for the given path. 83 | */ 84 | public function open(string $path): self 85 | { 86 | $this->media = Media::make($this->disk, $path); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Instantiates a MediaOnNetwork object for the given url and transforms 93 | * it into a Media object. 94 | */ 95 | public function openUrl(string $path, array $headers = [], ?callable $withCurl = null): self 96 | { 97 | $this->media = MediaOnNetwork::make($path, $headers)->toMedia($withCurl); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Sets the offset from to top. 104 | * 105 | * @param int $offset 106 | */ 107 | public function top($offset = 0): self 108 | { 109 | $this->top = $offset; 110 | $this->bottom = null; 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Sets the offset from the right. 117 | * 118 | * @param int $offset 119 | */ 120 | public function right($offset = 0): self 121 | { 122 | $this->right = $offset; 123 | $this->left = null; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Sets the offset from the bottom. 130 | * 131 | * @param int $offset 132 | */ 133 | public function bottom($offset = 0): self 134 | { 135 | $this->bottom = $offset; 136 | $this->top = null; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Sets the offset from the left. 143 | * 144 | * @param int $offset 145 | */ 146 | public function left($offset = 0): self 147 | { 148 | $this->left = $offset; 149 | $this->right = null; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Setter for the horizontal alignment with an optional offset. 156 | * 157 | * @param int $offset 158 | */ 159 | public function horizontalAlignment(string $alignment, $offset = 0): self 160 | { 161 | switch ($alignment) { 162 | case self::LEFT: 163 | $this->alignments['x'] = $offset; 164 | break; 165 | case self::CENTER: 166 | $this->alignments['x'] = "(W-w)/2+{$offset}"; 167 | break; 168 | case self::RIGHT: 169 | $this->alignments['x'] = "W-w+{$offset}"; 170 | break; 171 | } 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Setter for the vertical alignment with an optional offset. 178 | * 179 | * @param int $offset 180 | */ 181 | public function verticalAlignment(string $alignment, $offset = 0): self 182 | { 183 | switch ($alignment) { 184 | case self::TOP: 185 | $this->alignments['y'] = $offset; 186 | break; 187 | case self::CENTER: 188 | $this->alignments['y'] = "(H-h)/2+{$offset}"; 189 | break; 190 | case self::BOTTOM: 191 | $this->alignments['y'] = "H-h+{$offset}"; 192 | break; 193 | } 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * Returns the full path to the watermark file. 200 | */ 201 | public function getPath(): string 202 | { 203 | if (! $this->image) { 204 | return $this->media->getLocalPath(); 205 | } 206 | 207 | $path = Disk::makeTemporaryDisk() 208 | ->makeMedia($this->media->getFilename()) 209 | ->getLocalPath(); 210 | 211 | $this->image->save($path); 212 | 213 | return $path; 214 | } 215 | 216 | /** 217 | * Returns a new instance of the WatermarkFilter. 218 | * 219 | * @return \FFMpeg\Filters\Video\WatermarkFilter 220 | */ 221 | public function get(): WatermarkFilter 222 | { 223 | $path = $this->getPath(); 224 | 225 | if (! empty($this->alignments)) { 226 | return new WatermarkFilter($path, $this->alignments); 227 | } 228 | 229 | $coordinates = ['position' => 'relative']; 230 | 231 | foreach (['top', 'right', 'bottom', 'left'] as $attribute) { 232 | if (is_null($this->$attribute)) { 233 | continue; 234 | } 235 | 236 | $coordinates[$attribute] = $this->$attribute; 237 | } 238 | 239 | return new WatermarkFilter($path, $coordinates); 240 | } 241 | 242 | /** 243 | * Returns an instance of Image. 244 | */ 245 | private function image(): Image 246 | { 247 | if (! $this->image) { 248 | $this->image = Image::load($this->media->getLocalPath()); 249 | } 250 | 251 | return $this->image; 252 | } 253 | 254 | /** 255 | * Forwards calls to the Image manipulation class. 256 | */ 257 | public function __call($method, $arguments) 258 | { 259 | $this->forwardCallTo($this->image(), $method, $arguments); 260 | 261 | return $this; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/Filters/WatermarkFilter.php: -------------------------------------------------------------------------------- 1 | path = $watermarkPath; 16 | } 17 | 18 | /** 19 | * Gets the commands from the base filter and normalizes the path. 20 | * 21 | * @return array 22 | */ 23 | protected function getCommands() 24 | { 25 | $commands = parent::getCommands(); 26 | 27 | $commands[1] = str_replace($this->path, static::normalizePath($this->path), $commands[1]); 28 | 29 | return $commands; 30 | } 31 | 32 | /** 33 | * Normalizes the path when running on Windows. 34 | */ 35 | public static function normalizePath(string $path): string 36 | { 37 | $path = windows_os() ? static::normalizeWindowsPath($path) : $path; 38 | 39 | return "'{$path}'"; 40 | } 41 | 42 | /** 43 | * Replaces the slashes and escapes the colon. For some 44 | * reason, this filter doesn't work on Windows with 45 | * absolute paths that contain forward slashes. 46 | */ 47 | public static function normalizeWindowsPath(string $path): string 48 | { 49 | $path = str_replace('/', '\\', $path); 50 | $path = str_replace('\\', '\\\\', $path); 51 | $path = str_replace(':', '\\:', $path); 52 | 53 | return $path; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Http/DynamicHLSPlaylist.php: -------------------------------------------------------------------------------- 1 | fromDisk($disk ?: config('filesystems.default')); 66 | } 67 | 68 | /** 69 | * Set the disk to open files from. 70 | */ 71 | public function fromDisk($disk): self 72 | { 73 | $this->disk = Disk::make($disk); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Instantiates a Media object for the given path and clears the cache. 80 | */ 81 | public function open(string $path): self 82 | { 83 | $this->media = Media::make($this->disk, $path); 84 | 85 | $this->keyCache = []; 86 | $this->playlistCache = []; 87 | $this->mediaCache = []; 88 | 89 | return $this; 90 | } 91 | 92 | public function setMediaUrlResolver(callable $mediaResolver): self 93 | { 94 | $this->mediaResolver = $mediaResolver; 95 | 96 | return $this; 97 | } 98 | 99 | public function setPlaylistUrlResolver(callable $playlistResolver): self 100 | { 101 | $this->playlistResolver = $playlistResolver; 102 | 103 | return $this; 104 | } 105 | 106 | public function setKeyUrlResolver(callable $keyResolver): self 107 | { 108 | $this->keyResolver = $keyResolver; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Returns the resolved key filename from the cache or resolves it. 115 | */ 116 | private function resolveKeyFilename(string $key): string 117 | { 118 | if (array_key_exists($key, $this->keyCache)) { 119 | return $this->keyCache[$key]; 120 | } 121 | 122 | return $this->keyCache[$key] = call_user_func($this->keyResolver, $key); 123 | } 124 | 125 | /** 126 | * Returns the resolved media filename from the cache or resolves it. 127 | * 128 | * @param string $key 129 | */ 130 | private function resolveMediaFilename(string $media): string 131 | { 132 | if (array_key_exists($media, $this->mediaCache)) { 133 | return $this->mediaCache[$media]; 134 | } 135 | 136 | return $this->mediaCache[$media] = call_user_func($this->mediaResolver, $media); 137 | } 138 | 139 | /** 140 | * Returns the resolved playlist filename from the cache or resolves it. 141 | * 142 | * @param string $key 143 | */ 144 | private function resolvePlaylistFilename(string $playlist): string 145 | { 146 | if (array_key_exists($playlist, $this->playlistCache)) { 147 | return $this->playlistCache[$playlist]; 148 | } 149 | 150 | return $this->playlistCache[$playlist] = call_user_func($this->playlistResolver, $playlist); 151 | } 152 | 153 | /** 154 | * Parses the lines into a Collection 155 | */ 156 | public static function parseLines(string $lines): Collection 157 | { 158 | return Collection::make(preg_split('/\n|\r\n?/', $lines)); 159 | } 160 | 161 | /** 162 | * Returns a boolean wether the line contains a .M3U8 playlist filename 163 | * or a .TS segment filename. 164 | */ 165 | private static function lineHasMediaFilename(string $line): bool 166 | { 167 | return ! Str::startsWith($line, '#') && Str::endsWith($line, ['.m3u8', '.ts']); 168 | } 169 | 170 | /** 171 | * Returns the filename of the encryption key. 172 | */ 173 | private static function extractKeyFromExtLine(string $line): ?string 174 | { 175 | preg_match_all('/#EXT-X-KEY:METHOD=AES-128,URI="([a-zA-Z0-9-_\/:]+.key)",IV=[a-z0-9]+/', $line, $matches); 176 | 177 | return $matches[1][0] ?? null; 178 | } 179 | 180 | /** 181 | * Returns the processed content of the playlist. 182 | */ 183 | public function get(): string 184 | { 185 | return $this->getProcessedPlaylist($this->media->getPath()); 186 | } 187 | 188 | /** 189 | * Returns a collection of all processed segment playlists 190 | * and the processed main playlist. 191 | */ 192 | public function all(): Collection 193 | { 194 | return static::parseLines( 195 | $this->disk->get($this->media->getPath()) 196 | )->filter(function ($line) { 197 | return static::lineHasMediaFilename($line); 198 | })->mapWithKeys(function ($segmentPlaylist) { 199 | return [$segmentPlaylist => $this->getProcessedPlaylist($segmentPlaylist)]; 200 | })->prepend( 201 | $this->getProcessedPlaylist($this->media->getPath()), 202 | $this->media->getPath() 203 | ); 204 | } 205 | 206 | /** 207 | * Processes the given playlist. 208 | */ 209 | public function getProcessedPlaylist(string $playlistPath): string 210 | { 211 | return static::parseLines($this->disk->get($playlistPath))->map(function (string $line) { 212 | if (static::lineHasMediaFilename($line)) { 213 | return Str::endsWith($line, '.m3u8') 214 | ? $this->resolvePlaylistFilename($line) 215 | : $this->resolveMediaFilename($line); 216 | } 217 | 218 | $key = static::extractKeyFromExtLine($line); 219 | 220 | if (! $key) { 221 | return $line; 222 | } 223 | 224 | return str_replace( 225 | '#EXT-X-KEY:METHOD=AES-128,URI="'.$key.'"', 226 | '#EXT-X-KEY:METHOD=AES-128,URI="'.$this->resolveKeyFilename($key).'"', 227 | $line 228 | ); 229 | })->implode(PHP_EOL); 230 | } 231 | 232 | public function toResponse($request) 233 | { 234 | return Response::make($this->get(), 200, [ 235 | 'Content-Type' => 'application/vnd.apple.mpegurl', 236 | ]); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/MediaOpener.php: -------------------------------------------------------------------------------- 1 | fromDisk($disk ?: config('filesystems.default')); 59 | 60 | $this->driver = ($driver ?: app(PHPFFMpeg::class))->fresh(); 61 | 62 | $this->collection = $mediaCollection ?: new MediaCollection; 63 | } 64 | 65 | public function clone(): self 66 | { 67 | return new MediaOpener( 68 | $this->disk, 69 | $this->driver, 70 | $this->collection 71 | ); 72 | } 73 | 74 | /** 75 | * Set the disk to open files from. 76 | */ 77 | public function fromDisk($disk): self 78 | { 79 | $this->disk = Disk::make($disk); 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Alias for 'fromDisk', mostly for backwards compatibility. 86 | */ 87 | public function fromFilesystem(Filesystem $filesystem): self 88 | { 89 | return $this->fromDisk($filesystem); 90 | } 91 | 92 | private static function makeLocalDiskFromPath(string $path): Disk 93 | { 94 | $adapter = (new FilesystemManager(app()))->createLocalDriver([ 95 | 'root' => $path, 96 | ]); 97 | 98 | return Disk::make($adapter); 99 | } 100 | 101 | /** 102 | * Instantiates a Media object for each given path. 103 | */ 104 | public function open($paths): self 105 | { 106 | foreach (Arr::wrap($paths) as $path) { 107 | if ($path instanceof UploadedFile) { 108 | $disk = static::makeLocalDiskFromPath($path->getPath()); 109 | 110 | $media = Media::make($disk, $path->getFilename()); 111 | } else { 112 | $media = Media::make($this->disk, $path); 113 | } 114 | 115 | $this->collection->push($media); 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Instantiates a single Media object and sets the given options on the object. 123 | */ 124 | public function openWithInputOptions(string $path, array $options = []): self 125 | { 126 | $this->collection->push( 127 | Media::make($this->disk, $path)->setInputOptions($options) 128 | ); 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Instantiates a MediaOnNetwork object for each given url. 135 | */ 136 | public function openUrl($paths, array $headers = []): self 137 | { 138 | foreach (Arr::wrap($paths) as $path) { 139 | $this->collection->push(MediaOnNetwork::make($path, $headers)); 140 | } 141 | 142 | return $this; 143 | } 144 | 145 | public function get(): MediaCollection 146 | { 147 | return $this->collection; 148 | } 149 | 150 | public function getDriver(): PHPFFMpeg 151 | { 152 | return $this->driver->open($this->collection); 153 | } 154 | 155 | /** 156 | * Forces the driver to open the collection with the `openAdvanced` method. 157 | */ 158 | public function getAdvancedDriver(): PHPFFMpeg 159 | { 160 | return $this->driver->openAdvanced($this->collection); 161 | } 162 | 163 | /** 164 | * Shortcut to set the timecode by string. 165 | */ 166 | public function getFrameFromString(string $timecode): self 167 | { 168 | return $this->getFrameFromTimecode(TimeCode::fromString($timecode)); 169 | } 170 | 171 | /** 172 | * Shortcut to set the timecode by seconds. 173 | */ 174 | public function getFrameFromSeconds(float $seconds): self 175 | { 176 | return $this->getFrameFromTimecode(TimeCode::fromSeconds($seconds)); 177 | } 178 | 179 | public function getFrameFromTimecode(TimeCode $timecode): self 180 | { 181 | $this->timecode = $timecode; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Returns an instance of MediaExporter with the driver and timecode (if set). 188 | */ 189 | public function export(): MediaExporter 190 | { 191 | return tap(new MediaExporter($this->getDriver()), function (MediaExporter $mediaExporter) { 192 | if ($this->timecode) { 193 | $mediaExporter->frame($this->timecode); 194 | } 195 | }); 196 | } 197 | 198 | /** 199 | * Returns an instance of HLSExporter with the driver forced to AdvancedMedia. 200 | */ 201 | public function exportForHLS(): HLSExporter 202 | { 203 | return new HLSExporter($this->getAdvancedDriver()); 204 | } 205 | 206 | /** 207 | * Returns an instance of MediaExporter with a TileFilter and ImageFormat. 208 | */ 209 | public function exportTile(callable $withTileFactory): MediaExporter 210 | { 211 | return $this->export() 212 | ->addTileFilter($withTileFactory) 213 | ->inFormat(new ImageFormat); 214 | } 215 | 216 | public function exportFramesByAmount(int $amount, ?int $width = null, ?int $height = null, ?int $quality = null): MediaExporter 217 | { 218 | $interval = ($this->getDurationInSeconds() + 1) / $amount; 219 | 220 | return $this->exportFramesByInterval($interval, $width, $height, $quality); 221 | } 222 | 223 | public function exportFramesByInterval(float $interval, ?int $width = null, ?int $height = null, ?int $quality = null): MediaExporter 224 | { 225 | return $this->exportTile( 226 | fn (TileFactory $tileFactory) => $tileFactory 227 | ->interval($interval) 228 | ->grid(1, 1) 229 | ->scale($width, $height) 230 | ->quality($quality) 231 | ); 232 | } 233 | 234 | public function cleanupTemporaryFiles(): self 235 | { 236 | app(TemporaryDirectories::class)->deleteAll(); 237 | 238 | return $this; 239 | } 240 | 241 | public function each($items, callable $callback): self 242 | { 243 | Collection::make($items)->each(function ($item, $key) use ($callback) { 244 | return $callback($this->clone(), $item, $key); 245 | }); 246 | 247 | return $this; 248 | } 249 | 250 | /** 251 | * Returns the Media object from the driver. 252 | */ 253 | public function __invoke(): AbstractMediaType 254 | { 255 | return $this->getDriver()->get(); 256 | } 257 | 258 | /** 259 | * Forwards all calls to the underlying driver. 260 | * 261 | * @return void 262 | */ 263 | public function __call($method, $arguments) 264 | { 265 | $result = $this->forwardCallTo($driver = $this->getDriver(), $method, $arguments); 266 | 267 | return ($result === $driver) ? $this : $result; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Support/FFMpeg.php: -------------------------------------------------------------------------------- 1 | defaultDisk = $defaultDisk; 23 | $this->driver = $driver; 24 | $this->driverResolver = $driverResolver; 25 | } 26 | 27 | private function driver(): PHPFFMpeg 28 | { 29 | if ($this->driver) { 30 | return $this->driver; 31 | } 32 | 33 | $resolver = $this->driverResolver; 34 | 35 | return $this->driver = $resolver(); 36 | } 37 | 38 | public function new(): MediaOpener 39 | { 40 | return new MediaOpener($this->defaultDisk, $this->driver()); 41 | } 42 | 43 | public function dynamicHLSPlaylist(): DynamicHLSPlaylist 44 | { 45 | return new DynamicHLSPlaylist($this->defaultDisk); 46 | } 47 | 48 | /** 49 | * Handle dynamic method calls into the MediaOpener. 50 | * 51 | * @param string $method 52 | * @param array $parameters 53 | * @return mixed 54 | */ 55 | public function __call($method, $parameters) 56 | { 57 | return $this->forwardCallTo($this->new(), $method, $parameters); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/ProcessOutput.php: -------------------------------------------------------------------------------- 1 | all = $all; 16 | $this->errors = $errors; 17 | $this->out = $out; 18 | } 19 | 20 | public function all(): array 21 | { 22 | return $this->all; 23 | } 24 | 25 | public function errors(): array 26 | { 27 | return $this->errors; 28 | } 29 | 30 | public function out(): array 31 | { 32 | return $this->out; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Support/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 20 | $this->publishes([ 21 | __DIR__.'/../../config/config.php' => config_path('laravel-ffmpeg.php'), 22 | ], 'config'); 23 | } 24 | } 25 | 26 | /** 27 | * Register the application services. 28 | */ 29 | public function register() 30 | { 31 | // Automatically apply the package configuration 32 | $this->mergeConfigFrom(__DIR__.'/../../config/config.php', 'laravel-ffmpeg'); 33 | 34 | $this->app->singleton('laravel-ffmpeg-logger', function () { 35 | $logChannel = $this->app['config']->get('laravel-ffmpeg.log_channel'); 36 | 37 | if ($logChannel === false) { 38 | return null; 39 | } 40 | 41 | return $this->app['log']->channel($logChannel ?: $this->app['config']->get('logging.default')); 42 | }); 43 | 44 | $this->app->singleton('laravel-ffmpeg-configuration', function () { 45 | $config = $this->app['config']; 46 | 47 | $baseConfig = [ 48 | 'ffmpeg.binaries' => $config->get('laravel-ffmpeg.ffmpeg.binaries'), 49 | 'ffprobe.binaries' => $config->get('laravel-ffmpeg.ffprobe.binaries'), 50 | 'timeout' => $config->get('laravel-ffmpeg.timeout'), 51 | ]; 52 | 53 | $configuredThreads = $config->get('laravel-ffmpeg.ffmpeg.threads', 12); 54 | 55 | if ($configuredThreads !== false) { 56 | $baseConfig['ffmpeg.threads'] = $configuredThreads; 57 | } 58 | 59 | if ($configuredTemporaryRoot = $config->get('laravel-ffmpeg.temporary_files_root')) { 60 | $baseConfig['temporary_directory'] = $configuredTemporaryRoot; 61 | } 62 | 63 | return $baseConfig; 64 | }); 65 | 66 | $this->app->singleton(FFProbe::class, function () { 67 | return FFProbe::create( 68 | $this->app->make('laravel-ffmpeg-configuration'), 69 | $this->app->make('laravel-ffmpeg-logger') 70 | ); 71 | }); 72 | 73 | $this->app->singleton(FFMpegDriver::class, function () { 74 | return FFMpegDriver::create( 75 | $this->app->make('laravel-ffmpeg-logger'), 76 | $this->app->make('laravel-ffmpeg-configuration') 77 | ); 78 | }); 79 | 80 | $this->app->singleton(FFMpeg::class, function () { 81 | return new FFMpeg( 82 | $this->app->make(FFMpegDriver::class), 83 | $this->app->make(FFProbe::class) 84 | ); 85 | }); 86 | 87 | $this->app->singleton(PHPFFMpeg::class, function () { 88 | return new PHPFFMpeg($this->app->make(FFMpeg::class)); 89 | }); 90 | 91 | $this->app->singleton(TemporaryDirectories::class, function () { 92 | return new TemporaryDirectories( 93 | $this->app['config']->get('laravel-ffmpeg.temporary_files_root', sys_get_temp_dir()), 94 | ); 95 | }); 96 | 97 | // Register the main class to use with the facade 98 | $this->app->singleton('laravel-ffmpeg', function () { 99 | return new MediaOpenerFactory( 100 | $this->app['config']->get('filesystems.default'), 101 | null, 102 | fn () => $this->app->make(PHPFFMpeg::class) 103 | ); 104 | }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Support/StreamParser.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 15 | } 16 | 17 | public static function new(Stream $stream): StreamParser 18 | { 19 | return new static($stream); 20 | } 21 | 22 | public function getFrameRate(): ?string 23 | { 24 | $frameRate = trim(optional($this->stream)->get('avg_frame_rate')); 25 | 26 | if (! $frameRate || Str::endsWith($frameRate, '/0')) { 27 | return null; 28 | } 29 | 30 | if (Str::contains($frameRate, '/')) { 31 | [$numerator, $denominator] = explode('/', $frameRate); 32 | 33 | $frameRate = $numerator / $denominator; 34 | } 35 | 36 | return $frameRate ? number_format($frameRate, 3, '.', '') : null; 37 | } 38 | } 39 | --------------------------------------------------------------------------------