mirror of
https://github.com/sigmasternchen/MyTube
synced 2025-03-15 04:48:55 +00:00
transcoding is now handled in php; added video duration
This commit is contained in:
parent
78b3b6be2b
commit
07f9827ed2
9 changed files with 686 additions and 180 deletions
|
@ -13,6 +13,7 @@
|
|||
"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",
|
||||
|
|
231
composer.lock
generated
231
composer.lock
generated
|
@ -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",
|
||||
|
@ -2327,6 +2389,49 @@
|
|||
],
|
||||
"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": "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",
|
||||
|
@ -2944,6 +3049,130 @@
|
|||
],
|
||||
"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": "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",
|
||||
|
|
83
migrations/Version20210107174149.php
Normal file
83
migrations/Version20210107174149.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20210107174149 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() 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, 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)');
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
163
src/Service/TranscodingService.php
Normal file
163
src/Service/TranscodingService.php
Normal file
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Controller\WatchController;
|
||||
use App\Entity\Video;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Coordinate\TimeCode;
|
||||
use FFMpeg\FFMpeg;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Filters\Video\ResizeFilter;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class TranscodingService
|
||||
{
|
||||
private const QUALITY = [
|
||||
[
|
||||
"height" => 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
12
symfony.lock
12
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"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue