<?php
namespace App\Controller;
use App\DTO\CropVideoDTO;
use App\DTO\MergeVideoDTO;
use App\Entity\UserAgent;
use App\Exception\AppException;
use App\Service\ConverterServiceInterface;
use App\Service\FileService;
use App\Service\FileServiceInterface;
use App\Service\VideoProcessServiceInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Finder\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiController extends AbstractController
{
public function __construct(
private string $uploadDir,
private FileServiceInterface $fileService,
private ConverterServiceInterface $converter,
private VideoProcessServiceInterface $videoProcess,
private LoggerInterface $interviewPublishStreamLogger,
private LoggerInterface $logger,
private HttpClientInterface $client,
private SerializerInterface $serializer,
) {
}
/**
* @Route("/", name="homepage")
* @return Response
*/
public function index()
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
return $this->render('home.html.twig', []);
}
/**
* Used by Flashphoner server in process of allowing media session
*
* @Route("/rest/defaultHigher/connect", name="rest_application_verify", methods={"POST"})
*/
public function appVerify(Request $request)
{
$content = $request->getContent();
$this->denyAccessUnlessGranted('stream', $request);
return new Response($content, 200);
}
/**
* @Route("/rest/defaultHigher/publishStream", name="rest_publish_stream", methods={"POST"})
*/
public function publishStream(Request $request, EntityManagerInterface $entityManager)
{
$content = json_decode($request->getContent(), true);
$filename = 'stream-' . $content['name'] . '-' . $content['mediaSessionId'];
$userAgentObject = new UserAgent();
$userAgentObject->setFilename($filename);
if (!empty($content['custom']['userAgent'])) {
$userAgentObject->setUserAgentData($content['custom']['userAgent']);
} else {
$userAgentObject->setUserAgentData('-');
}
$entityManager->persist($userAgentObject);
$entityManager->flush();
return new JsonResponse($content, 200);
}
/**
* @Route("/rest/defaultHigher/ConnectionStatusEvent", name="rest_higher_connection_status_event", methods={"POST"})
*/
public function onHigherConnectionStatusEvent(Request $request)
{
return new Response($request->getContent(), 200);
}
/**
* Used by Flashphoner upon finish video stream recording.
*
* @Route("/rest/defaultHigher/convert/{filename}", name="rest_convert_video", methods={"GET"})
*/
public function convertVideo($filename)
{
$filename = $this->converter->convertToMp4($filename);
$this->videoProcess->addWatermark($filename);
$this->videoProcess->createSnapshot($filename);
$this->videoProcess->saveToDatabaseVideoFileByName($filename);
return new Response($filename);
}
/**
* Used by Flashphoner upon finish video interview recording.
*
* @Route("/rest/defaultHigher/convertInterview/{filename}", name="rest_convert_video_interview", methods={"GET"})
*/
public function convertInterviewVideo($filename)
{
$filename = $this->converter->convertToMp4($filename);
$this->videoProcess->addWatermark($filename, true);
$this->videoProcess->createSnapshot($filename);
$this->videoProcess->saveToDatabaseVideoFileByName($filename);
return new Response($filename);
}
/**
* Only for use of Frontend application.
*
* @Route("/rest/defaultHigher/path", name="media_server_folder", methods={"GET"})
*/
public function getCurrentMediaPath()
{
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR']);
}
/**
* Used by frontend application to upload files
*
* @Route("/rest/defaultHigher/upload", name="video_file_upload", methods={"POST"})
*/
public function uploadVideo(Request $request)
{
$this->denyAccessUnlessGranted('upload', $request);
/** @var UploadedFile $file */
$file = $request->files->get('nativeCam');
if (null === $file) {
throw new AppException('Missing video file in the request.', 400);
}
$filename = $this->fileService->createVideoFile($file);
$filename = $this->converter->convertToMp4($filename);
$filename = $this->videoProcess->resizeVideoIfLarge($filename, true);
if ($oldFilename = $this->videoProcess->getVideoFilenameByHash($filename)) {
unlink($this->uploadDir . $filename);
unlink($this->uploadDir . 'small_' . $filename);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $oldFilename);
}
$this->videoProcess->createSnapshot($filename);
$this->videoProcess->saveToDatabaseVideoFileByName($filename);
$this->videoProcess->generateHashForVideo($filename);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $filename);
}
/**
* Used by candidate application to upload camera files
*
* @Route("/rest/defaultHigher/uploadCamera", name="camera_file_upload", methods={"POST"})
*/
public function uploadCameraVideo(Request $request, EntityManagerInterface $entityManager)
{
$this->denyAccessUnlessGranted('upload', $request);
/** @var UploadedFile $file */
$file = $request->files->get('nativeCam');
if (null === $file) {
throw new AppException('Missing video file in the request.', 400);
}
$filename = $this->fileService->createVideoFile($file, true);
$this->videoProcess->checkVideoLength($filename, 33.00);
$filename = $this->converter->convertToMp4($filename);
$filename = $this->videoProcess->resizeVideoIfLarge($filename);
$this->videoProcess->addWatermark($filename);
$this->videoProcess->createSnapshot($filename);
$userAgentData = $request->headers->get('User-Agent');
$userAgentObject = new UserAgent();
$userAgentObject->setFilename(FileService::filenameWithoutExtension($filename));
$userAgentObject->setUserAgentData($userAgentData);
$entityManager->persist($userAgentObject);
$entityManager->flush();
$this->videoProcess->saveToDatabaseVideoFileByName($filename);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $filename);
}
/**
* Used by customer to merge streamed camera video and uploaded video
*
* @Route("/rest/defaultHigher/merge", name="merge_video_files", methods={"GET"})
*/
#[Route(path: '/rest/defaultHigher/marge', name: 'merge_video_files', methods: ['GET'])]
#[ParamConverter('mergeVideos', MergeVideoDTO::class)]
public function mergeStreamAndUploadVideo(Request $request, MergeVideoDTO $mergeVideos): Response
{
$this->denyAccessUnlessGranted('upload', $request);
$filename = $this->videoProcess->mergeVideos($mergeVideos);
$this->videoProcess->createSnapshot($filename);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $filename);
}
/**
* Used only by frontend application on action 'Re-record'
*
* @Route("/rest/defaultHigher/delete/{filename}", name="api_delete_video_file", methods={"DELETE"})
*/
public function delete(string $filename)
{
$filenameNoExt = FileService::filenameWithoutExtension($filename);
try {
if (!file_exists($_ENV['RECORDS_DIR_PATH'].$filename)) {
$this->videoProcess->remove($filenameNoExt);
return new JsonResponse(['message' => 'Video removed.']);
}
//copy($_ENV['RECORDS_DIR_PATH'].$filename, $_ENV['RECORDS_DIR_PATH'].'trash/'.$filename);
unlink($_ENV['RECORDS_DIR_PATH'].$filename);
unlink($_ENV['RECORDS_DIR_PATH'].$filenameNoExt.'.jpg');
} catch (\Exception $ex) {
$this->logger->error(sprintf('Problem in delete file: "%s", message: %s', $filename, $ex->getMessage()));
}
$this->videoProcess->remove($filenameNoExt);
return new JsonResponse(['message' => 'Video removed.']);
}
/**
* Used by API command to check if video file in database exists physically
* @Route("/rest/defaultHigher/check/{filename}", name="api_check_video_file", methods={"GET"})
*/
public function checkIfVideoPhysicallyExist(string $filename): Response
{
if (file_exists($this->uploadDir . $filename)) {
return new Response('OK', 200);
}
return new Response('Not found', 404);
}
/**
* @Route("/rest/defaultHigher/checkList", name="api_check_list_video_files", methods={"POST"})
*/
public function checkListOfVideoFiles(Request $request): Response
{
$this->denyAccessUnlessGranted('interview-list', $request);
$files = json_decode($request->getContent(), true);
if (empty($files)) {
return new JsonResponse([]);
}
$existingFiles = [];
foreach($files as $file) {
if (file_exists($this->uploadDir.$file)) {
$existingFiles[] = $file;
}
}
return new JsonResponse($existingFiles);
}
/**
* @Route("/rest/defaultHigher/changePoster", name="api_change_video_poster", methods={"POST"})
*/
public function changeVideoPoster(Request $request)
{
$this->denyAccessUnlessGranted('upload', $request);
/** @var UploadedFile $file */
$file = $request->files->get('posterImage');
if (null === $file) {
throw new AppException('Missing image file in the request.', 400);
}
if (!in_array($file->getClientOriginalExtension(), ['jpg', 'jpeg'])) {
throw new AppException('Image format invalid.', 400);
}
$videoFilename = $request->get('video');
if (!file_exists($_ENV['RECORDS_DIR_PATH'] . $videoFilename)) {
throw new AppException('Video file does not exists.', 400);
}
$filename = explode('.', $videoFilename)[0] . '.jpg';
try {
$file->move($_ENV['RECORDS_DIR_PATH'], $filename);
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage());
throw new AppException('Error saving poster image.', 500);
}
return new Response('OK', 200);
}
/**
* Used by candidate application to upload camera files
*
* @Route("/rest/defaultHigher/uploadWbVideo", name="wb_file_upload", methods={"POST"})
*/
public function uploadWbVideo(Request $request, EntityManagerInterface $entityManager)
{
$captchaToken = $request->request->get('captcha');
$response = $this->client->request('POST', 'https://www.google.com/recaptcha/api/siteverify?secret='
.$_ENV['RECAPTCHA_SECRET_KEY']. '&response=' . $captchaToken);
$arrResponse = \json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!$arrResponse['success']) {
throw new AccessDeniedException('Invalid re-captcha token.', 401);
}
/** @var UploadedFile $file */
$file = $request->files->get('nativeCam');
if (null === $file) {
throw new AppException('Missing video file in the request.', 400);
}
$filename = $this->fileService->createWbVideoFile($file);
$this->videoProcess->checkVideoLength($filename, 185.00);
$filename = $this->converter->convertToMp4($filename);
$filename = $this->videoProcess->resizeVideoIfLarge($filename);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $filename);
}
/**
* Used by customer to crop video file for use in social networks
*
* @Route("/rest/defaultHigher/cropVideo", name="crop_video", methods={"POST"})
*/
public function cropVideoForSocialNetworks(Request $request)
{
$this->denyAccessUnlessGranted('upload', $request);
$cropVideoDTO = $this->serializer->deserialize($request->getContent(), CropVideoDTO::class, 'json', [
AbstractNormalizer::ATTRIBUTES => ['videoUrl', 'marginH', 'marginV'],
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
return new Response($_ENV['MS_URL'] . '/' . $_ENV['RECORDS_DIR'] . $this->videoProcess
->cropVideoFileForSocialNetworks($cropVideoDTO));
}
}