diff --git a/README.md b/README.md
index ddea07f23..7b0561592 100644
--- a/README.md
+++ b/README.md
@@ -1002,6 +1002,7 @@ Want to add your own open-source project to this list? [Click here!](https://doc
* Whether the user will receive notifications when contacts sign up: account.getContactSignUpNotification
* Whether this is altervista: isAltervista
* Whether we can convert any audio/video file to a VoIP OGG OPUS file, or the files must be preconverted using @libtgvoipbot: canConvertOgg
+ * Whether we can convert any audio/video file using ffmpeg: canUseFFmpeg
* Whether we're an IPC client instance: isIpc
* Whether we're an IPC server process (as opposed to an event handler): isIpcWorker
* Whether we're currently connected to the test DCs: isTestMode
diff --git a/examples/bot.php b/examples/bot.php
index ea22cb369..0dcc15188 100755
--- a/examples/bot.php
+++ b/examples/bot.php
@@ -35,6 +35,7 @@ use danog\MadelineProto\EventHandler\Plugin\RestartPlugin;
use danog\MadelineProto\EventHandler\SimpleFilter\FromAdmin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\EventHandler\SimpleFilter\IsReply;
+use danog\MadelineProto\LocalFile;
use danog\MadelineProto\Logger;
use danog\MadelineProto\ParseMode;
use danog\MadelineProto\RemoteUrl;
@@ -103,6 +104,9 @@ class MyEventHandler extends SimpleEventHandler
$this->logger($this->getFullInfo('MadelineProto'));
$this->sendMessageToAdmins("The bot was started!");
+
+ $this->sendAudio(peer: 'danogentili', file: new LocalFile('/home/daniil/Music/01. Madge - 2WORLDS.flac'));
+ $this->sendVideo(peer: 'danogentili', file: new LocalFile('/home/daniil/m.mp4'));
}
/**
diff --git a/src/InternalDoc.php b/src/InternalDoc.php
index 67d355b0b..754961dd3 100644
--- a/src/InternalDoc.php
+++ b/src/InternalDoc.php
@@ -1741,9 +1741,9 @@ abstract class InternalDoc
* @param ?Cancellation $cancellation Cancellation.
*
*/
- final public function sendGif(string|int $peer, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream $file, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream|null $thumb = null, string $caption = '', \danog\MadelineProto\ParseMode $parseMode = \danog\MadelineProto\ParseMode::TEXT, ?callable $callback = null, ?string $fileName = null, ?int $ttl = null, bool $spoiler = false, ?int $replyToMsgId = null, ?int $topMsgId = null, ?array $replyMarkup = null, string|int|null $sendAs = null, ?int $scheduleDate = null, bool $silent = false, bool $noForwards = false, bool $background = false, bool $clearDraft = false, bool $forceResend = false, ?\Amp\Cancellation $cancellation = null): \danog\MadelineProto\EventHandler\Message
+ final public function sendGif(string|int $peer, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream $file, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream|null $thumb = null, string $caption = '', \danog\MadelineProto\ParseMode $parseMode = \danog\MadelineProto\ParseMode::TEXT, ?callable $callback = null, ?string $fileName = null, ?int $ttl = null, bool $spoiler = false, ?int $duration = null, ?int $width = null, ?int $height = null, string $thumbSeek = '00:00:01.000', ?int $replyToMsgId = null, ?int $topMsgId = null, ?array $replyMarkup = null, string|int|null $sendAs = null, ?int $scheduleDate = null, bool $silent = false, bool $noForwards = false, bool $background = false, bool $clearDraft = false, bool $forceResend = false, ?\Amp\Cancellation $cancellation = null): \danog\MadelineProto\EventHandler\Message
{
- return $this->wrapper->getAPI()->sendGif($peer, $file, $thumb, $caption, $parseMode, $callback, $fileName, $ttl, $spoiler, $replyToMsgId, $topMsgId, $replyMarkup, $sendAs, $scheduleDate, $silent, $noForwards, $background, $clearDraft, $forceResend, $cancellation);
+ return $this->wrapper->getAPI()->sendGif($peer, $file, $thumb, $caption, $parseMode, $callback, $fileName, $ttl, $spoiler, $duration, $width, $height, $thumbSeek, $replyToMsgId, $topMsgId, $replyMarkup, $sendAs, $scheduleDate, $silent, $noForwards, $background, $clearDraft, $forceResend, $cancellation);
}
/**
* Sends a message.
@@ -1873,9 +1873,9 @@ abstract class InternalDoc
* @param Cancellation $cancellation Cancellation.
*
*/
- final public function sendVideo(string|int $peer, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream $file, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream|null $thumb = null, string $caption = '', \danog\MadelineProto\ParseMode $parseMode = \danog\MadelineProto\ParseMode::TEXT, ?callable $callback = null, ?string $fileName = null, string $mimeType = 'video/mp4', ?int $ttl = null, bool $spoiler = false, bool $roundMessage = false, bool $supportsStreaming = true, bool $noSound = false, ?int $duration = null, ?int $width = null, ?int $height = null, ?int $replyToMsgId = null, ?int $topMsgId = null, ?array $replyMarkup = null, string|int|null $sendAs = null, ?int $scheduleDate = null, bool $silent = false, bool $noForwards = false, bool $background = false, bool $clearDraft = false, bool $forceResend = false, bool $updateStickersetsOrder = false, ?\Amp\Cancellation $cancellation = null): \danog\MadelineProto\EventHandler\Message
+ final public function sendVideo(string|int $peer, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream $file, \danog\MadelineProto\EventHandler\Message|\danog\MadelineProto\EventHandler\Media|\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\danog\MadelineProto\BotApiFileId|\Amp\ByteStream\ReadableStream|null $thumb = null, string $caption = '', \danog\MadelineProto\ParseMode $parseMode = \danog\MadelineProto\ParseMode::TEXT, ?callable $callback = null, ?string $fileName = null, string $mimeType = 'video/mp4', ?int $ttl = null, bool $spoiler = false, bool $roundMessage = false, bool $supportsStreaming = true, bool $noSound = false, ?int $duration = null, ?int $width = null, ?int $height = null, string $thumbSeek = '00:00:01.000', ?int $replyToMsgId = null, ?int $topMsgId = null, ?array $replyMarkup = null, string|int|null $sendAs = null, ?int $scheduleDate = null, bool $silent = false, bool $noForwards = false, bool $background = false, bool $clearDraft = false, bool $forceResend = false, bool $updateStickersetsOrder = false, ?\Amp\Cancellation $cancellation = null): \danog\MadelineProto\EventHandler\Message
{
- return $this->wrapper->getAPI()->sendVideo($peer, $file, $thumb, $caption, $parseMode, $callback, $fileName, $mimeType, $ttl, $spoiler, $roundMessage, $supportsStreaming, $noSound, $duration, $width, $height, $replyToMsgId, $topMsgId, $replyMarkup, $sendAs, $scheduleDate, $silent, $noForwards, $background, $clearDraft, $forceResend, $updateStickersetsOrder, $cancellation);
+ return $this->wrapper->getAPI()->sendVideo($peer, $file, $thumb, $caption, $parseMode, $callback, $fileName, $mimeType, $ttl, $spoiler, $roundMessage, $supportsStreaming, $noSound, $duration, $width, $height, $thumbSeek, $replyToMsgId, $topMsgId, $replyMarkup, $sendAs, $scheduleDate, $silent, $noForwards, $background, $clearDraft, $forceResend, $updateStickersetsOrder, $cancellation);
}
/**
* Sends a voice.
diff --git a/src/MTProtoTools/FilesAbstraction.php b/src/MTProtoTools/FilesAbstraction.php
index 8ff2cbfa7..076d261ce 100644
--- a/src/MTProtoTools/FilesAbstraction.php
+++ b/src/MTProtoTools/FilesAbstraction.php
@@ -20,12 +20,10 @@ declare(strict_types=1);
namespace danog\MadelineProto\MTProtoTools;
+use Amp\ByteStream\Pipe;
use Amp\ByteStream\ReadableBuffer;
use Amp\ByteStream\ReadableStream;
use Amp\Cancellation;
-use Amp\CompositeCancellation;
-use Amp\DeferredCancellation;
-use Amp\NullCancellation;
use Amp\Process\Process;
use AssertionError;
use danog\MadelineProto\BotApiFileId;
@@ -45,14 +43,14 @@ use danog\MadelineProto\LocalFile;
use danog\MadelineProto\ParseMode;
use danog\MadelineProto\RemoteUrl;
use danog\MadelineProto\Settings;
+use danog\MadelineProto\StreamDuplicator;
use danog\MadelineProto\TL\Types\Bytes;
use danog\MadelineProto\Tools;
+use finfo;
use Webmozart\Assert\Assert;
use function Amp\async;
use function Amp\ByteStream\buffer;
-use function Amp\ByteStream\pipe;
-use function Amp\ByteStream\splitLines;
use function Amp\Future\await;
/**
@@ -117,7 +115,7 @@ trait FilesAbstraction
type: Document::class,
mimeType: $mimeType,
thumb: $thumb,
- attributes: [],
+ attributesOrig: [],
peer: $peer,
file: $file,
caption: $caption,
@@ -190,7 +188,7 @@ trait FilesAbstraction
type: Photo::class,
mimeType: 'image/jpeg',
thumb: null,
- attributes: [],
+ attributesOrig: [],
peer: $peer,
file: $file,
caption: $caption,
@@ -261,7 +259,7 @@ trait FilesAbstraction
type: Sticker::class,
mimeType: $mimeType,
thumb: null,
- attributes: [],
+ attributesOrig: [],
peer: $peer,
file: $file,
caption: $emoji,
@@ -334,6 +332,7 @@ trait FilesAbstraction
?int $duration = null,
?int $width = null,
?int $height = null,
+ string $thumbSeek = '00:00:01.000',
?int $replyToMsgId = null,
?int $topMsgId = null,
?array $replyMarkup = null,
@@ -351,13 +350,14 @@ trait FilesAbstraction
type: Video::class,
mimeType: $mimeType,
thumb: $thumb,
- attributes: [
+ attributesOrig: [
'round_message' => $roundMessage,
'supports_streaming' => $supportsStreaming,
'no_sound' => $noSound,
'duration' => $duration,
'w' => $width,
'h' => $height,
+ 'thumbSeek' => $thumbSeek,
],
peer: $peer,
file: $file,
@@ -418,6 +418,10 @@ trait FilesAbstraction
?string $fileName = null,
?int $ttl = null,
bool $spoiler = false,
+ ?int $duration = null,
+ ?int $width = null,
+ ?int $height = null,
+ string $thumbSeek = '00:00:01.000',
?int $replyToMsgId = null,
?int $topMsgId = null,
?array $replyMarkup = null,
@@ -434,7 +438,15 @@ trait FilesAbstraction
type: Gif::class,
mimeType: 'video/mp4',
thumb: $thumb,
- attributes: [],
+ attributesOrig: [
+ 'round_message' => false,
+ 'supports_streaming' => true,
+ 'no_sound' => true,
+ 'duration' => $duration,
+ 'w' => $width,
+ 'h' => $height,
+ 'thumbSeek' => $thumbSeek,
+ ],
peer: $peer,
file: $file,
caption: $caption,
@@ -514,7 +526,7 @@ trait FilesAbstraction
type: Audio::class,
mimeType: $mimeType,
thumb: $thumb,
- attributes: [
+ attributesOrig: [
'duration' => $duration,
'title' => $title,
'performer' => $performer,
@@ -599,7 +611,7 @@ trait FilesAbstraction
type: Voice::class,
mimeType: 'audio/ogg',
thumb: null,
- attributes: $attributes,
+ attributesOrig: $attributes,
peer: $peer,
file: $file,
caption: $caption,
@@ -634,7 +646,7 @@ trait FilesAbstraction
?string $mimeType,
Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file,
Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream|null $thumb,
- array $attributes,
+ array $attributesOrig,
string $caption,
ParseMode $parseMode,
?callable $callback,
@@ -703,22 +715,22 @@ trait FilesAbstraction
'_' => 'documentAttributeVideo',
'round_message' => $file instanceof RoundVideo
? true
- : $attributes['round_message'],
+ : $attributesOrig['round_message'],
'supports_streaming' => $file instanceof AbstractVideo
? $file->supportsStreaming
- : $attributes['supports_streaming'],
+ : $attributesOrig['supports_streaming'],
'no_sound' => $file instanceof Gif
? true
- : $attributes['no_sound'],
+ : $attributesOrig['no_sound'],
'duration' => $file instanceof AbstractVideo
? $file->duration
- : $attributes['duration'],
+ : $attributesOrig['duration'],
'w' => $file instanceof AbstractVideo
? $file->width
- : $attributes['w'],
+ : $attributesOrig['w'],
'h' => $file instanceof AbstractVideo
? $file->height
- : $attributes['h'],
+ : $attributesOrig['h'],
],
],
Audio::class => [
@@ -726,13 +738,13 @@ trait FilesAbstraction
'_' => 'documentAttributeAudio',
'duration' => $file instanceof Audio
? $file->duration
- : $attributes['duration'],
+ : $attributesOrig['duration'],
'title' => $file instanceof Audio
? $file->title
- : $attributes['title'],
+ : $attributesOrig['title'],
'performer' => $file instanceof Audio
? $file->performer
- : $attributes['performer'],
+ : $attributesOrig['performer'],
],
],
Voice::class => [
@@ -741,10 +753,10 @@ trait FilesAbstraction
'voice' => true,
'duration' => $file instanceof Voice
? $file->duration
- : $attributes['duration'],
+ : $attributesOrig['duration'],
'waveform' => $file instanceof Voice
? $file->waveform
- : $attributes['waveform'],
+ : $attributesOrig['waveform'],
],
],
default => [],
@@ -875,79 +887,12 @@ trait FilesAbstraction
if ($reuseId) {
// Reuse
} elseif ($type === Video::class || $type === Gif::class) {
- if (!Tools::canUseFFmpeg($cancellation)) {
- $this->logger->logger('Install ffmpeg for video info extraction!');
- } elseif ($thumb === null || $attributes[0]['duration'] === null || $attributes[0]['w'] === null || $attributes[0]['h'] === null) {
- $dl = new DeferredCancellation;
- $copy = $this->getStream($file, new CompositeCancellation($dl->getCancellation(), $cancellation ?? new NullCancellation));
- $ffmpeg = 'ffmpeg -i pipe: -ss 00:00:01.000 -frames:v 1 -f image2pipe -vcodec mjpeg pipe:1';
- $process = Process::start($ffmpeg, cancellation: $cancellation);
- $stdin = $process->getStdin();
- async(pipe(...), $copy, $stdin, $cancellation)->finally(static function () use ($stdin, $dl): void {
- $stdin->close();
- $dl->cancel();
- })->ignore();
- [$stdout, $stderr] = await([
- async(buffer(...), $process->getStdout(), $cancellation),
- async(buffer(...), $process->getStderr(), $cancellation),
- ]);
- $thumb ??= new ReadableBuffer($stdout);
- $process->join($cancellation);
-
- if (preg_match('~Duration: (\d{2}:\d{2}:\d{2}\.\d{2}),.*? (\d{3,4})x(\d{3,4})~s', $stderr, $matches)) {
- $time = explode(':', $matches[1]);
- $hours = (int) $time[0];
- $minutes = (int) $time[1];
- $seconds = (int) $time[2];
- $duration = $hours * 3600 + $minutes * 60 + $seconds;
- $width = $matches[2];
- $height = $matches[3];
- $attributes[0]['w'] ??= $width;
- $attributes[0]['h'] ??= $height;
- $attributes[0]['duration'] ??= $duration;
- }
- }
+ $this->extractVideoInfo($attributesOrig['thumbSeek'], $file, $fileName, $callback, $cancellation, $mimeType, $attributes, $thumb);
} elseif ($type === Audio::class || $type === Voice::class) {
- if (!Tools::canUseFFmpeg($cancellation)) {
- $this->logger->logger('Install ffmpeg for audio info extraction!');
- } elseif ($attributes[0]['duration'] === null || $attributes[0]['title'] === null || $attributes[0]['performer'] === null) {
- $dl = new DeferredCancellation;
- $copy = $this->getStream($file, new CompositeCancellation($dl->getCancellation(), $cancellation ?? new NullCancellation));
- // Todo: cover
- $ffmpeg = 'ffmpeg -i pipe: -f ffmetadata -';
- $process = Process::start($ffmpeg, cancellation: $cancellation);
- $stdin = $process->getStdin();
- $stdout = $process->getStdout();
- async(pipe(...), $copy, $stdin, $cancellation)->finally(static function () use ($stdin, $dl): void {
- $stdin->close();
- $dl->cancel();
- })->ignore();
- [$result, $stderr] = await([
- async(static function () use ($stdout, $cancellation): array {
- $result = [];
- foreach (splitLines($stdout, $cancellation) as $line) {
- if (!str_contains($line, '=')) {
- continue;
- }
- [$k, $v] = explode("=", $line, 2);
- $result[strtolower($k)] = $v;
- }
- return $result;
- }),
- async(buffer(...), $process->getStderr(), $cancellation),
- ]);
- $process->join($cancellation);
- if (preg_match('~Duration: (\d{2}:\d{2}:\d{2}\.\d{2})~', $stderr, $matches)) {
- $time = explode(':', $matches[1]);
- $hours = (int) $time[0];
- $minutes = (int) $time[1];
- $seconds = (int) $time[2];
- $duration = $hours * 3600 + $minutes * 60 + $seconds;
- $attributes[0]['duration'] = $duration;
- }
- $attributes[0]['title'] ??= $result['title'] ?? null;
- $attributes[0]['performer'] ??= $result['artist'] ?? null;
- }
+ $this->extractAudioInfo($file, $fileName, $callback, $cancellation, $mimeType, $attributes, $thumb);
+
+ } elseif ($mimeType === null) {
+ $mimeType = $this->extractMime($file, $fileName, $callback, $cancellation);
}
$method = 'messages.sendMedia';
@@ -1014,7 +959,7 @@ trait FilesAbstraction
default => 'inputMediaDocument',
};
$media['id'] = $reuseId;
- } else {
+ } elseif (!\is_array($media['file'])) {
$media['file'] = $this->upload($media['file'], $fileName ?? '', $callback, cancellation: $cancellation);
}
@@ -1044,4 +989,153 @@ trait FilesAbstraction
\assert($res !== null);
return $res;
}
+
+ /**
+ * @return list{array, string}
+ */
+ private function extractMime(Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream $file, ?string $fileName, ?callable $callback, ?Cancellation $cancellation): array
+ {
+ $file = $this->getStream($file, $cancellation);
+ $p = new Pipe(1024*1024);
+ $fileFuture = async(fn () => $this->upload(new StreamDuplicator($file, $p->getSink()), $fileName ?? '', $callback, cancellation: $cancellation));
+
+ $buff = '';
+ while (\strlen($buff) < 1024*1024 && null !== $chunk = $p->getSource()->read($cancellation)) {
+ $buff .= $chunk;
+ }
+ $p->getSink()->close();
+ $p->getSource()->close();
+ unset($p);
+
+ return [$fileFuture->await(), (new finfo())->buffer($buff, FILEINFO_MIME_TYPE)];
+ }
+ private function extractAudioInfo(Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream &$file, ?string $fileName, ?callable $callback, ?Cancellation $cancellation, ?string &$mimeType, array &$attributes, mixed &$thumb): void
+ {
+ if (!Tools::canUseFFmpeg($cancellation)) {
+ $this->logger->logger('Install ffmpeg for audio info extraction!');
+ if ($mimeType === null) {
+ [$file, $mimeType] = $this->extractMime($file, $fileName, $callback, $cancellation);
+ }
+ return;
+ }
+ if (!(
+ $attributes[0]['duration'] === null
+ || $attributes[0]['title'] === null
+ || $attributes[0]['performer'] === null
+ || $thumb === null
+ )) {
+ return;
+ }
+
+ $file = $this->getStream($file, $cancellation);
+ $process = Process::start('ffmpeg -i pipe: -f image2pipe -', cancellation: $cancellation);
+ $stdin = $process->getStdin();
+ $stdout = $process->getStdout();
+ $p = new Pipe(1024*1024);
+ $fileFuture = async(fn () => $this->upload(new StreamDuplicator($file, $stdin, $p->getSink()), $fileName ?? '', $callback, cancellation: $cancellation));
+
+ $f = [
+ async(buffer(...), $process->getStdout(), $cancellation),
+ async(buffer(...), $process->getStderr(), $cancellation),
+ ];
+ if ($mimeType === null) {
+ $f []= async(static function () use ($p, $cancellation, &$mimeType): void {
+ $buff = '';
+ while (\strlen($buff) < 1024*1024 && null !== $chunk = $p->getSource()->read($cancellation)) {
+ $buff .= $chunk;
+ }
+ $p->getSink()->close();
+ $p->getSource()->close();
+ unset($p);
+
+ $mimeType ??= (new finfo())->buffer($buff, FILEINFO_MIME_TYPE);
+ });
+ }
+ [$stdout, $stderr] = await($f);
+
+ $process->join($cancellation);
+ if (preg_match('~Duration: (\d{2}:\d{2}:\d{2}\.\d{2})~', $stderr, $matches)) {
+ $time = explode(':', $matches[1]);
+ $hours = (int) $time[0];
+ $minutes = (int) $time[1];
+ $seconds = (int) $time[2];
+ $duration = $hours * 3600 + $minutes * 60 + $seconds;
+ $attributes[0]['duration'] = $duration;
+ }
+ if (preg_match('/TITLE\s*:\s*(.+)/', $stderr, $matches)) {
+ $attributes[0]['title'] ??= $matches[1];
+ }
+ if (preg_match('/ARTIST\s*:\s*(.+)/', $stderr, $matches)) {
+ $attributes[0]['performer'] ??= $matches[1];
+ }
+ if ($stdout !== '') {
+ // Todo check if jpg, but should be jpg in most cases anyway
+ $thumb ??= new ReadableBuffer($stdout);
+ }
+
+ $file = $fileFuture->await();
+ }
+
+ private function extractVideoInfo(string $thumbSeek, Message|Media|LocalFile|RemoteUrl|BotApiFileId|ReadableStream &$file, ?string $fileName, ?callable $callback, ?Cancellation $cancellation, ?string &$mimeType, array &$attributes, mixed &$thumb): void
+ {
+ if (!Tools::canUseFFmpeg($cancellation)) {
+ $this->logger->logger('Install ffmpeg for video info extraction!');
+ if ($mimeType === null) {
+ [$file, $mimeType] = $this->extractMime($file, $fileName, $callback, $cancellation);
+ }
+ return;
+ }
+ if (!(
+ $thumb === null
+ || $attributes[0]['duration'] === null
+ || $attributes[0]['w'] === null
+ || $attributes[0]['h'] === null
+ )) {
+ return;
+ }
+
+ $file = $this->getStream($file, $cancellation);
+ $ffmpeg = 'ffmpeg -i pipe: -ss '.$thumbSeek.' -frames:v 1 -f image2pipe -';
+ $process = Process::start($ffmpeg, cancellation: $cancellation);
+ $stdin = $process->getStdin();
+ $p = new Pipe(1024*1024);
+ $fileFuture = async(fn () => $this->upload(new StreamDuplicator($file, $stdin, $p->getSink()), $fileName ?? '', $callback, cancellation: $cancellation));
+
+ $f = [
+ async(buffer(...), $process->getStdout(), $cancellation),
+ async(buffer(...), $process->getStderr(), $cancellation),
+ ];
+ if ($mimeType === null) {
+ $f []= async(static function () use ($p, $cancellation, &$mimeType): void {
+ $buff = '';
+ while (\strlen($buff) < 1024*1024 && null !== $chunk = $p->getSource()->read($cancellation)) {
+ $buff .= $chunk;
+ }
+ $p->getSink()->close();
+ $p->getSource()->close();
+ unset($p);
+
+ $mimeType ??= (new finfo())->buffer($buff, FILEINFO_MIME_TYPE);
+ });
+ }
+ [$stdout, $stderr] = await($f);
+
+ $thumb ??= new ReadableBuffer($stdout);
+ $process->join($cancellation);
+
+ if (preg_match('~Duration: (\d{2}:\d{2}:\d{2}\.\d{2}),.*? (\d{3,4})x(\d{3,4})~s', $stderr, $matches)) {
+ $time = explode(':', $matches[1]);
+ $hours = (int) $time[0];
+ $minutes = (int) $time[1];
+ $seconds = (int) $time[2];
+ $duration = $hours * 3600 + $minutes * 60 + $seconds;
+ $width = $matches[2];
+ $height = $matches[3];
+ $attributes[0]['w'] ??= $width;
+ $attributes[0]['h'] ??= $height;
+ $attributes[0]['duration'] ??= $duration;
+ }
+
+ $file = $fileFuture->await();
+ }
}