playback and owner links work

This commit is contained in:
overflowerror 2021-01-06 15:46:55 +01:00
parent 53217b7d37
commit 4541164ad3
14 changed files with 2275 additions and 2 deletions

View file

@ -0,0 +1,67 @@
<?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 Version20210106134556 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('CREATE TABLE video_link (id BLOB NOT NULL, video_id BLOB NOT NULL, created DATETIME NOT NULL --(DC2Type:datetime_immutable)
, mode INTEGER NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_313BC42D29C1004E ON video_link (video_id)');
$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, 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)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE video_link');
$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)');
}
}

1727
public/css/video-js.css Normal file

File diff suppressed because one or more lines are too long

26
public/js/video.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@ namespace App\Controller;
use App\Entity\Video; use App\Entity\Video;
use App\Form\VideoType; use App\Form\VideoType;
use App\Mapper\CustomUuidMapper;
use App\Service\UserService; use App\Service\UserService;
use App\Service\VideoService; use App\Service\VideoService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -18,11 +19,13 @@ class HomeController extends AbstractController
{ {
private $userService; private $userService;
private $videoService; private $videoService;
private $uuidMapper;
public function __construct(UserService $userService, VideoService $videoService) public function __construct(UserService $userService, VideoService $videoService, CustomUuidMapper $uuidMapper)
{ {
$this->userService = $userService; $this->userService = $userService;
$this->videoService = $videoService; $this->videoService = $videoService;
$this->uuidMapper = $uuidMapper;
} }
/** /**
@ -38,6 +41,10 @@ class HomeController extends AbstractController
$user = $this->userService->getLoggedInUser(); $user = $this->userService->getLoggedInUser();
$videos = $this->videoService->getVideos($user); $videos = $this->videoService->getVideos($user);
foreach ($videos as $video) {
$video->setCustomId($this->uuidMapper->toString($video->getId()));
}
return $this->render("home/dashboard.html.twig", [ return $this->render("home/dashboard.html.twig", [
"videos" => $videos "videos" => $videos
]); ]);

View file

@ -0,0 +1,152 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Entity\Video;
use App\Mapper\CustomUuidMapper;
use App\Service\UserService;
use App\Service\VideoLinkService;
use App\Service\VideoService;
use Doctrine\DBAL\Types\ConversionException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
class WatchController extends AbstractController
{
private const NOT_ALLOWED = 0;
private const ALLOWED = 1;
private const IS_OWNER = 2;
public const OWNER_LINK_ID = "owner";
public const CONTENT_DIRECTORY = "../content/";
private const PLAYLIST_MIME_TYPE = "application/x-mpegURL";
private const TS_FILE_MIME_TYPE = "video/MP2T";
private const TS_FILE_FORMAT = "seg-%06d-ts";
private $userService;
private $videoService;
private $videoLinkService;
private $uuidMapper;
public function __construct(
UserService $userService,
VideoService $videoService,
VideoLinkService $videoLinkService,
CustomUuidMapper $uuidMapper
)
{
$this->userService = $userService;
$this->videoService = $videoService;
$this->videoLinkService = $videoLinkService;
$this->uuidMapper = $uuidMapper;
}
private function isAllowed(?Video $video, ?User $user, $linkId): int
{
if ($video->getUploader() == $user) {
return self::IS_OWNER;
}
$link = $this->videoLinkService->get($this->uuidMapper->fromString($linkId));
if (!$link) {
return self::NOT_ALLOWED;
}
if ($link->getVideo() != $video) {
return self::NOT_ALLOWED;
}
// TODO: check constraints
return self::ALLOWED;
}
private function checkRequestData($videoId, $linkId): array
{
try {
$video = $this->videoService->get($this->uuidMapper->fromString($videoId));
$user = $this->userService->getLoggedInUser();
$allowed = $this->isAllowed($video, $user, $linkId);
} catch (ConversionException $e) {
throw new AccessDeniedHttpException();
}
if (!$allowed) {
throw new AccessDeniedHttpException();
}
return [
"video" => $video,
"user" => $user,
"isOwner" => $allowed == self::IS_OWNER
];
}
/**
* @Route("/{linkId}/{videoId}/playlist", name="app_watch_global")
*/
public function globalPlaylist($videoId, $linkId): Response
{
$data = $this->checkRequestData($videoId, $linkId);
$file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . "playlist.m3u8";
$response = new BinaryFileResponse($file);
$response->headers->set("Content-Type", self::PLAYLIST_MIME_TYPE);
return $response;
}
/**
* @Route("/{linkId}/{videoId}/{quality}/playlist", name="app_watch_quality", requirements={"quality"="360|480|720|1080"})
*/
public function qualityPlaylist($videoId, $linkId, int $quality): Response
{
$data = $this->checkRequestData($videoId, $linkId);
$file = self::CONTENT_DIRECTORY . $data["video"]->getId() . "/" . $quality . "p/" . "playlist.m3u8";
$response = new BinaryFileResponse($file);
$response->headers->set("Content-Type", self::PLAYLIST_MIME_TYPE);
return $response;
}
/**
* @Route("/{linkId}/{videoId}/{quality}/seg-{tsFileId}-ts", name="app_watch_segment", requirements={"quality"="360|480|720|1080", "tsFileId"="\d+"})
*/
public function tsFiles($videoId, $linkId, int $quality, int $tsFileId): Response
{
$data = $this->checkRequestData($videoId, $linkId);
$file = 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);
return $response;
}
/**
* @Route("/{linkId}/{videoId}/", name="app_watch_page")
*/
public function watchPage($videoId, $linkId): Response
{
$data = $this->checkRequestData($videoId, $linkId);
return $this->render("watch/watch.html.twig", [
"thumbnail" => "thumbnail.jpg",
"global" => $this->generateUrl("app_watch_global", [
"linkId" => $linkId,
"videoId" => $videoId
])
]);
}
}

