diff --git a/.env b/.env index 5145bf5072..992de4b7ae 100644 --- a/.env +++ b/.env @@ -23,10 +23,10 @@ _APP_DB_SCHEMA=appwrite _APP_DB_USER=user _APP_DB_PASS=password _APP_DB_ROOT_PASS=rootsecretpassword -_APP_STORAGE_DEVICE=DOSpaces +_APP_STORAGE_DEVICE=Local _APP_STORAGE_S3_ACCESS_KEY= _APP_STORAGE_S3_SECRET= -_APP_STORAGE_S3_REGION=us-east-1 +_APP_STORAGE_S3_REGION= _APP_STORAGE_S3_BUCKET= _APP_STORAGE_DO_SPACES_ACCESS_KEY= _APP_STORAGE_DO_SPACES_SECRET= diff --git a/app/console b/app/console index ac4181aea4..2fac7c1f39 160000 --- a/app/console +++ b/app/console @@ -1 +1 @@ -Subproject commit ac4181aea403d888e63cb527c700e80013c68ea8 +Subproject commit 2fac7c1f390637f7dfc11882dcad6fb6508c83f1 diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index f6d7c5392e..9034722ac2 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -807,7 +807,7 @@ App::delete('/v1/videos/:videoId/renditions/:renditionId') $response->noContent(); }); -App::get('/v1/videos/:videoId/outputs/:output') +App::get('/v1/videos/:videoId/outputs/:output/:fileName') ->desc('Get video master renditions manifest') ->groups(['api', 'videos']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -821,10 +821,11 @@ App::get('/v1/videos/:videoId/outputs/:output') ->label('scope', 'videos.read') ->param('videoId', null, new UID(), 'Video unique ID.') ->param('output', '', new WhiteList(['hls', 'dash']), 'output name') + ->param('fileName', '', new WhiteList(['master.m3u8', 'master.mpd']), 'manifest filename') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $videoId, string $output, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $videoId, string $output, string $fileName, Response $response, Database $dbForProject, string $mode) { $dbStartTime = microtime(true); $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); @@ -852,7 +853,7 @@ App::get('/v1/videos/:videoId/outputs/:output') ])); $dbEnd = microtime(true) - $dbStartTime; - var_dump('$dbEnd = ' . $dbEnd); + var_dump('$dbTime = ' . $dbEnd); $prosessStart = microtime(true); $_renditions = $_subtitles = []; @@ -862,7 +863,7 @@ App::get('/v1/videos/:videoId/outputs/:output') 'name' => $subtitle->getAttribute('name'), 'code' => $subtitle->getAttribute('code'), 'default' => !empty($subtitle->getAttribute('default')) ? 'YES' : 'NO', - 'uri' => $baseUrl . '/subtitles/' . $subtitle->getId(), + 'uri' => $baseUrl . '/subtitles/' . $subtitle->getId() . '/subtitles.m3u8', ]; } @@ -878,10 +879,10 @@ App::get('/v1/videos/:videoId/outputs/:output') 'name' => $stream['language'], 'default' => ($i === 0) ? 'YES' : 'NO', 'language' => $stream['language'], - 'uri' => $baseUrl . '/renditions/' . $rendition->getId() . '/streams/' . $stream['id'], + 'uri' => $baseUrl . '/renditions/' . $rendition->getId() . '/streams/' . $stream['id'] . '/playlist.m3u8', ]; } elseif ($stream['type'] === 'video') { - $uri = $baseUrl . '/renditions/' . $rendition->getId() . '/streams/' . $stream['id']; + $uri = $baseUrl . '/renditions/' . $rendition->getId() . '/streams/' . $stream['id'] . '/playlist.m3u8'; $resolution = $stream['resolution'] ?? $rendition->getAttribute('width') . 'x' . $rendition->getAttribute('height'); $bandwidth = $stream['bandwidth'] ?? ($rendition->getAttribute('videoBitRate') + $rendition->getAttribute('audioBitRate')) * 1024; $codecs = $stream['codecs'] ?? null; @@ -910,7 +911,6 @@ App::get('/v1/videos/:videoId/outputs/:output') // $prosessEnd = microtime(true) - $prosessStart; // var_dump('$dbEnd = ' . $dbEnd); // var_dump('$prosessEnd = ' . $prosessEnd); - } else { $adaptationId = 0; foreach ($renditions as $rendition) { @@ -942,11 +942,11 @@ App::get('/v1/videos/:videoId/outputs/:output') foreach ($segments ?? [] as $segment) { if ($segment->getAttribute('isInit')) { - $representation['segmentList']['init'] = $segment->getId(); + $representation['segmentList']['init'] = $segment->getId() . '/segment.m4s'; continue; } - $representation['segmentList']['media'][] = $segment->getId(); + $representation['segmentList']['media'][] = $segment->getId() . '/segment.m4s'; } $attributes = (array)$adaptation->attributes(); @@ -964,7 +964,7 @@ App::get('/v1/videos/:videoId/outputs/:output') foreach ($subtitles ?? [] as $subtitle) { $_subtitles[] = [ 'id' => $adaptationId, - 'baseUrl' => $baseUrl . '/subtitles/' . $subtitle->getId(), + 'baseUrl' => $baseUrl . '/subtitles/' . $subtitle->getId() . '/subtitle.vtt', 'name' => $subtitle->getAttribute('name'), ]; $adaptationId++; @@ -981,7 +981,7 @@ App::get('/v1/videos/:videoId/outputs/:output') } }); -App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:streamId') +App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:streamId/:fileName') ->desc('Get video rendition manifest') ->groups(['api', 'videos']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -997,10 +997,11 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:s ->param('output', '', new WhiteList(['hls']), 'Output name.') ->param('renditionId', null, new UID(), 'Rendition unique ID.') ->param('streamId', 0, new Range(0, 10), 'Stream ID.') + ->param('fileName', '', new WhiteList(['playlist.m3u8']), 'Playlist filename') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->action(function (string $videoId, string $output, string $renditionId, string $streamId, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $videoId, string $output, string $renditionId, string $streamId, string $fileName, Response $response, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); @@ -1034,7 +1035,7 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:s foreach ($segments as $segment) { $_segments[] = [ 'duration' => $segment->getAttribute('duration'), - 'url' => TMP_HOST . 'v1/videos/' . $videoId . '/outputs/' . $output . '/renditions/' . $renditionId . '/segments/' . $segment->getId(), + 'url' => TMP_HOST . 'v1/videos/' . $videoId . '/outputs/' . $output . '/renditions/' . $renditionId . '/segments/' . $segment->getId() . '/segment.ts', ]; } @@ -1045,7 +1046,7 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/streams/:s ->send($template->render(false)); }); -App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/segments/:segmentId') +App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/segments/:segmentId/:fileName') ->desc('Get video rendition segment') ->groups(['api', 'videos']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -1061,10 +1062,11 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/segments/: ->param('output', '', new WhiteList(['hls', 'dash']), 'Output name') ->param('renditionId', '', new UID(), 'Rendition unique ID.') ->param('segmentId', '', new UID(), 'Segment unique ID.') + ->param('fileName', '', new WhiteList(['segment.ts', 'segment.m4s']), 'Segment filename') ->inject('response') ->inject('dbForProject') ->inject('deviceVideos') - ->action(function (string $videoId, string $output, string $renditionId, string $segmentId, Response $response, Database $dbForProject, Device $deviceVideos) { + ->action(function (string $videoId, string $output, string $renditionId, string $segmentId, string $fileName, Response $response, Database $dbForProject, Device $deviceVideos) { $segment = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions_segments', $segmentId)); if ($segment->isEmpty()) { @@ -1076,13 +1078,13 @@ App::get('/v1/videos/:videoId/outputs/:output/renditions/:renditionId/segments/: if ($output === 'hls') { $response->setContentType('video/MP2T') ->send($data); - } else { - $response->setContentType('video/iso.segment') - ->send($data); } + + $response->setContentType('video/iso.segment') + ->send($data); }); -App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId') +App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/:fileName') ->desc('Get video subtitle') ->groups(['api', 'videos']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -1097,11 +1099,12 @@ App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId') ->param('videoId', null, new UID(), 'Video unique ID.') ->param('output', '', new WhiteList(['hls', 'dash']), 'Protocol name') ->param('subtitleId', '', new UID(), 'Subtitle unique ID.') + ->param('fileName', '', new WhiteList(['subtitles.m3u8', 'subtitle.vtt']), 'Subtitle filename') ->inject('response') ->inject('dbForProject') ->inject('deviceVideos') ->inject('mode') - ->action(function (string $videoId, string $output, string $subtitleId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode) { + ->action(function (string $videoId, string $output, string $subtitleId, $fileName, Response $response, Database $dbForProject, Device $deviceVideos, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if ($video->isEmpty()) { @@ -1119,36 +1122,36 @@ App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId') throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } - if ($output === 'hls') { - $segments = Authorization::skip(fn () => $dbForProject->find('videos_subtitles_segments', [ - Query::equal('subtitleId', [$subtitleId]), - ])); - - if (empty($segments)) { - throw new Exception(Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); - } - - $_segments = []; - foreach ($segments as $segment) { - $_segments[] = [ - 'duration' => $segment->getAttribute('duration'), - 'url' => TMP_HOST . 'v1/videos/' . $videoId . '/outputs/' . $output . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId(), - ]; - } - - $template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml'); - $template->setParam('targetDuration', $subtitle->getAttribute('targetDuration')); - $template->setParam('segments', $_segments); - $response->setContentType('application/x-mpegurl') - ->send($template->render(false)); - } else { - $data = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId')) . '/' . $subtitle->getId() . '.vtt'); + if ($output === 'dash') { + $data = $deviceVideos->read($deviceVideos->getPath($subtitle->getAttribute('videoId')) . '/subtitles/' . $subtitle->getId() . '.vtt'); $response->setContentType('text/vtt') ->send($data); } + + $segments = Authorization::skip(fn () => $dbForProject->find('videos_subtitles_segments', [ + Query::equal('subtitleId', [$subtitleId]), + ])); + + if (empty($segments)) { + throw new Exception(Exception::VIDEO_SUBTITLE_SEGMENT_NOT_FOUND); + } + + $_segments = []; + foreach ($segments as $segment) { + $_segments[] = [ + 'duration' => $segment->getAttribute('duration'), + 'url' => TMP_HOST . 'v1/videos/' . $videoId . '/outputs/' . $output . '/subtitles/' . $subtitleId . '/segments/' . $segment->getId() . '/subtitle.vtt', + ]; + } + + $template = new View(__DIR__ . '/../../views/videos/hls-subtitles.phtml'); + $template->setParam('targetDuration', $subtitle->getAttribute('targetDuration')); + $template->setParam('segments', $_segments); + $response->setContentType('application/x-mpegurl') + ->send($template->render(false)); }); -App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/segments/:segmentId') +App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/segments/:segmentId/:fileName') ->desc('Get video subtitle segment') ->groups(['api', 'videos']) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) @@ -1164,11 +1167,12 @@ App::get('/v1/videos/:videoId/outputs/:output/subtitles/:subtitleId/segments/:se ->param('output', '', new WhiteList(['hls', 'dash']), 'output name') ->param('subtitleId', '', new UID(), 'Subtitle unique ID.') ->param('segmentId', '', new UID(), 'Segment unique ID.') + ->param('fileName', '', new WhiteList(['subtitle.vtt']), 'Subtitle filename') ->inject('response') ->inject('dbForProject') ->inject('deviceVideos') ->inject('mode') - ->action(function (string $videoId, string $output, string $subtitleId, string $segmentId, Response $response, Database $dbForProject, Device $deviceVideos, string $mode) { + ->action(function (string $videoId, string $output, string $subtitleId, string $segmentId, string $fileName, Response $response, Database $dbForProject, Device $deviceVideos, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); if ($video->isEmpty()) { diff --git a/app/init.php b/app/init.php index e5a3114ace..c053ff9d30 100644 --- a/app/init.php +++ b/app/init.php @@ -889,7 +889,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) { /** @var Utopia\Database\Document $console */ $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); - //$projectId = '6434237946e602be6953'; + $projectId = '6444e6ceb5f6d277860d'; if ($projectId === 'console') { return $console; diff --git a/app/views/videos/dash.phtml b/app/views/videos/dash.phtml index fa5b6881ff..45ddbfd86e 100644 --- a/app/views/videos/dash.phtml +++ b/app/views/videos/dash.phtml @@ -31,7 +31,7 @@ echo ''; ?> - escape($subtitle['baseUrl']); endif;?> + escape($subtitle['baseUrl']); endif;?> diff --git a/app/views/videos/hls-master.phtml b/app/views/videos/hls-master.phtml index 801db9640b..d6849d390b 100644 --- a/app/views/videos/hls-master.phtml +++ b/app/views/videos/hls-master.phtml @@ -1,12 +1,12 @@ #EXTM3U #EXT-X-VERSION:3 getParam('audios', []) as $audio):?> -#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio",NAME="",AUTOSELECT=YES,FORCED=NO,DEFAULT=,LANGUAGE="",URI="" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio",NAME="",AUTOSELECT=YES,LANGUAGE="",URI="" getParam('subtitles', []) as $subtitle):?> #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="escape($subtitle['name']); ?>",DEFAULT=,AUTOSELECT=YES,FORCED=NO,LANGUAGE="escape($subtitle['code']); ?>",URI="" getParam('renditions', []) as $rendition):?> -#EXT-X-STREAM-INF:BANDWIDTH="",RESOLUTION="",NAME="",CODECS=" +#EXT-X-STREAM-INF:BANDWIDTH="",RESOLUTION="",NAME="",CODECS="",SUBTITLES="",AUDIO="" diff --git a/app/views/videos/hls-subtitles.phtml b/app/views/videos/hls-subtitles.phtml index fd179ea035..cc15fc2d03 100644 --- a/app/views/videos/hls-subtitles.phtml +++ b/app/views/videos/hls-subtitles.phtml @@ -5,6 +5,6 @@ #EXT-X-TARGETDURATION:getParam('targetDuration', 0)?> #EXT-X-MEDIA-SEQUENCE:0 getParam('segments', []) as $segment): ?> - #EXTINF: - +#EXTINF: + \ No newline at end of file diff --git a/app/workers/videos.php b/app/workers/videos.php index 421cf92062..901c689716 100644 --- a/app/workers/videos.php +++ b/app/workers/videos.php @@ -58,6 +58,7 @@ class VideosV1 extends Worker private Document $bucket; private FFProbe $ffprobe; private FFMpeg $ffmpeg; + private bool $isVideo = false; public function getName(): string { @@ -128,6 +129,7 @@ class VideosV1 extends Worker ->setAttribute('format', $general->has('format') ? $general->get('format')->getShortName() : ''); foreach ($mediaInfoContainer->getVideos() ?? [] as $video) { + $this->isVideo = true; $this->video ->setAttribute('height', $video->has('height') ? $video->get('height')->getAbsoluteValue() : 0) ->setAttribute('width', $video->has('width') ? $video->get('width')->getAbsoluteValue() : 0) @@ -163,10 +165,13 @@ class VideosV1 extends Worker ), ); } - + $this->video->getAttribute('videoBitRate'); $media = $this->ffmpeg->open($inPath); if ($this->action === 'preview') { + if (!$this->isVideo) { + return; + } console::info('Creating preview image from second ' . $this->args['second']); $path = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/preview/'; @@ -242,6 +247,9 @@ class VideosV1 extends Worker * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * Text Domain: playerjs */ + if (!$this->isVideo) { + return; + } $interval = 2; $ranges = [ @@ -671,9 +679,10 @@ class VideosV1 extends Worker $attr['resolution'] = $parts[1]; break; case str_contains($parts[0], 'CODECS'): - $attr['codecs'] = $parts[1]; - if(!empty($attributes[$key + 1])){ - $attr['codecs'] .= ',' . $attributes[$key + 1]; + $attr['codecs'] = trim($parts[1], " \n\r\t\v\x00"); + + if (!empty($attributes[$key + 1])) { + $attr['codecs'] .= ',' . trim($attributes[$key + 1], " \n\r\t\v\x00"); } break; } diff --git a/docker-compose.yml b/docker-compose.yml index 4ee4274d2a..2bb1135b1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -243,9 +243,9 @@ services: - appwrite-uploads:/storage/uploads:rw - appwrite-videos:/storage/videos:rw #- ./tests:/usr/src/code/tests #TODO remove when done!! - #- ./vendor/aminyazdanpanah/php-ffmpeg-video-streaming:/usr/src/code/vendor/aminyazdanpanah/php-ffmpeg-video-streaming #TODO remove when done!! - #- ./vendor/php-ffmpeg/php-ffmpeg:/usr/src/code/vendor/php-ffmpeg/php-ffmpeg #TODO remove when done!! - #- ./vendor/alchemy/binary-driver:/usr/src/code/vendor/alchemy/binary-driver + - ./vendor/aminyazdanpanah/php-ffmpeg-video-streaming:/usr/src/code/vendor/aminyazdanpanah/php-ffmpeg-video-streaming #TODO remove when done!! + - ./vendor/php-ffmpeg/php-ffmpeg:/usr/src/code/vendor/php-ffmpeg/php-ffmpeg #TODO remove when done!! + - ./vendor/alchemy/binary-driver:/usr/src/code/vendor/alchemy/binary-driver - ./vendor/utopia-php/storage:/usr/src/code/vendor/utopia-php/storage #- ./vendor/symfony/process:/usr/src/code/vendor/symfony/process