From 07f9827ed2b2f17644f96f5e46786caa56ed1a6e Mon Sep 17 00:00:00 2001 From: overflowerror Date: Thu, 7 Jan 2021 18:53:55 +0100 Subject: [PATCH] transcoding is now handled in php; added video duration --- composer.json | 21 +- composer.lock | 465 ++++++++++++++++++++------- migrations/Version20210107174149.php | 83 +++++ src/Command/TranscodeCommand.php | 43 +-- src/Controller/WatchController.php | 13 +- src/Entity/Video.php | 55 +++- src/Service/TranscodingService.php | 163 ++++++++++ src/Service/VideoService.php | 11 +- symfony.lock | 12 + 9 files changed, 686 insertions(+), 180 deletions(-) create mode 100644 migrations/Version20210107174149.php create mode 100644 src/Service/TranscodingService.php diff --git a/composer.json b/composer.json index 6a8ce03..b9fac7d 100644 --- a/composer.json +++ b/composer.json @@ -13,21 +13,22 @@ "doctrine/doctrine-bundle": "^2.2", "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^2.8", + "php-ffmpeg/php-ffmpeg": "^0.17.0", "phpdocumentor/reflection-docblock": "^5.2", "ramsey/uuid-doctrine": "^1.6", "sensio/framework-extra-bundle": "^5.6", "symfony/asset": "5.2.*", "symfony/console": "5.2.*", - "symfony/dotenv": "5.2.*", - "symfony/expression-language": "5.2.*", - "symfony/flex": "^1.3.1", - "symfony/form": "5.2.*", - "symfony/framework-bundle": "5.2.*", - "symfony/http-client": "5.2.*", - "symfony/intl": "5.2.*", - "symfony/mailer": "5.2.*", - "symfony/mime": "5.2.*", - "symfony/monolog-bundle": "^3.1", + "symfony/dotenv": "5.2.*", + "symfony/expression-language": "5.2.*", + "symfony/flex": "^1.3.1", + "symfony/form": "5.2.*", + "symfony/framework-bundle": "5.2.*", + "symfony/http-client": "5.2.*", + "symfony/intl": "5.2.*", + "symfony/mailer": "5.2.*", + "symfony/mime": "5.2.*", + "symfony/monolog-bundle": "^3.1", "symfony/notifier": "5.2.*", "symfony/process": "5.2.*", "symfony/property-access": "5.2.*", diff --git a/composer.lock b/composer.lock index f648a5c..7fc3afc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,70 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7a63ec16e3e2daa0fbf514d4aedd88d5", + "content-hash": "a08b90e42235f47d768d0ce3aba9a85e", "packages": [ + { + "name": "alchemy/binary-driver", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/alchemy-fr/BinaryDriver.git", + "reference": "e0615cdff315e6b4b05ada67906df6262a020d22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/alchemy-fr/BinaryDriver/zipball/e0615cdff315e6b4b05ada67906df6262a020d22", + "reference": "e0615cdff315e6b4b05ada67906df6262a020d22", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0|^2.0|^1.0", + "php": ">=5.5", + "psr/log": "^1.0", + "symfony/process": "^2.3|^3.0|^4.0|^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0|^5.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Alchemy": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Le Goff", + "email": "legoff.n@gmail.com" + }, + { + "name": "Romain Neutron", + "email": "imprec@gmail.com", + "homepage": "http://www.lickmychip.com/" + }, + { + "name": "Phraseanet Team", + "email": "info@alchemy.fr", + "homepage": "http://www.phraseanet.com/" + }, + { + "name": "Jens Hausdorf", + "email": "mail@jens-hausdorf.de", + "homepage": "https://jens-hausdorf.de", + "role": "Maintainer" + } + ], + "description": "A set of tools to build binary drivers", + "keywords": [ + "binary", + "driver" + ], + "time": "2020-02-12T19:35:11+00:00" + }, { "name": "amphp/amp", "version": "v2.5.1", @@ -853,16 +915,16 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/brick/math/zipball/283a40c901101e66de7061bd359252c013dcc43c", - "reference": "283a40c901101e66de7061bd359252c013dcc43c", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.1|^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15|^8.5", + "reference": "283a40c901101e66de7061bd359252c013dcc43c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15|^8.5", "vimeo/psalm": "^3.5" }, "type": "library", @@ -948,14 +1010,14 @@ ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" @@ -1014,16 +1076,16 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", - "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/cache": "1.*", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/cache": "1.*", "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", "phpunit/phpunit": "^7.5 || ^9.1.5" @@ -2319,25 +2381,68 @@ "validation", "validator" ], - "funding": [ - { - "url": "https://github.com/egulias", - "type": "github" - } - ], - "time": "2020-12-29T14:50:06+00:00" + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2020-12-29T14:50:06+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "name": "friendsofphp/proxy-manager-lts", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", - "reference": "4a66e4e0d3279d3bb3722963b4294331fabe15bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/4a66e4e0d3279d3bb3722963b4294331fabe15bc", + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "time": "2017-07-23T21:35:13+00:00" + }, + { + "name": "friendsofphp/proxy-manager-lts", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", + "reference": "4a66e4e0d3279d3bb3722963b4294331fabe15bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/4a66e4e0d3279d3bb3722963b4294331fabe15bc", "reference": "4a66e4e0d3279d3bb3722963b4294331fabe15bc", "shasum": "" }, @@ -2394,17 +2499,17 @@ "service proxies" ], "funding": [ - { - "url": "https://github.com/Ocramius", - "type": "github" - }, + { + "url": "https://github.com/Ocramius", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", "type": "tidelift" } ], - "time": "2021-01-04T11:21:26+00:00" - }, + "time": "2021-01-04T11:21:26+00:00" + }, { "name": "kelunik/certificate", "version": "v1.1.2", @@ -2465,16 +2570,16 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/laminas/laminas-code/zipball/28a6d70ea8b8bca687d7163300e611ae33baf82a", - "reference": "28a6d70ea8b8bca687d7163300e611ae33baf82a", - "shasum": "" - }, - "require": { - "laminas/laminas-eventmanager": "^3.3", - "php": "^7.4 || ~8.0.0" - }, - "conflict": { - "phpspec/prophecy": "<1.9.0" - }, + "reference": "28a6d70ea8b8bca687d7163300e611ae33baf82a", + "shasum": "" + }, + "require": { + "laminas/laminas-eventmanager": "^3.3", + "php": "^7.4 || ~8.0.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, "replace": { "zendframework/zend-code": "self.version" }, @@ -2617,16 +2722,16 @@ } }, "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Alias legacy ZF class names to Laminas Project equivalents.", - "keywords": [ - "ZendFramework", - "autoloading", - "laminas", - "zf" - ], + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], "funding": [ { "url": "https://funding.communitybridge.org/projects/laminas-project", @@ -2863,16 +2968,16 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", - "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/log": "^1.0.1" - }, - "provide": { - "psr/log-implementation": "1.0.0" - }, + "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", @@ -2935,26 +3040,150 @@ "funding": [ { "url": "https://github.com/Seldaek", - "type": "github" + "type": "github" }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } ], - "time": "2020-12-14T13:15:25+00:00" + "time": "2020-12-14T13:15:25+00:00" + }, + { + "name": "neutron/temporary-filesystem", + "version": "3.0", + "source": { + "type": "git", + "url": "https://github.com/romainneutron/Temporary-Filesystem.git", + "reference": "60e79adfd16f42f4b888e351ad49f9dcb959e3c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/romainneutron/Temporary-Filesystem/zipball/60e79adfd16f42f4b888e351ad49f9dcb959e3c2", + "reference": "60e79adfd16f42f4b888e351ad49f9dcb959e3c2", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/filesystem": "^2.3 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.0.4" + }, + "type": "library", + "autoload": { + "psr-0": { + "Neutron": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Neutron", + "email": "imprec@gmail.com" + } + ], + "description": "Symfony filesystem extension to handle temporary files", + "time": "2020-07-27T14:00:33+00:00" + }, + { + "name": "php-ffmpeg/php-ffmpeg", + "version": "v0.17.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git", + "reference": "a5147d1ae041e78e7870bf2443d4e2dfa7635856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/a5147d1ae041e78e7870bf2443d4e2dfa7635856", + "reference": "a5147d1ae041e78e7870bf2443d4e2dfa7635856", + "shasum": "" + }, + "require": { + "alchemy/binary-driver": "^1.5 || ~2.0.0 || ^5.0", + "doctrine/cache": "^1.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "neutron/temporary-filesystem": "^2.1.1 || ^3.0", + "php": ">=5.3.9" + }, + "require-dev": { + "silex/silex": "~1.0", + "symfony/phpunit-bridge": "^5.0.4", + "symfony/process": "2.8 || 3.3" + }, + "suggest": { + "php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.x-dev" + } + }, + "autoload": { + "psr-0": { + "FFMpeg": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Romain Neutron", + "email": "imprec@gmail.com", + "homepage": "http://www.lickmychip.com/" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "name": "Phraseanet Team", + "email": "info@alchemy.fr", + "homepage": "http://www.phraseanet.com/" + }, + { + "name": "Patrik Karisch", + "email": "patrik@karisch.guru", + "homepage": "http://www.karisch.guru" + }, + { + "name": "Romain Biard", + "email": "romain.biard@gmail.com", + "homepage": "https://www.strime.io/" + }, + { + "name": "Jens Hausdorf", + "email": "hello@jens-hausdorf.de", + "homepage": "https://jens-hausdorf.de" + } + ], + "description": "FFMpeg PHP, an Object Oriented library to communicate with AVconv / ffmpeg", + "keywords": [ + "audio", + "audio processing", + "avconv", + "avprobe", + "ffmpeg", + "ffprobe", + "video", + "video processing" + ], + "time": "2020-12-18T14:31:34+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, @@ -3213,16 +3442,16 @@ "Psr\\EventDispatcher\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], "description": "Standard interfaces for event handling.", "keywords": [ "events", @@ -3292,16 +3521,16 @@ "dist": { "type": "zip", "url": "https://api.github.com/repos/php-fig/link/zipball/eea8e8662d5cd3ae4517c9b864493f59fca95562", - "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" + "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" } }, "autoload": { diff --git a/migrations/Version20210107174149.php b/migrations/Version20210107174149.php new file mode 100644 index 0000000..e70f2bc --- /dev/null +++ b/migrations/Version20210107174149.php @@ -0,0 +1,83 @@ +addSql('DROP INDEX UNIQ_8D93D6495E237E06'); + $this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, password, name, roles FROM user'); + $this->addSql('DROP TABLE user'); + $this->addSql('CREATE TABLE user (id BLOB NOT NULL, password VARCHAR(255) NOT NULL COLLATE BINARY, name VARCHAR(180) NOT NULL COLLATE BINARY, roles CLOB NOT NULL COLLATE BINARY --(DC2Type:json) + , PRIMARY KEY(id))'); + $this->addSql('INSERT INTO user (id, password, name, roles) SELECT id, password, name, roles FROM __temp__user'); + $this->addSql('DROP TABLE __temp__user'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D6495E237E06 ON user (name)'); + $this->addSql('DROP INDEX IDX_7CC7DA2C16678C77'); + $this->addSql('CREATE TEMPORARY TABLE __temp__video AS SELECT id, uploader_id, uploaded, name, description, tags, state FROM video'); + $this->addSql('DROP TABLE video'); + $this->addSql('CREATE TABLE video (id BLOB NOT NULL, uploader_id BLOB NOT NULL, uploaded DATETIME NOT NULL --(DC2Type:datetime_immutable) + , name VARCHAR(255) NOT NULL COLLATE BINARY, description VARCHAR(1024) NOT NULL COLLATE BINARY, tags CLOB NOT NULL COLLATE BINARY --(DC2Type:array) + , state INTEGER NOT NULL, length DOUBLE PRECISION DEFAULT NULL, transcoding_progress INTEGER DEFAULT NULL, PRIMARY KEY(id), CONSTRAINT FK_7CC7DA2C16678C77 FOREIGN KEY (uploader_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO video (id, uploader_id, uploaded, name, description, tags, state) SELECT id, uploader_id, uploaded, name, description, tags, state FROM __temp__video'); + $this->addSql('DROP TABLE __temp__video'); + $this->addSql('CREATE INDEX IDX_7CC7DA2C16678C77 ON video (uploader_id)'); + $this->addSql('DROP INDEX IDX_313BC42D29C1004E'); + $this->addSql('DROP INDEX IDX_313BC42D61220EA6'); + $this->addSql('CREATE TEMPORARY TABLE __temp__video_link AS SELECT id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment FROM video_link'); + $this->addSql('DROP TABLE video_link'); + $this->addSql('CREATE TABLE video_link (id BLOB NOT NULL, video_id BLOB NOT NULL, creator_id BLOB NOT NULL, created DATETIME NOT NULL --(DC2Type:datetime_immutable) + , max_views INTEGER DEFAULT NULL, viewable_for INTEGER DEFAULT NULL, viewable_until DATETIME DEFAULT NULL, comment VARCHAR(1024) DEFAULT NULL COLLATE BINARY, PRIMARY KEY(id), CONSTRAINT FK_313BC42D29C1004E FOREIGN KEY (video_id) REFERENCES video (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_313BC42D61220EA6 FOREIGN KEY (creator_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO video_link (id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment) SELECT id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment FROM __temp__video_link'); + $this->addSql('DROP TABLE __temp__video_link'); + $this->addSql('CREATE INDEX IDX_313BC42D29C1004E ON video_link (video_id)'); + $this->addSql('CREATE INDEX IDX_313BC42D61220EA6 ON video_link (creator_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX UNIQ_8D93D6495E237E06'); + $this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, name, roles, password FROM user'); + $this->addSql('DROP TABLE user'); + $this->addSql('CREATE TABLE user (id BLOB NOT NULL, name VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) + , password VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO user (id, name, roles, password) SELECT id, name, roles, password FROM __temp__user'); + $this->addSql('DROP TABLE __temp__user'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D6495E237E06 ON user (name)'); + $this->addSql('DROP INDEX IDX_7CC7DA2C16678C77'); + $this->addSql('CREATE TEMPORARY TABLE __temp__video AS SELECT id, uploader_id, uploaded, name, description, tags, state FROM video'); + $this->addSql('DROP TABLE video'); + $this->addSql('CREATE TABLE video (id BLOB NOT NULL, uploaded DATETIME NOT NULL --(DC2Type:datetime_immutable) + , name VARCHAR(255) NOT NULL, description VARCHAR(1024) NOT NULL, tags CLOB NOT NULL --(DC2Type:array) + , state INTEGER NOT NULL, uploader_id BLOB NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO video (id, uploader_id, uploaded, name, description, tags, state) SELECT id, uploader_id, uploaded, name, description, tags, state FROM __temp__video'); + $this->addSql('DROP TABLE __temp__video'); + $this->addSql('CREATE INDEX IDX_7CC7DA2C16678C77 ON video (uploader_id)'); + $this->addSql('DROP INDEX IDX_313BC42D29C1004E'); + $this->addSql('DROP INDEX IDX_313BC42D61220EA6'); + $this->addSql('CREATE TEMPORARY TABLE __temp__video_link AS SELECT id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment FROM video_link'); + $this->addSql('DROP TABLE video_link'); + $this->addSql('CREATE TABLE video_link (id BLOB NOT NULL, created DATETIME NOT NULL --(DC2Type:datetime_immutable) + , max_views INTEGER DEFAULT NULL, viewable_for INTEGER DEFAULT NULL, viewable_until DATETIME DEFAULT NULL, comment VARCHAR(1024) DEFAULT NULL, video_id BLOB NOT NULL, creator_id BLOB NOT NULL, PRIMARY KEY(id))'); + $this->addSql('INSERT INTO video_link (id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment) SELECT id, video_id, creator_id, created, max_views, viewable_for, viewable_until, comment FROM __temp__video_link'); + $this->addSql('DROP TABLE __temp__video_link'); + $this->addSql('CREATE INDEX IDX_313BC42D29C1004E ON video_link (video_id)'); + $this->addSql('CREATE INDEX IDX_313BC42D61220EA6 ON video_link (creator_id)'); + } +} diff --git a/src/Command/TranscodeCommand.php b/src/Command/TranscodeCommand.php index 23dfa56..e691a94 100644 --- a/src/Command/TranscodeCommand.php +++ b/src/Command/TranscodeCommand.php @@ -2,7 +2,7 @@ namespace App\Command; -use App\Entity\Video; +use App\Service\TranscodingService; use App\Service\VideoService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -14,12 +14,17 @@ class TranscodeCommand extends Command protected static $defaultName = "app:start-transcode"; private $videoService; + private $transcodingService; - public function __construct(VideoService $videoService, string $name = null) + public function __construct( + VideoService $videoService, + TranscodingService $transcodingService, + string $name = null + ) { parent::__construct($name); $this->videoService = $videoService; - + $this->transcodingService = $transcodingService; } protected function configure() @@ -40,32 +45,6 @@ class TranscodeCommand extends Command return $process->isSuccessful(); } - private function handleVideo(Video $video, OutputInterface $output) - { - $output->writeln("starting creation of thumbnail..."); - $this->videoService->setVideoState($video, Video::PROCESSING_THUMBNAIL); - if ($this->callScript("thumbnail.sh", [$video->getId()->toString()])) { - $output->writeln("thumbnail creation successful"); - } else { - $output->writeln("thumbnail creation failed"); - $this->videoService->setVideoState($video, Video::FAIL); - return; - } - - - $output->writeln("starting transcoding..."); - $this->videoService->setVideoState($video, Video::PROCESSING_TRANSCODE); - if ($this->callScript("transcode.sh", [$video->getId()->toString()])) { - $output->writeln("transcoding successful"); - } else { - $output->writeln("transcoding failed"); - $this->videoService->setVideoState($video, Video::FAIL); - return; - } - - $this->videoService->setVideoState($video, Video::DONE); - } - protected function execute(InputInterface $input, OutputInterface $output): int { while (true) { @@ -73,11 +52,11 @@ class TranscodeCommand extends Command $videos = $this->videoService->getVideosForTranscode(); foreach ($videos as $video) { - $output->writeln("New video: " . $video->getName() . ", " . $video->getUploader()->getName()); + $output->writeln("new: " . $video->getName() . ", " . $video->getUploader()->getName()); - $this->handleVideo($video, $output); + $this->transcodingService->doTranscode($video, $output); - $output->writeln("Done"); + $output->writeln("done"); } } } diff --git a/src/Controller/WatchController.php b/src/Controller/WatchController.php index 706ccae..8585f9a 100644 --- a/src/Controller/WatchController.php +++ b/src/Controller/WatchController.php @@ -22,13 +22,14 @@ class WatchController extends AbstractController private const IS_OWNER = 2; public const OWNER_LINK_ID = "owner"; - public const CONTENT_DIRECTORY = "../content/"; + public const CONTENT_RELATIVE = "../"; + public const CONTENT_DIRECTORY = "content/"; private const PLAYLIST_MIME_TYPE = "application/x-mpegURL"; private const TS_FILE_MIME_TYPE = "video/MP2T"; private const THUMBNAIL_MIME_TYPE = "image/png"; - private const TS_FILE_FORMAT = "seg-%06d-ts"; + public const TS_FILE_FORMAT = "seg-%06d-ts"; private $userService; private $videoService; @@ -97,7 +98,7 @@ class WatchController extends AbstractController { $data = $this->checkRequestData($videoId, $linkId); - $file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . "playlist.m3u8"; + $file = self::CONTENT_RELATIVE . self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . "playlist.m3u8"; $response = new BinaryFileResponse($file); $response->headers->set("Content-Type", self::PLAYLIST_MIME_TYPE); @@ -112,7 +113,7 @@ class WatchController extends AbstractController { $data = $this->checkRequestData($videoId, $linkId); - $file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . $quality . "p/" . "playlist.m3u8"; + $file = self::CONTENT_RELATIVE . self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . $quality . "p/" . "playlist.m3u8"; $response = new BinaryFileResponse($file); $response->headers->set("Content-Type", self::PLAYLIST_MIME_TYPE); @@ -127,7 +128,7 @@ class WatchController extends AbstractController { $data = $this->checkRequestData($videoId, $linkId); - $file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . $quality . "p/" . sprintf(self::TS_FILE_FORMAT, $tsFileId); + $file = self::CONTENT_RELATIVE . self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . $quality . "p/" . sprintf(self::TS_FILE_FORMAT, $tsFileId); $response = new BinaryFileResponse($file); $response->headers->set("Content-Type", self::TS_FILE_MIME_TYPE); @@ -142,7 +143,7 @@ class WatchController extends AbstractController { $data = $this->checkRequestData($videoId, $linkId); - $file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . "thumb.png"; + $file = self::CONTENT_RELATIVE . self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . "thumb.png"; $response = new BinaryFileResponse($file); $response->headers->set("Content-Type", self::THUMBNAIL_MIME_TYPE); diff --git a/src/Entity/Video.php b/src/Entity/Video.php index 0a1498d..b575bac 100644 --- a/src/Entity/Video.php +++ b/src/Entity/Video.php @@ -15,10 +15,11 @@ use Ramsey\Uuid\UuidInterface; */ class Video { - public const WAITING = 1; - public const PROCESSING_THUMBNAIL = 2; - public const PROCESSING_TRANSCODE = 3; - public const DONE = 4; + public const QUEUED = 1; + public const PROCESSING_META = 2; + public const PROCESSING_THUMBNAIL = 3; + public const PROCESSING_TRANSCODE = 4; + public const DONE = 5; public const FAIL = -1; /** @@ -59,13 +60,23 @@ class Video /** * @ORM\Column(type="integer") */ - private $state = self::WAITING; + private $state = self::QUEUED; /** * @ORM\OneToMany(targetEntity=VideoLink::class, mappedBy="video") */ private $videoLinks; + /** + * @ORM\Column(type="float", nullable=true) + */ + private $length; + + /** + * @ORM\Column(type="integer", nullable=true) + */ + private $transcodingProgress; + public function __construct() { $this->videoLinks = new ArrayCollection(); @@ -149,12 +160,14 @@ class Video public function getStateString(): string { switch ($this->state) { - case self::WAITING: - return "waiting"; + case self::QUEUED: + return "queued"; + case self::PROCESSING_META: + return "processing..."; case self::PROCESSING_THUMBNAIL: - return "thumbnail"; + return "creating thumbnail..."; case self::PROCESSING_TRANSCODE: - return "transcoding"; + return "transcoding..."; case self::DONE: return "done"; case self::FAIL: @@ -204,4 +217,28 @@ class Video $this->customId = $customId; return $this; } + + public function getLength(): ?float + { + return $this->length; + } + + public function setLength(?float $length): self + { + $this->length = $length; + + return $this; + } + + public function getTranscodingProgress(): ?int + { + return $this->transcodingProgress; + } + + public function setTranscodingProgress(?int $transcodingProgress): self + { + $this->transcodingProgress = $transcodingProgress; + + return $this; + } } diff --git a/src/Service/TranscodingService.php b/src/Service/TranscodingService.php new file mode 100644 index 0000000..61e1e32 --- /dev/null +++ b/src/Service/TranscodingService.php @@ -0,0 +1,163 @@ + 1080, + "crf" => 23, + "playlistResolution" => "1920x1080", + "playlistBandwidth" => "800000", + ], + [ + "height" => 720, + "crf" => 23, + "playlistResolution" => "1280x720", + "playlistBandwidth" => "1400000", + ], + [ + "height" => 480, + "crf" => 23, + "playlistResolution" => "842x480", + "playlistBandwidth" => "2800000", + ], + [ + "height" => 360, + "crf" => 23, + "playlistResolution" => "640x360", + "playlistBandwidth" => "5000000", + ] + ]; + + private $ffmpeg; + private $ffprobe; + + private $videoService; + + public function __construct(VideoService $videoService) + { + $this->ffmpeg = FFMpeg::create(); + $this->ffprobe = FFProbe::create(); + + $this->videoService = $videoService; + } + + private function rawPath($id): string + { + return VideoService::LANDINGZONE_DIRECTORY . $id . VideoService::LANDINGZONE_EXTENTION; + } + + private function getLength($id): float + { + return $this->ffprobe->format($this->rawPath($id))->get("duration"); + } + + private function contentDir($id): string + { + return WatchController::CONTENT_DIRECTORY . $id . "/"; + } + + private function createDirectories($id) + { + $dir = $this->contentDir($id); + + mkdir($dir); + mkdir($dir . "360p"); + mkdir($dir . "480p"); + mkdir($dir . "720p"); + mkdir($dir . "1080p"); + } + + private function createThumbnail($id) + { + $video = $this->ffmpeg->open($this->rawPath($id)); + $video->filters()->custom("thumbnail,scale=640:360"); + $video->frame(TimeCode::fromSeconds(1))->save($this->contentDir($id) . "thumb.png"); + } + + private function transcode(Video $video) + { + $height = $this->ffprobe->streams($this->rawPath($video->getId()))->videos()->first()->getDimensions()->getHeight(); + + $countQuality = count(self::QUALITY); + $total = $countQuality; + $i = 0; + foreach (self::QUALITY as $quality) { + if ($quality["height"] > $height) { + $total--; + } + + $ffvideo = $this->ffmpeg->open($this->rawPath($video->getId())); + $ffvideo->filters()->resize(new Dimension(1, $quality["height"]), ResizeFilter::RESIZEMODE_SCALE_WIDTH)->synchronize(); + + $format = new X264("aac"); + $format->setAdditionalParameters([ + "-crf", $quality["crf"], + "-hls_segment_filename", $this->contentDir($video->getId()) . $quality["height"] . "p/" . WatchController::TS_FILE_FORMAT, + "-hls_playlist_type", "vod", + "-keyint_min", "48", + "-g", "48", + "-sc_threshold", "0", + ]); + $format->on('progress', function ($v, $f, $percentage) use ($i, $total, $video) { + $percentage = (($i) * 100.0 + $percentage) / ($total); + $video->setTranscodingProgress($percentage); + $this->videoService->update($video); + }); + + $ffvideo->save($format, $this->contentDir($video->getId()) . $quality["height"] . "p/playlist.m3u8"); + + $i++; + } + + $globalPlaylist = "#EXTM3U\n"; + $globalPlaylist .= "#EXT-X-VERSION:3\n"; + for ($i = $countQuality - $total; $i < $countQuality; $i++) { + $quality = self::QUALITY[$i]; + $globalPlaylist .= "#EXT-X-STREAM-INF:BANDWIDTH=" . $quality["playlistBandwidth"] . ",RESOLUTION=" . $quality["playlistResolution"] . "\n"; + $globalPlaylist .= $quality["height"] . "/playlist\n"; + } + + file_put_contents($this->contentDir($video->getId()) . "playlist.m3u8", $globalPlaylist); + } + + public function doTranscode(Video $video, OutputInterface $output) + { + $video->setTranscodingProgress(0); + + $output->writeln(" meta"); + $video->setState(Video::PROCESSING_META); + $this->videoService->update($video); + + $video->setLength($this->getLength($video->getId())); + $this->createDirectories($video->getId()); + + $output->writeln(" thumbnail"); + $video->setState(Video::PROCESSING_THUMBNAIL); + $this->videoService->update($video); + + $this->createThumbnail($video->getId()); + + $output->writeln(" transcode"); + $video->setState(Video::PROCESSING_TRANSCODE); + $this->videoService->update($video); + + $this->transcode($video); + + $video->setState(Video::DONE); + $this->videoService->update($video); + } +} \ No newline at end of file diff --git a/src/Service/VideoService.php b/src/Service/VideoService.php index 94279f3..795f5f4 100644 --- a/src/Service/VideoService.php +++ b/src/Service/VideoService.php @@ -11,7 +11,9 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; class VideoService { - private const LANDINGZONE_DIRECTORY = "../landingzone/"; + public const LANDINGZONE_RELATIVE = "../"; + public const LANDINGZONE_DIRECTORY = "landingzone/"; + public const LANDINGZONE_EXTENTION = ".vid"; private $videoRepository; private $userService; @@ -33,17 +35,16 @@ class VideoService $video->setUploader($this->userService->getLoggedInUser()); $this->videoRepository->save($video); - $file->move(self::LANDINGZONE_DIRECTORY, $video->getId()->toString() . ".vid"); + $file->move(self::LANDINGZONE_RELATIVE . self::LANDINGZONE_DIRECTORY, $video->getId()->toString() . self::LANDINGZONE_EXTENTION); } public function getVideosForTranscode(): array { - return $this->videoRepository->findByState(Video::WAITING); + return $this->videoRepository->findByState(Video::QUEUED); } - public function setVideoState(Video $video, $state) + public function update(Video $video) { - $video->setState($state); $this->videoRepository->update(); } diff --git a/symfony.lock b/symfony.lock index 7257f8b..c746033 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,7 @@ { + "alchemy/binary-driver": { + "version": "v5.2.0" + }, "amphp/amp": { "version": "v2.5.1" }, @@ -141,6 +144,9 @@ "egulias/email-validator": { "version": "2.1.25" }, + "evenement/evenement": { + "version": "v3.0.1" + }, "friendsofphp/proxy-manager-lts": { "version": "v1.0.2" }, @@ -168,12 +174,18 @@ "monolog/monolog": { "version": "2.2.0" }, + "neutron/temporary-filesystem": { + "version": "3.0" + }, "nikic/php-parser": { "version": "v4.10.4" }, "php": { "version": "7.4" }, + "php-ffmpeg/php-ffmpeg": { + "version": "v0.17.0" + }, "phpdocumentor/reflection-common": { "version": "2.2.0" },