View file

@ -4,6 +4,8 @@ namespace App\Entity;
use App\Repository\VideoRepository; use App\Repository\VideoRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Doctrine\UuidGenerator; use Ramsey\Uuid\Doctrine\UuidGenerator;
use Ramsey\Uuid\UuidInterface; use Ramsey\Uuid\UuidInterface;
@ -17,6 +19,7 @@ class Video
public const PROCESSING_THUMBNAIL = 2; public const PROCESSING_THUMBNAIL = 2;
public const PROCESSING_TRANSCODE = 3; public const PROCESSING_TRANSCODE = 3;
public const DONE = 4; public const DONE = 4;
public const FAIL = -1;
/** /**
* @ORM\Id * @ORM\Id
@ -25,6 +28,7 @@ class Video
* @ORM\CustomIdGenerator(class=UuidGenerator::class) * @ORM\CustomIdGenerator(class=UuidGenerator::class)
*/ */
private $id; private $id;
private $customId;
/** /**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="videos") * @ORM\ManyToOne(targetEntity=User::class, inversedBy="videos")
@ -57,6 +61,16 @@ class Video
*/ */
private $state = self::WAITING; private $state = self::WAITING;
/**
* @ORM\OneToMany(targetEntity=VideoLink::class, mappedBy="video")
*/
private $videoLinks;
public function __construct()
{
$this->videoLinks = new ArrayCollection();
}
public function getId(): ?UuidInterface public function getId(): ?UuidInterface
{ {
return $this->id; return $this->id;
@ -131,4 +145,63 @@ class Video
{ {
return $this->state; return $this->state;
} }
public function getStateString(): string
{
switch ($this->state) {
case self::WAITING:
return "waiting";
case self::PROCESSING_THUMBNAIL:
return "thumbnail";
case self::PROCESSING_TRANSCODE:
return "transcoding";
case self::DONE:
return "done";
case self::FAIL:
return "fail";
default:
return "unknown";
}
}
/**
* @return Collection|VideoLink[]
*/
public function getVideoLinks(): Collection
{
return $this->videoLinks;
}
public function addVideoLink(VideoLink $videoLink): self
{
if (!$this->videoLinks->contains($videoLink)) {
$this->videoLinks[] = $videoLink;
$videoLink->setVideo($this);
}
return $this;
}
public function removeVideoLink(VideoLink $videoLink): self
{
if ($this->videoLinks->removeElement($videoLink)) {
// set the owning side to null (unless already changed)
if ($videoLink->getVideo() === $this) {
$videoLink->setVideo(null);
}
}
return $this;
}
public function getCustomId(): string
{
return $this->customId;
}
public function setCustomId($customId): self
{
$this->customId = $customId;
return $this;
}
} }

