diff --git a/.env b/.env index e059fee9fc..5eefcfd48b 100644 --- a/.env +++ b/.env @@ -58,7 +58,7 @@ _APP_SMTP_USERNAME= _APP_SMTP_PASSWORD= _APP_SMS_PROVIDER=sms://username:password@mock _APP_SMS_FROM=+123456789 -_APP_STORAGE_LIMIT=3000000000000 +_APP_STORAGE_LIMIT=30000000000000 _APP_STORAGE_PREVIEW_LIMIT=20000000 _APP_PHONE_PROVIDER=phone://mock _APP_PHONE_FROM=+123456789 diff --git a/app/config/videos-profiles.php b/app/config/videos-profiles.php index 792bcf2428..184afabf09 100644 --- a/app/config/videos-profiles.php +++ b/app/config/videos-profiles.php @@ -24,7 +24,7 @@ return [ 'audioBitrate' => 128, 'width' => 1280, 'height' => 720, - 'protocol' => 'hls' + 'protocol' => 'dash' ], ]; diff --git a/app/controllers/api/videos.php b/app/controllers/api/videos.php index cdd4474402..09de858b03 100644 --- a/app/controllers/api/videos.php +++ b/app/controllers/api/videos.php @@ -42,6 +42,7 @@ function validateFilePermissions(Database $dbForProject, string $bucketId, strin $fileSecurity = $bucket->getAttribute('fileSecurity', false); $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); + if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } @@ -52,6 +53,10 @@ function validateFilePermissions(Database $dbForProject, string $bucketId, strin $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + return $file; } @@ -77,7 +82,6 @@ App::post('/v1/videos') ->action(action: function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode) { $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode); - $video = Authorization::skip(function () use ($dbForProject, $bucketId, $file) { return $dbForProject->createDocument('videos', new Document([ 'bucketId' => $bucketId, @@ -115,6 +119,8 @@ App::delete('/v1/videos/:videoId') throw new Exception(Exception::VIDEO_NOT_FOUND); } + validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode); + $deleted = $dbForProject->deleteDocument('videos', $videoId); if (!$deleted) { @@ -150,10 +156,13 @@ App::put('/v1/videos/:videoId') ->action(function (string $videoId, $bucketId, $fileId, Response $response, Document $project, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if ($video->isEmpty()) { throw new Exception(Exception::VIDEO_NOT_FOUND); } + validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode); + $file = validateFilePermissions($dbForProject, $bucketId, $fileId, $mode); $video = Authorization::skip(fn() => $dbForProject->updateDocument('videos', $videoId, new Document([ @@ -188,13 +197,17 @@ App::get('/v1/videos/:videoId') ->param('videoId', '', new UID(), 'Video unique ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $videoId, Response $response, Database $dbForProject) { + ->inject('mode') + ->action(function (string $videoId, Response $response, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if ($video->isEmpty()) { throw new Exception(Exception::VIDEO_NOT_FOUND); } + validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode); + $response->dynamic($video, Response::MODEL_VIDEO); }); @@ -262,6 +275,7 @@ App::post('/v1/videos/:videoId/subtitles') ->action(action: function (string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Request $request, Response $response, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if ($video->isEmpty()) { throw new Exception(Exception::VIDEO_NOT_FOUND); } @@ -308,6 +322,7 @@ App::patch('/v1/videos/:videoId/subtitles/:subtitleId') ->action(action: function (string $subtitleId, string $videoId, string $bucketId, string $fileId, string $name, string $code, bool $default, Response $response, Database $dbForProject) { $subtitle = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles', $subtitleId)); + if ($subtitle->isEmpty()) { throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } @@ -344,6 +359,7 @@ App::delete('/v1/videos/:videoId/subtitles/:subtitleId') ->action(function (string $videoId, string $subtitleId, Response $response, Database $dbForProject, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if ($video->isEmpty()) { throw new Exception(Exception::VIDEO_NOT_FOUND); } @@ -351,6 +367,7 @@ App::delete('/v1/videos/:videoId/subtitles/:subtitleId') validateFilePermissions($dbForProject, $video['bucketId'], $video['fileId'], $mode); $subtitle = Authorization::skip(fn() => $dbForProject->getDocument('videos_subtitles', $subtitleId)); + if ($subtitle->isEmpty()) { throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); } @@ -385,6 +402,7 @@ App::get('/v1/videos/:videoId/subtitles') ]; $subtitles = Authorization::skip(fn () => $dbForProject->find('videos_subtitles', $query)); + if (empty($subtitles)) { throw new Exception(Exception::VIDEO_SUBTITLE_NOT_FOUND); } @@ -418,6 +436,7 @@ App::post('/v1/videos/:videoId/rendition') ->action(action: function (string $videoId, string $profileId, Request $request, Response $response, Database $dbForProject, Document $project, string $mode) { $video = Authorization::skip(fn() => $dbForProject->getDocument('videos', $videoId)); + if ($video->isEmpty()) { throw new Exception(Exception::VIDEO_NOT_FOUND); } @@ -432,8 +451,8 @@ App::post('/v1/videos/:videoId/rendition') $transcoder = new Transcoding(); $transcoder ->setProject($project) - ->setVideoId($video->getId()) - ->setProfileId($profile->getId()) + ->setVideo($video) + ->setProfile($profile) ->trigger(); $response->noContent(); @@ -599,8 +618,7 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') Query::equal('videoId', [$video->getId()]), ])); - $_renditions = []; - $_subtitles = []; + $_renditions = $_subtitles = []; if ($protocolId === 'hls') { foreach ($subtitles ?? [] as $subtitle) { @@ -671,6 +689,8 @@ App::get('/v1/videos/:videoId/protocols/:protocolId') $segments = Authorization::skip(fn () => $dbForProject->find('videos_renditions_segments', [ Query::equal('renditionId', [$rendition->getId()]), Query::equal('streamId', [$streamId]), + Query::orderAsc('_id'), + Query::limit(5000), ])); if (count($segments) === 0) { @@ -758,6 +778,8 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/stre $segments = Authorization::skip(fn () => $dbForProject->find('videos_renditions_segments', [ Query::equal('renditionId', [$renditionId]), Query::equal('streamId', [$streamId]), + Query::orderAsc('_id'), + Query::limit(5000) ])); if (empty($segments)) { @@ -800,6 +822,7 @@ App::get('/v1/videos/:videoId/protocols/:protocolId/renditions/:renditionId/segm ->action(function (string $videoId, string $protocolId, string $renditionId, string $segmentId, Response $response, Database $dbForProject, Device $deviceVideos) { $segment = Authorization::skip(fn() => $dbForProject->getDocument('videos_renditions_segments', $segmentId)); + if ($segment->isEmpty()) { throw new Exception(Exception::VIDEO_RENDITION_SEGMENT_NOT_FOUND); } diff --git a/app/init.php b/app/init.php index 22aba21edc..8f9500d9a1 100644 --- a/app/init.php +++ b/app/init.php @@ -884,7 +884,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) { $projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', 'console')); - $projectId = '637b63377afb0ffd80c0'; + //$projectId = '637e1b81cfbec5916845'; if ($projectId === 'console') { return $console; } diff --git a/app/views/videos/dash.phtml b/app/views/videos/dash.phtml index c9f9b34db7..2e4ec549d3 100644 --- a/app/views/videos/dash.phtml +++ b/app/views/videos/dash.phtml @@ -11,7 +11,7 @@ echo ''; ?> $representation = $adaptation['representation']; ?> id="" contentType="" startWithSAP="" segmentAlignment="" bitstreamSwitching="" frameRate="" maxWidth="" par="" lang=""> - id="" mimeType="" codecs="" width="" height="" sar="" audioSamplingRate=""> + id="" mimeType="" codecs="" bandwidth="" width="" height="" sar="" audioSamplingRate=""> diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index ca4ec8c639..031aa2be75 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -1,9 +1,10 @@ video = new Document($this->args['video']); + $this->profile = new Document($this->args['profile']); + $this->project = new Document($this->args['project']); - $this->basePath .= $this->args['videoId'] . '/' . $this->args['profileId']; + console::info('Receiving videoId [' . $this->video->getId() . '] of size [' . $this->video['size'] . ']'); + + $this->basePath .= $this->video->getId() . '/' . $this->profile->getId(); $this->inDir = $this->basePath . '/in/'; $this->outDir = $this->basePath . '/out/'; @mkdir($this->inDir, 0755, true); @mkdir($this->outDir, 0755, true); - $this->outPath = $this->outDir . $this->args['videoId']; + $this->outPath = $this->outDir . $this->video->getId(); } public function run(): void { - $project = new Document($this->args['project']); - $this->database = $this->getProjectDB($project->getId()); - - $sourceVideo = $this->database->findOne('videos', [ - Query::equal('_uid', [$this->args['videoId']]), - ]); - - if (empty($sourceVideo)) { - throw new Exception(Exception::VIDEO_NOT_FOUND); - } - - $profile = $this->database->findOne('videos_profiles', [ - Query::equal('_uid', [$this->args['profileId']]), - ]); - - if (empty($profile)) { - throw new Exception(Exception::VIDEO_PROFILE_NOT_FOUND); - } - - $bucket = $this->database->getDocument('buckets', $sourceVideo->getAttribute('bucketId')); - $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $sourceVideo->getAttribute('fileId')); + $startTime = \microtime(true); + $this->database = $this->getProjectDB($this->project->getId()); + $bucket = $this->database->getDocument('buckets', $this->video->getAttribute('bucketId')); + $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $this->video->getAttribute('fileId')); $path = basename($file->getAttribute('path')); $inPath = $this->inDir . $path; - $result = $this->writeData($project, $file); + $result = $this->writeData($this->project, $file); + + console::info('Transferring video from storage to [' . $this->inDir . ']'); if (empty($result)) { - throw new Exception(Exception::GENERAL_UNKNOWN); + console::error('Storage transfer error'); } - $ffprobe = FFMpeg\FFProbe::create(); - $ffmpeg = Streaming\FFMpeg::create([ + $this->ffprobe = FFProbe::create(); + $this->ffmpeg = FFMpeg::create([ 'timeout' => 0, 'ffmpeg.threads' => 12 ]); - if (!$ffprobe->isValid($inPath)) { - throw new Exception('Not an valid Video file "' . $inPath . '"'); + if (!$this->ffprobe->isValid($inPath)) { + console::error('Not an valid Video file "' . $inPath . '"'); } - foreach ($ffprobe->streams($inPath)->audios()->getIterator() as $stream) { + foreach ($this->ffprobe->streams($inPath)->audios()->getIterator() as $stream) { if (!empty($stream->get('tags')['language'])) { $this->audioTracks[] = $stream->get('tags')['language']; } } - $audioStreamCount = $ffprobe->streams($inPath)->audios()->count(); - $videoStreamCount = $ffprobe->streams($inPath)->videos()->count(); - $streams = $ffprobe->streams($inPath); + $audioStreamCount = $this->ffprobe->streams($inPath)->audios()->count(); + $videoStreamCount = $this->ffprobe->streams($inPath)->videos()->count(); + $streams = $this->ffprobe->streams($inPath); - $sourceVideo + $this->video ->setAttribute('duration', $videoStreamCount > 0 ? $streams->videos()->first()->get('duration') : null) ->setAttribute('height', $videoStreamCount > 0 ? $streams->videos()->first()->get('height') : null) ->setAttribute('width', $videoStreamCount > 0 ? $streams->videos()->first()->get('width') : null) @@ -133,34 +133,28 @@ class TranscodingV1 extends Worker ->setAttribute('audioBitrate', $audioStreamCount > 0 ? $streams->audios()->first()->get('bit_rate') : null) ; - $this->database->updateDocument( - 'videos', - $sourceVideo->getId(), - $sourceVideo - ); + $this->database->updateDocument('videos', $this->video->getId(), $this->video); - $video = $ffmpeg->open($inPath); - $this->setRenditionName($profile); + $media = $this->ffmpeg->open($inPath); + $this->setRenditionName($this->profile); $subs = []; $subtitles = $this->database->find('videos_subtitles', [ - Query::equal('videoId', [$this->args['videoId']]), + Query::equal('videoId', [$this->video->getId()]), Query::equal('status', ['']), ]); foreach ($subtitles as $subtitle) { $subtitle->setAttribute('status', self::STATUS_START); - $this->database->updateDocument( - 'videos_subtitles', - $subtitle->getId(), - $subtitle - ); + $this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle); $bucket = $this->database->getDocument('buckets', $subtitle->getAttribute('bucketId')); - $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId')); - + $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $subtitle->getAttribute('fileId')); $path = basename($file->getAttribute('path')); - $this->writeData($project, $file); + $this->writeData($this->project, $file); + + console::info('Transferring subtitle from storage to [' . $this->inDir . ']'); + $ext = pathinfo($path, PATHINFO_EXTENSION); $subtitlePath = $this->inDir . $subtitle->getId() . '.vtt'; @@ -177,44 +171,47 @@ class TranscodingV1 extends Worker } $query = $this->database->createDocument('videos_renditions', new Document([ - 'videoId' => $this->args['videoId'], - 'profileId' => $profile->getId(), + 'videoId' => $this->video->getId(), + 'profileId' => $this->profile->getId(), 'name' => $this->getRenditionName(), 'startedAt' => DateTime::now(), 'status' => self::STATUS_START, - 'protocol' => $profile->getAttribute('protocol'), + 'protocol' => $this->profile->getAttribute('protocol'), ])); - $renditionRootPath = $this->getVideoDevice($project->getId())->getPath($this->args['videoId']) . '/'; + $renditionRootPath = $this->getVideoDevice($this->project->getId())->getPath($this->video->getId()) . '/'; $renditionPath = $renditionRootPath . $this->getRenditionName() . '-' . $query->getId() . '/'; try { $representation = (new Representation()) - ->setKiloBitrate($profile->getAttribute('videoBitrate')) - ->setAudioKiloBitrate($profile->getAttribute('audioBitrate')) - ->setResize($profile->getAttribute('width'), $profile->getAttribute('height')) + ->setKiloBitrate($this->profile->getAttribute('videoBitrate')) + ->setAudioKiloBitrate($this->profile->getAttribute('audioBitrate')) + ->setResize($this->profile->getAttribute('width'), $this->profile->getAttribute('height')) ; + console::info('Setting video bitrate to [' . $this->profile->getAttribute('videoBitrate') . ']'); + console::info('Setting audio bitrate to [' . $this->profile->getAttribute('audioBitrate') . ']'); + console::info('Setting resolution to [' . $this->profile->getAttribute('width') . 'X' . $this->profile->getAttribute('height') . ']'); $format = new Streaming\Format\X264(); - $format->on('progress', function ($video, $format, $percentage) use ($query) { + $format->on('progress', function ($media, $format, $percentage) use ($query) { if ($percentage % 3 === 0) { $query->setAttribute('progress', (string)$percentage); - $this->database->updateDocument( - 'videos_renditions', - $query->getId(), - $query - ); + $this->database->updateDocument('videos_renditions', $query->getId(), $query); } }); - $general = $this->transcode($profile->getAttribute('protocol'), $video, $format, $representation, $subs); + $general = $this->transcode($this->profile->getAttribute('protocol'), $media, $format, $representation, $subs); if (!empty($general)) { foreach ($general as $key => $value) { $query->setAttribute($key, (string)$value); } } + unset($media); + //exec('/usr/bin/ffmpeg -y -i /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/in/637f59ab5bce0e36d05e.mp4 -c:v libx264 -c:a aac -bf 1 -keyint_min 25 -g 250 -sc_threshold 40 -use_timeline 0 -use_template 0 -seg_duration 10 -hls_playlist 0 -f dash -dn -sn -vf scale=iw:-2:force_original_aspect_ratio=increase,setsar=1:1 -b_strategy 1 -bf 3 -force_key_frames "expr:gte(t,n_forced*2)" -map 0 -s:v:0 1024x576 -b:v:0 2538k -b:a:0 128k -strict -2 -threads 12 /usr/src/code/tests/tmp/637f59c88f9ff0fe3b1f/637e1b82aeab8980400e/out/637f59c88f9ff0fe3b1f.mpd2>&1', $o, $v); + //var_dump($o); + //var_dump($v); - if ($profile->getAttribute('protocol') === self::PROTOCOL_HLS) { + if ($this->profile->getAttribute('protocol') === self::PROTOCOL_HLS) { $streams = $this->getHlsSegmentsUrls($this->outDir . 'master.m3u8'); foreach ($streams as $stream) { $m3u8 = $this->getHlsSegments($this->outDir . $stream['path']); @@ -257,7 +254,7 @@ class TranscodingV1 extends Worker $this->database->updateDocument('videos_renditions', $query->getId(), $query); foreach ($subtitles ?? [] as $subtitle) { - if ($profile->getAttribute('protocol') === 'hls') { + if ($this->profile->getAttribute('protocol') === 'hls') { $m3u8 = $this->getHlsSegments($this->outPath . '_subtitles_' . $subtitle['code'] . '.m3u8'); foreach ($m3u8['segments'] ?? [] as $segment) { $this->database->createDocument('videos_subtitles_segments', new Document([ @@ -269,19 +266,15 @@ class TranscodingV1 extends Worker } $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())); + $this->getFilesDevice($this->project->getId())->transfer($this->inDir . $subtitle->getId() . '.vtt', $this->outDir . $subtitle->getId() . '.vtt', $this->getFilesDevice($this->project->getId())); } $subtitle->setAttribute('status', self::STATUS_READY); $subtitle->setAttribute('path', $renditionRootPath); - $this->database->updateDocument( - 'videos_subtitles', - $subtitle->getId(), - $subtitle - ); + $this->database->updateDocument('videos_subtitles', $subtitle->getId(), $subtitle); } - /** Upload & cleanup **/ + /** Upload & cleanup **/ $start = 0; $fileNames = scandir($this->outDir); foreach ($fileNames as $fileName) { @@ -290,66 +283,67 @@ class TranscodingV1 extends Worker continue; } - $data = $this->getFilesDevice($project->getId())->read($this->outDir . $fileName); + $data = $this->getFilesDevice($this->project->getId())->read($this->outDir . $fileName); $to = $renditionPath; if (str_contains($fileName, "_subtitles_") || str_contains($fileName, ".vtt")) { $to = $renditionRootPath; } - $this->getVideoDevice($project->getId())->write($to . $fileName, $data, \mime_content_type($this->outDir . $fileName)); + $this->getVideoDevice($this->project->getId())->write($to . $fileName, $data, \mime_content_type($this->outDir . $fileName)); if ($start === 0) { $query->setAttribute('progress', '100'); $query->setAttribute('status', self::STATUS_UPLOADING); $query->setAttribute('path', $renditionPath); $this->database->updateDocument('videos_renditions', $query->getId(), $query); $start = 1; + + console::info('Uploading to [' . $to . ']'); } - //@unlink($this->outDir . $fileName); + @unlink($this->outDir . $fileName); } $query->setAttribute('status', self::STATUS_READY); $this->database->updateDocument('videos_renditions', $query->getId(), $query); + + Console::info('Process lasted ' . (microtime(true) - $startTime) . ' seconds'); } catch (\Throwable $th) { $query->setAttribute('metadata', json_encode([ - 'code' => $th->getCode(), - 'message' => substr($th->getMessage(), 0, 255), + 'code' => $th->getCode(), + 'message' => substr($th->getMessage(), 0, 255), ])); $query->setAttribute('status', self::STATUS_ERROR); $this->database->updateDocument('videos_renditions', $query->getId(), $query); - throw new Exception($th->getMessage(), 500, Exception::GENERAL_UNKNOWN); + console::error('Transcoding general error'); } } /** * @param string $protocol - * @param $video Media + * @param $media Media * @param $format StreamFormat * @param $representation Representation * @param array $subtitles * @return string|array */ - private function transcode(string $protocol, Media $video, StreamFormat $format, Representation $representation, array $subtitles): string | array + private function transcode(string $protocol, Media $media, StreamFormat $format, Representation $representation, array $subtitles): string | array { -// $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', + //'-r', '24', '-b_strategy', '1', - '-bf', '3', - '-g', '120' + '-bf', '3', // bframe + //'-g', '120' // gop + '-force_key_frames', 'expr:gte(t,n_forced*2)' //enforce strict key frame ]; - $segmentSize = 8; + $segmentSize = 10; if ($protocol === self::PROTOCOL_DASH) { - $dash = $video->dash() + $dash = $media->dash() ->setFormat($format) ->setSegDuration($segmentSize) ->addRepresentation($representation) @@ -357,10 +351,12 @@ class TranscodingV1 extends Worker ->save($this->outPath) ; + console::info(strtoupper($protocol) . ' rendition conversion ended'); + return $this->getVideoStreamInfo($dash->metadata()->export(), $representation); } - $hls = $video->hls(); + $hls = $media->hls(); foreach ($subtitles as $subtitle) { $sub = new HLSSubtitle($subtitle['path'], $subtitle['name'], $subtitle['code']); @@ -377,8 +373,11 @@ class TranscodingV1 extends Worker ->save($this->outPath) ; + console::info(strtoupper($protocol) . ' rendition conversion ended'); + return $this->getVideoStreamInfo($hls->metadata()->export(), $representation); } + /** * @param string $path * @return array @@ -438,7 +437,7 @@ class TranscodingV1 extends Worker } $end = strpos($line, 'm3u8'); if ($end !== false) { - $start = strpos($line, $this->args['videoId']); + $start = strpos($line, $this->video->getId()); if ($start !== false) { $path = substr($line, $start, ($end - $start) + 4); $parts = explode('_', $path); @@ -461,7 +460,6 @@ class TranscodingV1 extends Worker return $files; } - /** * @param string $path * @return array @@ -507,23 +505,21 @@ class TranscodingV1 extends Worker { $info = []; -// if (!empty($metadata['stream']['resolutions'][0])) { -// $general = $metadata['stream']['resolutions'][0]; -// $info['resolution'] = $general['dimension']; -// } - $info['width'] = $representation->getWidth(); - $info['height'] = $representation->getHeight(); + $general = $metadata['stream']['resolutions'][0] ?? []; + $parts = explode('X', $general['dimension']); + $info['width'] = $parts[0] ?? $representation->getWidth(); + $info['height'] = isset($parts[1]) ?? $representation->getHeight(); + $info['videoBitrate'] = isset($general['video_kilo_bitrate']) ?? '0'; + $info['audioBitrate'] = isset($general['audio_kilo_bitrate']) ?? '0'; 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']) ? $streams['bit_rate'] : $representation->getKiloBitrate(); $info['videoFramerate'] = !empty($streams['avg_frame_rate']) ? $streams['avg_frame_rate'] : '0'; } elseif ($streams['codec_type'] === 'audio') { $info['audioCodec'] = !empty($streams['codec_name']) ? $streams['codec_name'] : '' ; $info['audioSamplerate'] = !empty($streams['sample_rate']) ? $streams['sample_rate'] : '0'; - $info['audioBitrate'] = !empty($streams['bit_rate']) ? $streams['bit_rate'] : $representation->getAudioKiloBitrate(); } } return $info; @@ -542,10 +538,9 @@ class TranscodingV1 extends Worker if ( !empty($file->getAttribute('openSSLCipher')) || - !empty($file->getAttribute('algorithm', '')) + $file->getAttribute('algorithm', 'none') !== 'none' ) { $data = $this->getFilesDevice($project->getId())->read($fullPath); - if (!empty($file->getAttribute('openSSLCipher'))) { $data = OpenSSL::decrypt( $data, @@ -596,13 +591,19 @@ class TranscodingV1 extends Worker private function cleanup(): bool { - var_dump("rm -rf {$this->basePath}"); - //return \exec("rm -rf {$this->basePath}"); - return true; + $stdout = ''; + $stderr = ''; + $stdin = ''; + + return Console::execute("rm -rf {$this->basePath}", $stdin, $stdout, $stderr, 3) === 0; } public function shutdown(): void { - $this->cleanup(); + $result = $this->cleanup(); + if (!$result) { + Console::error('Failed Removing files from [' . $this->basePath . ']'); + } + Console::info('Removing files from [' . $this->basePath . ']'); } } diff --git a/docker-compose.yml b/docker-compose.yml index 4b172b243c..0e2abacc64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -238,7 +238,10 @@ services: - appwrite-uploads:/storage/uploads:rw - appwrite-videos:/storage/videos:rw #- ./tests:/usr/src/code/tests #TODO remove when done!! - #- ./vendor:/usr/src/code/vendor #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/utopia-php/storage:/usr/src/code/vendor/utopia-php/storage depends_on: - redis diff --git a/src/Appwrite/Event/Transcoding.php b/src/Appwrite/Event/Transcoding.php index 4cc0ecfdfa..ef8b9244a9 100644 --- a/src/Appwrite/Event/Transcoding.php +++ b/src/Appwrite/Event/Transcoding.php @@ -7,8 +7,9 @@ use Utopia\Database\Document; class Transcoding extends Event { - protected string $videoId = ''; - protected string $profileId = ''; + protected Document $video; + + protected Document $profile; public function __construct() { @@ -16,49 +17,49 @@ class Transcoding extends Event } /** - * Sets videoId event. + * Sets video. * - * @param $videoId string + * @param Document $video * @return self */ - public function setVideoId(string $videoId): self + public function setVideo(Document $video): self { - $this->videoId = $videoId; + $this->video = $video; return $this; } /** - * Returns bucketId. + * Returns video. * * @return null|Document */ - public function getVideoId(): ?string + public function getVideo(): ?string { - return $this->videoId; + return $this->video; } /** - * Sets profileId event. + * Sets profile. * - * @param $profileId string + * @param Document $profile * @return self */ - public function setProfileId(string $profileId): self + public function setProfile(Document $profile): self { - $this->profileId = $profileId; + $this->profile = $profile; return $this; } /** - * Returns profileId. + * Returns profile. * * @return null|Document */ - public function getProfileId(): ?string + public function getProfile(): ?Document { - return $this->profileId; + return $this->profile; } /** @@ -72,8 +73,8 @@ class Transcoding extends Event return Resque::enqueue($this->queue, $this->class, [ 'project' => $this->project, 'user' => $this->user, - 'videoId' => $this->videoId, - 'profileId' => $this->profileId, + 'video' => $this->video, + 'profile' => $this->profile, ]); } } diff --git a/tests/e2e/Services/Videos/VideoCustomServerTest.php b/tests/e2e/Services/Videos/VideoCustomServerTest.php index 0fcce6e87d..06b4024790 100644 --- a/tests/e2e/Services/Videos/VideoCustomServerTest.php +++ b/tests/e2e/Services/Videos/VideoCustomServerTest.php @@ -7,7 +7,6 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\VideoCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; -use Tests\E2E\Services\Videos\VideoPermissionsScope; use Utopia\Database\Permission; use Utopia\Database\Role; @@ -16,7 +15,6 @@ class VideoCustomServerTest extends Scope use ProjectCustom; use VideoCustom; use SideServer; - use VideoPermissionsScope; public function testCreateProfile(): string { @@ -139,8 +137,6 @@ class VideoCustomServerTest extends Scope 'fileId' => $fileId ]); - var_dump($response['body']); - exit; } /** @@ -522,7 +518,7 @@ class VideoCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/rendition/' . $renditionId, [ + $response = $this->client->call(Client::METHOD_GET, '/videos/' . $videoId . '/renditions/' . $renditionId, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'],