diff --git a/app/config/roles.php b/app/config/roles.php index 72b1d505bb..b3891a1769 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -19,6 +19,8 @@ $member = [ 'avatars.read', 'execution.read', 'execution.write', + 'videos.read', + 'videos.write', ]; $admins = [ @@ -49,6 +51,8 @@ $admins = [ 'functions.write', 'execution.read', 'execution.write', + 'videos.read', + 'videos.write', ]; return [ @@ -62,6 +66,7 @@ return [ 'files.read', 'locale.read', 'avatars.read', + 'videos.read', ], ], Auth::USER_ROLE_MEMBER => [ diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index d4d78c5ad2..9271618c35 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -717,11 +717,11 @@ App::get('/v1/videos/:videoId/streams/:streamId') } $baseUrl = 'http://127.0.0.1/v1/videos/' . $videoId . '/streams/' . $streamId; + $subtitles = Authorization::skip(fn() => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$video->getId()])])); if ($streamId === 'hls') { - $subtitles = Authorization::skip(fn() => $dbForProject->find('videos_subtitles', [new Query('videoId', Query::TYPE_EQUAL, [$video->getId()])], 12, 0, [], ['ASC'])); $paramsSubtitles = []; - foreach ($subtitles as $subtitle) { + foreach ($subtitles ?? [] as $subtitle) { $paramsSubtitles[] = [ 'name' => $subtitle->getAttribute('name'), 'code' => $subtitle->getAttribute('code'), @@ -731,7 +731,7 @@ App::get('/v1/videos/:videoId/streams/:streamId') } $paramsRenditions = []; - foreach ($renditions as $rendition) { + foreach ($renditions ?? [] as $rendition) { $paramsRenditions[] = [ 'bandwidth' => ($rendition->getAttribute('videoBitrate') + $rendition->getAttribute('audioBitrate')), 'resolution' => $rendition->getAttribute('width') . 'X' . $rendition->getAttribute('height'), @@ -746,22 +746,24 @@ App::get('/v1/videos/:videoId/streams/:streamId') $template->setParam('paramsRenditions', $paramsRenditions); $response->setContentType('application/x-mpegurl')->send($template->render(false)); } else { + $adaptationsSubs = []; $adaptations = []; $adaptationId = 0; - foreach ($renditions as $rendition) { + foreach ($renditions ?? [] as $rendition) { $metadata = $rendition->getAttribute('metadata'); $xml = simplexml_load_string($metadata['mpd']); if (empty($xml)) { continue; } $representationId = 0; - foreach ($xml->Period->AdaptationSet as $adaptation) { + foreach ($xml->Period->AdaptationSet ?? [] as $adaptation) { $representation = []; $representation['id'] = $representationId; $attributes = (array)$adaptation->Representation->attributes(); $representation['attributes'] = $attributes['@attributes'] ?? []; $attributes = (array)$adaptation->Representation->SegmentList->attributes(); - $representation['SegmentList']['attributes'] = $attributes['@attributes'] ?? []; + $representation['segmentList']['attributes'] = $attributes['@attributes'] ?? []; + $segments = Authorization::skip(fn() => $dbForProject->find('videos_renditions_segments', [ new Query('renditionId', Query::TYPE_EQUAL, [$rendition->getId()]), new Query('representationId', Query::TYPE_EQUAL, [$representationId]), @@ -771,23 +773,33 @@ App::get('/v1/videos/:videoId/streams/:streamId') } foreach ($segments ?? [] as $segment) { if ($segment->getAttribute('isInit')) { - $representation['SegmentList']['Initialization'] = $segment->getId(); + $representation['segmentList']['init'] = $segment->getId(); continue; } - $representation['SegmentList']['media'][] = $segment->getId(); + $representation['segmentList']['media'][] = $segment->getId(); } $attributes = (array)$adaptation->attributes(); $adaptations[] = ['attributes' => $attributes['@attributes'] ?? [], 'id' => $adaptationId, 'baseUrl' => $baseUrl . '/renditions/' . $rendition->getId() . '/' . 'segments/', - 'Representation' => $representation, + 'representation' => $representation, ]; $adaptationId++; $representationId++; } + + foreach ($subtitles ?? [] as $subtitle) { + $adaptationsSubs[] = [ + 'id' => $adaptationId, + 'baseUrl' => $baseUrl . '/subtitles/' . $subtitle->getId(), + 'code' => $subtitle->getAttribute('code'), + ]; + $adaptationId++; + } } $template = new View(__DIR__ . '/../../views/videos/dash.phtml'); + $template->setParam('subtitles', $adaptationsSubs); $template->setParam('params', $adaptations); $response->setContentType('application/dash+xml')->send($template->render(false)); } @@ -982,7 +994,7 @@ App::get('/v1/videos/:videoId/streams/:streamId/subtitles/:subtitleId') $template->setParam('paramsSegments', $paramsSegments); $response->setContentType('application/x-mpegurl')->send($template->render(false)); } else { - $output = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId') . $subtitle->getAttribute('fileId') . '.vtt')); + $output = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId')) . '/' . $subtitle->getId() . '.vtt'); $response->setContentType('text/vtt')->send($output); } }); diff --git a/app/init.php b/app/init.php index 1eeeee2ade..2ddef1e77d 100644 --- a/app/init.php +++ b/app/init.php @@ -844,8 +844,8 @@ App::setResource('project', function ($dbForConsole, $request, $console) { /** @var Utopia\Database\Database $dbForConsole */ /** @var Utopia\Database\Document $console */ - //$projectId = 'dev'; - $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); + $projectId = 'dev'; + //$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); if ($projectId === 'console') { return $console; diff --git a/app/views/videos/dash.phtml b/app/views/videos/dash.phtml index 40312b9774..79c1d0f257 100644 --- a/app/views/videos/dash.phtml +++ b/app/views/videos/dash.phtml @@ -8,7 +8,7 @@ echo ''; ?> getParam('params', []) as $param): - $representation = $param['Representation']; + $representation = $param['representation']; ?> id="" contentType="" startWithSAP="" segmentAlignment="" bitstreamSwitching="" frameRate="" maxWidth="" par="" lang=""> id="" mimeType="" codecs="" width="" height="" sar="" audioSamplingRate=""> @@ -16,10 +16,10 @@ echo ''; ?> - timescale="" duration="" startNumber=""> - - - + timescale="" duration="" startNumber=""> + + + @@ -27,5 +27,13 @@ echo ''; ?> + getParam('subtitles', []) as $subtitle):?> + + + + + + + \ No newline at end of file diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index 8cff6ca778..88d0989ebf 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -187,16 +187,17 @@ class TranscodingV1 extends Worker } $ext = pathinfo($subtitleFileName, PATHINFO_EXTENSION); + $subtitlePath = $this->inDir . $subtitle->getId() . '.vtt'; if ($ext === 'srt') { $srt = new SubripFile($this->inDir . $subtitleFileName); - $srt->convertTo('webvtt')->save($this->inDir . $this->args['videoId'] . '.vtt'); + $srt->convertTo('webvtt')->save($subtitlePath); } $subs[] = [ 'name' => $subtitle->getAttribute('name'), 'code' => $subtitle->getAttribute('code'), - 'path' => $this->inDir . $this->args['videoId'] . '.vtt', + 'path' => $subtitlePath, ]; } @@ -281,36 +282,36 @@ class TranscodingV1 extends Worker $query->setAttribute('endedAt', time()); Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query)); - if (!empty($subtitles)) { - foreach ($subtitles as $subtitle) { - if ($profile['stream'] === 'hls') { - $m3u8 = $this->parseM3u8($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8'); - foreach ($m3u8['segments'] as $segment) { - Authorization::skip(function () use ($segment, $project, $subtitle, $renditionRootPath) { - return $this->database->createDocument('videos_subtitles_segments', new Document([ - 'subtitleId' => $subtitle->getId(), - 'fileName' => $segment['fileName'], - 'path' => $renditionRootPath , - 'duration' => $segment['duration'], - ])); - }); - } - $subtitle->setAttribute('targetDuration', $m3u8['targetDuration']); + foreach ($subtitles ?? [] as $subtitle) { + if ($profile['stream'] === 'hls') { + $m3u8 = $this->parseM3u8($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8'); + foreach ($m3u8['segments'] ?? [] as $segment) { + Authorization::skip(function () use ($segment, $project, $subtitle, $renditionRootPath) { + return $this->database->createDocument('videos_subtitles_segments', new Document([ + 'subtitleId' => $subtitle->getId(), + 'fileName' => $segment['fileName'], + 'path' => $renditionRootPath , + 'duration' => $segment['duration'], + ])); + }); } - - $subtitle->setAttribute('status', self::STATUS_READY); - $subtitle->setAttribute('path', $renditionRootPath); - Authorization::skip(fn() => $this->database->updateDocument( - 'videos_subtitles', - $subtitle->getId(), - $subtitle - )); + $subtitle->setAttribute('targetDuration', $m3u8['targetDuration']); + } else { + $this->getFilesDevice($project->getId())->transfer($this->inDir . $subtitle->getId() . '.vtt', $this->outDir . $subtitle->getId() . '.vtt', $this->getFilesDevice($project->getId())); } + + $subtitle->setAttribute('status', self::STATUS_READY); + $subtitle->setAttribute('path', $renditionRootPath); + Authorization::skip(fn() => $this->database->updateDocument( + 'videos_subtitles', + $subtitle->getId(), + $subtitle + )); } + /** Upload & cleanup **/ $start = 0; $fileNames = scandir($this->outDir); - foreach ($fileNames as $fileName) { if ($fileName === '.' || $fileName === '..') { //str_contains($fileName, '.json')) { @@ -330,7 +331,6 @@ class TranscodingV1 extends Worker Authorization::skip(fn() => $this->database->updateDocument($collection, $query->getId(), $query)); $start = 1; } - //@unlink($this->outDir . $fileName); } @@ -341,7 +341,7 @@ class TranscodingV1 extends Worker var_dump($th->getMessage()); $query->setAttribute('metadata', json_encode([ 'code' => $th->getCode(), - 'message' => substr($th->getMessage(), 0, 3800), + 'message' => substr($th->getMessage(), 0, 3600), ])); $query->setAttribute('status', self::STATUS_ERROR); @@ -359,14 +359,18 @@ class TranscodingV1 extends Worker */ private function transcode(string $stream, Media $video, StreamFormat $format, Representation $representation, array $subtitles): string | array { - $video->filters() - ->framerate(new FFMpeg\Coordinate\FrameRate(24), 2) - ; +// $video->filters() +// ->framerate(new FFMpeg\Coordinate\FrameRate(24), 2) +// ; $additionalParams = [ '-dn', '-sn', '-vf', 'scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1', + '-r', '24', + '-b_strategy', '1', + '-bf', '3', + '-g', '2' ]; $segmentSize = 8; @@ -480,8 +484,8 @@ class TranscodingV1 extends Worker private function getVideoSourceInfo(StreamCollection $streams): array { return [ - 'duration' => $streams->videos()->count()> 0 ? $streams->videos()->first()->get('duration') : '0', - 'height' => $streams->videos()->count()> 0 ? $streams->videos()->first()->get('height') : 0, + 'duration' => $streams->videos()->count() > 0 ? $streams->videos()->first()->get('duration') : '0', + 'height' => $streams->videos()->count() > 0 ? $streams->videos()->first()->get('height') : 0, 'width' => $streams->videos()->count() > 0 ? $streams->videos()->first()->get('width') : 0, 'videoCodec' => $streams->videos()->count() > 0 ? $streams->videos()->first()->get('codec_name') : '', 'videoFramerate' => $streams->videos()->count() > 0 ? $streams->videos()->first()->get('avg_frame_rate') : '', @@ -506,17 +510,17 @@ class TranscodingV1 extends Worker $info['width'] = $representation->getWidth(); $info['height'] = $representation->getHeight(); - foreach ($metadata['video']['streams'] ?? [] as $streams) { - if ($streams['codec_type'] === 'video') { - $info['duration'] = !empty($streams['duration']) ? $streams['duration'] : '0'; - $info['videoCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] : ''; - $info['videoBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getKiloBitrate(); - $info['videoFramerate'] = !empty($streams['avg_frame_rate']) ? $streams['avg_frame_rate'] : ''; - } elseif ($streams['codec_type'] === 'audio') { - $info['audioCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] : '' ; - $info['audioSamplerate'] = !empty($streams['sample_rate']) ? (int)$streams['sample_rate'] : 0; - $info['audioBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getAudioKiloBitrate(); - } + foreach ($metadata['video']['streams'] ?? [] as $streams) { + if ($streams['codec_type'] === 'video') { + $info['duration'] = !empty($streams['duration']) ? $streams['duration'] : '0'; + $info['videoCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] : ''; + $info['videoBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getKiloBitrate(); + $info['videoFramerate'] = !empty($streams['avg_frame_rate']) ? $streams['avg_frame_rate'] : ''; + } elseif ($streams['codec_type'] === 'audio') { + $info['audioCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] : '' ; + $info['audioSamplerate'] = !empty($streams['sample_rate']) ? (int)$streams['sample_rate'] : 0; + $info['audioBitrate'] = !empty($streams['bit_rate']) ? (int)$streams['bit_rate'] : $representation->getAudioKiloBitrate(); + } } return $info; }