80
src/Entity/VideoLink.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace App\Entity;
use App\Repository\VideoLinkRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Doctrine\UuidGenerator;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity(repositoryClass=VideoLinkRepository::class)
*/
class VideoLink
{
/**
* @ORM\Id
* @ORM\Column(type="uuid", unique=true)
* @ORM\GeneratedValue(strategy="CUSTOM")
* @ORM\CustomIdGenerator(class=UuidGenerator::class)
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Video::class, inversedBy="videoLinks")
* @ORM\JoinColumn(nullable=false)
*/
private $video;
/**
* @ORM\Column(type="datetime_immutable")
*/
private $created;
/**
* @ORM\Column(type="integer")
*/
private $mode = 0;
public function getId(): ?UuidInterface
{
return $this->id;
}
public function getVideo(): ?Video
{
return $this->video;
}
public function setVideo(?Video $video): self
{
$this->video = $video;
return $this;
}
public function getCreated(): ?DateTimeImmutable
{
return $this->created;
}
public function setCreated(DateTimeImmutable $created): self
{
$this->created = $created;
return $this;
}
public function getMode(): ?int
{
return $this->mode;
}
public function setMode(int $mode): self
{
$this->mode = $mode;
return $this;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Mapper;
use Doctrine\DBAL\Types\ConversionException;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
class CustomUuidMapper
{
private const REPLACE_SLASH = "-";
public function fromString($str): UuidInterface
{
try {
return Uuid::fromBytes(base64_decode(str_replace(self::REPLACE_SLASH, "/", $str) . "=="));
} catch (InvalidArgumentException $e) {
throw new ConversionException($e);
}
}
public function toString(UuidInterface $uuid): string
{
return str_replace("/", self::REPLACE_SLASH, str_replace("==", "", base64_encode($uuid->getBytes())));
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Repository;
use App\Entity\VideoLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method VideoLink|null find($id, $lockMode = null, $lockVersion = null)
* @method VideoLink|null findOneBy(array $criteria, array $orderBy = null)
* @method VideoLink[] findAll()
* @method VideoLink[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class VideoLinkRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, VideoLink::class);
}
// /**
// * @return VideoLink[] Returns an array of VideoLink objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('v')
->andWhere('v.exampleField = :val')
->setParameter('val', $value)
->orderBy('v.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?VideoLink
{
return $this->createQueryBuilder('v')
->andWhere('v.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Service;
use App\Entity\VideoLink;
use App\Repository\VideoLinkRepository;
class VideoLinkService
{
private $videoLinkRepository;
public function __construct(VideoLinkRepository $videoLinkRepository)
{
$this->videoLinkRepository;
}
public function get($linkId): ?VideoLink
{
return $this->videoLinkRepository->findOneById($linkId);
}
}

View file

@ -46,4 +46,9 @@ class VideoService
$video->setState($state); $video->setState($state);
$this->videoRepository->update(); $this->videoRepository->update();
} }
public function get($videoId): ?Video
{
return $this->videoRepository->findOneById($videoId);
}
} }

View file

@ -13,7 +13,7 @@
<div class="container"> <div class="container">
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div> </div>
{% block javascripts %}{% endblock %}
<script src="{{ asset("js/mdb.min.js") }}"></script> <script src="{{ asset("js/mdb.min.js") }}"></script>
{% block javascripts %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -7,6 +7,7 @@
{% for video in videos %} {% for video in videos %}
<div> <div>
{{ video.name }} {{ video.name }}
(<a href="{{ path("app_watch_page", {linkId: constant("App\\Controller\\WatchController::OWNER_LINK_ID"), videoId: video.customId}) }}">Link</a>)
</div> </div>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,32 @@
{% extends 'base.html.twig' %}
{% block title %}Watch{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset("css/video-js.css") }}">
{% endblock %}
{% block javascripts %}
<script src="{{ asset("js/video.min.js") }}"></script>
{% endblock %}
{% block body %}
<video
id="my-video"
class="video-js"
controls
preload="auto"
width="640"
height="264"
poster="{{ thumbnail }}"
data-setup="{}"
>
<!--<source src="{{ global }}#.m3u8" type="application/vnd.apple.mpegur" />-->
<source src="{{ global }}#.m3u8" type="application/x-mpegURL"/>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
</p>
</video>
{% endblock %}