1
0
mirror of https://github.com/danog/MadelineProto.git synced 2025-01-22 19:11:16 +01:00

Improve API

This commit is contained in:
Daniil Gentili 2023-08-14 17:16:59 +02:00
parent 23702168b1
commit 4b9ade0fea
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
7 changed files with 133 additions and 103 deletions

View File

@ -33,6 +33,7 @@ use danog\MadelineProto\EventHandler\Message\Service\DialogPhotoChanged;
use danog\MadelineProto\EventHandler\SimpleFilter\FromAdmin; use danog\MadelineProto\EventHandler\SimpleFilter\FromAdmin;
use danog\MadelineProto\EventHandler\SimpleFilter\Incoming; use danog\MadelineProto\EventHandler\SimpleFilter\Incoming;
use danog\MadelineProto\EventHandler\SimpleFilter\IsReply; use danog\MadelineProto\EventHandler\SimpleFilter\IsReply;
use danog\MadelineProto\LocalFile;
use danog\MadelineProto\Logger; use danog\MadelineProto\Logger;
use danog\MadelineProto\ParseMode; use danog\MadelineProto\ParseMode;
use danog\MadelineProto\Settings; use danog\MadelineProto\Settings;
@ -295,13 +296,13 @@ class MyEventHandler extends SimpleEventHandler
#[FilterCommand('call')] #[FilterCommand('call')]
public function callVoip(Incoming&Message $message): void public function callVoip(Incoming&Message $message): void
{ {
$this->requestCall($message->senderId)->play(__DIR__.'/../music.ogg'); $this->requestCall($message->senderId)->play(new LocalFile(__DIR__.'/../music.ogg'));
} }
#[Handler] #[Handler]
public function handleIncomingCall(VoIP&Incoming $call): void public function handleIncomingCall(VoIP&Incoming $call): void
{ {
$call->accept()->play(__DIR__.'/../music.ogg'); $call->accept()->play(new LocalFile(__DIR__.'/../music.ogg'));
} }
public static function getPluginPaths(): string|array|null public static function getPluginPaths(): string|array|null

View File

@ -1121,6 +1121,7 @@
<file src="src/MTProto.php"> <file src="src/MTProto.php">
<DocblockTypeContradiction> <DocblockTypeContradiction>
<code><![CDATA[\is_object($this->datacenter)]]></code> <code><![CDATA[\is_object($this->datacenter)]]></code>
<code><![CDATA[isset($this->TL)]]></code>
<code><![CDATA[isset($this->settings)]]></code> <code><![CDATA[isset($this->settings)]]></code>
<code><![CDATA[isset($this->settings)]]></code> <code><![CDATA[isset($this->settings)]]></code>
<code><![CDATA[new DataCenter($this, $this->dcList, $this->settings->getConnection())]]></code> <code><![CDATA[new DataCenter($this, $this->dcList, $this->settings->getConnection())]]></code>
@ -1186,7 +1187,6 @@
</PossiblyInvalidArgument> </PossiblyInvalidArgument>
<PossiblyInvalidArrayAccess> <PossiblyInvalidArrayAccess>
<code><![CDATA[$this->getSelf()['bot']]]></code> <code><![CDATA[$this->getSelf()['bot']]]></code>
<code><![CDATA[$this->getSelf()['id']]]></code>
<code><![CDATA[$this->getSelf()['premium']]]></code> <code><![CDATA[$this->getSelf()['premium']]]></code>
</PossiblyInvalidArrayAccess> </PossiblyInvalidArrayAccess>
<PossiblyNullArrayAccess> <PossiblyNullArrayAccess>
@ -1228,7 +1228,6 @@
</RedundantConditionGivenDocblockType> </RedundantConditionGivenDocblockType>
<UndefinedThisPropertyFetch> <UndefinedThisPropertyFetch>
<code><![CDATA[$this->full_chats]]></code> <code><![CDATA[$this->full_chats]]></code>
<code><![CDATA[$this->usernames]]></code>
</UndefinedThisPropertyFetch> </UndefinedThisPropertyFetch>
<UnsupportedReferenceUsage> <UnsupportedReferenceUsage>
<code><![CDATA[Lang::$current_lang =& Lang::$lang[$this->settings->getAppInfo()->getLangCode()]]]></code> <code><![CDATA[Lang::$current_lang =& Lang::$lang[$this->settings->getAppInfo()->getLangCode()]]]></code>
@ -1456,7 +1455,6 @@
</PossiblyNullArgument> </PossiblyNullArgument>
<PossiblyUndefinedArrayOffset> <PossiblyUndefinedArrayOffset>
<code><![CDATA[$_SERVER['REQUEST_METHOD']]]></code> <code><![CDATA[$_SERVER['REQUEST_METHOD']]]></code>
<code><![CDATA[$_SERVER['REQUEST_METHOD']]]></code>
</PossiblyUndefinedArrayOffset> </PossiblyUndefinedArrayOffset>
<PossiblyUndefinedMethod> <PossiblyUndefinedMethod>
<code>read</code> <code>read</code>

View File

@ -1313,6 +1313,15 @@ abstract class InternalDoc
{ {
return \danog\MadelineProto\StrTools::mbSubstr($text, $offset, $length); return \danog\MadelineProto\StrTools::mbSubstr($text, $offset, $length);
} }
/**
* Provide a buffered reader for a file, URL or amp stream.
*
* @return Closure(int, ?Cancellation): ?string
*/
public static function openBuffered(\danog\MadelineProto\LocalFile|\danog\MadelineProto\RemoteUrl|\Amp\ByteStream\ReadableStream $stream): \Closure
{
return \danog\MadelineProto\Tools::openBuffered($stream);
}
/** /**
* Opens a file in append-only mode. * Opens a file in append-only mode.
* *
@ -1481,7 +1490,7 @@ abstract class InternalDoc
* *
* @param mixed $user User * @param mixed $user User
*/ */
public function requestCall(mixed $user) public function requestCall(mixed $user): \danog\MadelineProto\VoIP
{ {
return $this->wrapper->getAPI()->requestCall($user); return $this->wrapper->getAPI()->requestCall($user);
} }

View File

@ -20,14 +20,8 @@ namespace danog\MadelineProto;
use Amp\ByteStream\ReadableStream; use Amp\ByteStream\ReadableStream;
use Amp\ByteStream\WritableStream; use Amp\ByteStream\WritableStream;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use AssertionError; use AssertionError;
use danog\MadelineProto\Stream\BufferedStreamInterface; use Closure;
use danog\MadelineProto\Stream\BufferInterface;
use danog\MadelineProto\Stream\Common\SimpleBufferedRawStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\Stream\Transport\PremadeStream;
use FFI; use FFI;
use FFI\CData; use FFI\CData;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
@ -204,11 +198,6 @@ final class Ogg
*/ */
private int $streamCount; private int $streamCount;
/**
* Buffered stream interface.
*/
private BufferInterface $stream;
/** /**
* Pack format. * Pack format.
*/ */
@ -222,13 +211,15 @@ final class Ogg
public readonly iterable $opusPackets; public readonly iterable $opusPackets;
/** /**
* Constructor. * @var (Closure(int, ?Cancellation): ?string) $stream The stream
*
* @param BufferedStreamInterface $stream The stream
*/ */
public function __construct(BufferedStreamInterface $stream) private readonly Closure $stream;
/**
* Constructor.
*/
public function __construct(LocalFile|RemoteUrl|ReadableStream $stream)
{ {
$this->stream = $stream->getReadBuffer($l); $this->stream = Tools::openBuffered($stream);
$pack_format = [ $pack_format = [
'stream_structure_version' => 'C', 'stream_structure_version' => 'C',
'header_type_flag' => 'C', 'header_type_flag' => 'C',
@ -355,6 +346,9 @@ final class Ogg
/** @psalm-suppress InvalidArgument */ /** @psalm-suppress InvalidArgument */
$this->opusPayload .= \substr($content, $preOffset, (int) (($offset - $preOffset) + $sum + $paddingLen)); $this->opusPayload .= \substr($content, $preOffset, (int) (($offset - $preOffset) + $sum + $paddingLen));
if ($this->currentDuration === 60_000) { if ($this->currentDuration === 60_000) {
if (($s = \strlen($this->opusPayload)) > 1024) {
throw new AssertionError("Encountered a packet with size $s > 1024, please convert the audio files using Ogg::convert to avoid issues with packet size!");
}
yield $this->opusPayload; yield $this->opusPayload;
$this->opusPayload = ''; $this->opusPayload = '';
$this->currentDuration = 0; $this->currentDuration = 0;
@ -376,6 +370,9 @@ final class Ogg
if (\strlen($this->opusPayload) !== \strlen($content)) { if (\strlen($this->opusPayload) !== \strlen($content)) {
throw new AssertionError(); throw new AssertionError();
} }
if (($s = \strlen($this->opusPayload)) > 1024) {
throw new AssertionError("Encountered a packet with size $s > 1024, please convert the audio files using Ogg::convert to avoid issues with packet size!");
}
yield $this->opusPayload; yield $this->opusPayload;
$this->opusPayload = ''; $this->opusPayload = '';
$this->currentDuration = 0; $this->currentDuration = 0;
@ -385,6 +382,14 @@ final class Ogg
} }
} }
/**
* Validate that the specified file, URL or stream is a valid VoIP OGG OPUS file.
*/
public function validate(LocalFile|RemoteUrl|ReadableStream $file): void
{
foreach ((new self($file))->opusPackets as $_) {
}
}
/** /**
* Read frames. * Read frames.
* *
@ -398,7 +403,7 @@ final class Ogg
$ignoredStreams = []; $ignoredStreams = [];
while (true) { while (true) {
$capture = $this->stream->bufferRead(4); $capture = ($this->stream)(4);
if ($capture !== self::CAPTURE_PATTERN) { if ($capture !== self::CAPTURE_PATTERN) {
if ($capture === null) { if ($capture === null) {
return; return;
@ -408,7 +413,7 @@ final class Ogg
$headers = \unpack( $headers = \unpack(
$this->packFormat, $this->packFormat,
$this->stream->bufferRead(23) ($this->stream)(23)
); );
$ignore = \in_array($headers['bitstream_serial_number'], $ignoredStreams, true); $ignore = \in_array($headers['bitstream_serial_number'], $ignoredStreams, true);
@ -424,7 +429,7 @@ final class Ogg
$segments = \unpack( $segments = \unpack(
'C*', 'C*',
$this->stream->bufferRead($headers['number_page_segments']), ($this->stream)($headers['number_page_segments']),
); );
//$serial = $headers['bitstream_serial_number']; //$serial = $headers['bitstream_serial_number'];
@ -439,7 +444,7 @@ final class Ogg
foreach ($segments as $segment_size) { foreach ($segments as $segment_size) {
$sizeAccumulated += $segment_size; $sizeAccumulated += $segment_size;
if ($segment_size < 255) { if ($segment_size < 255) {
$piece = $this->stream->bufferRead($sizeAccumulated); $piece = ($this->stream)($sizeAccumulated);
$sizeAccumulated = 0; $sizeAccumulated = 0;
if ($ignore) { if ($ignore) {
continue; continue;
@ -539,33 +544,24 @@ final class Ogg
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BANDWIDTH_REQUEST, self::OPUS_BANDWIDTH_FULLBAND)); $checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BANDWIDTH_REQUEST, self::OPUS_BANDWIDTH_FULLBAND));
$checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BITRATE_REQUEST, 130*1000)); $checkErr($opus->opus_encoder_ctl($encoder, self::OPUS_SET_BITRATE_REQUEST, 130*1000));
$in = $wavIn instanceof LocalFile $read = Tools::openBuffered($wavIn);
? openFile($wavIn->file, 'r')
: (
$wavIn instanceof RemoteUrl
? HttpClientBuilder::buildDefault()->request(new Request($wavIn->url))->getBody()
: $wavIn
);
$ctx = (new ConnectionContext())->addStream(PremadeStream::class, $in)->addStream(SimpleBufferedRawStream::class); Assert::eq($read(4), 'RIFF', "A .wav file must be provided!");
/** @var SimpleBufferedRawStream */ $totalLength = \unpack('V', $read(4))[1];
$in = $ctx->getStream(); Assert::eq($read(4), 'WAVE', "A .wav file must be provided!");
Assert::eq($in->bufferRead(length: 4), 'RIFF', "A .wav file must be provided!");
$totalLength = \unpack('V', $in->bufferRead(length: 4))[1];
Assert::eq($in->bufferRead(length: 4), 'WAVE', "A .wav file must be provided!");
do { do {
$type = $in->bufferRead(length: 4); $type = $read(4);
$length = \unpack('V', $in->bufferRead(length: 4))[1]; $length = \unpack('V', $read(4))[1];
if ($type === 'fmt ') { if ($type === 'fmt ') {
Assert::eq($length, 16); Assert::eq($length, 16);
$contents = $in->bufferRead(length: $length + ($length % 2)); $contents = $read($length + ($length % 2));
$header = \unpack('vaudioFormat/vchannels/VsampleRate/VbyteRate/vblockAlign/vbitsPerSample', $contents); $header = \unpack('vaudioFormat/vchannels/VsampleRate/VbyteRate/vblockAlign/vbitsPerSample', $contents);
Assert::eq($header['audioFormat'], 1, "The wav file must contain PCM audio"); Assert::eq($header['audioFormat'], 1, "The wav file must contain PCM audio");
Assert::eq($header['sampleRate'], 48000, "The sample rate of the wav file must be 48khz!"); Assert::eq($header['sampleRate'], 48000, "The sample rate of the wav file must be 48khz!");
} elseif ($type === 'data') { } elseif ($type === 'data') {
break; break;
} else { } else {
$in->bufferRead($length); $read($length);
} }
} while (true); } while (true);
@ -636,6 +632,7 @@ final class Ogg
$writeTag("MadelineProto ".API::RELEASE.", ".$opus->opus_get_version_string()); $writeTag("MadelineProto ".API::RELEASE.", ".$opus->opus_get_version_string());
$tags .= \pack('V', 2); $tags .= \pack('V', 2);
$writeTag("ENCODER=MadelineProto ".API::RELEASE." with ".$opus->opus_get_version_string()); $writeTag("ENCODER=MadelineProto ".API::RELEASE." with ".$opus->opus_get_version_string());
$writeTag("MADELINE_ENCODER_V=1");
$writeTag('See https://docs.madelineproto.xyz/docs/CALLS.html for more info'); $writeTag('See https://docs.madelineproto.xyz/docs/CALLS.html for more info');
$writePage( $writePage(
0, 0,
@ -648,7 +645,7 @@ final class Ogg
$granule = 0; $granule = 0;
$buf = FFI::cast(FFI::type('char*'), FFI::addr($opus->new('char[1024]'))); $buf = FFI::cast(FFI::type('char*'), FFI::addr($opus->new('char[1024]')));
do { do {
$chunkOrig = $in->bufferRead(length: $chunkSize); $chunkOrig = $read($chunkSize);
$chunk = \str_pad($chunkOrig, $chunkSize, "\0"); $chunk = \str_pad($chunkOrig, $chunkSize, "\0");
$granuleDiff = \strlen($chunk) >> $shift; $granuleDiff = \strlen($chunk) >> $shift;
$len = $opus->opus_encode($encoder, $chunk, $granuleDiff, $buf, 1024); $len = $opus->opus_encode($encoder, $chunk, $granuleDiff, $buf, 1024);

View File

@ -21,7 +21,11 @@ declare(strict_types=1);
namespace danog\MadelineProto; namespace danog\MadelineProto;
use Amp\ByteStream\ReadableBuffer; use Amp\ByteStream\ReadableBuffer;
use Amp\ByteStream\ReadableStream;
use Amp\Cancellation;
use Amp\File\File; use Amp\File\File;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use ArrayAccess; use ArrayAccess;
use Closure; use Closure;
use Countable; use Countable;
@ -600,6 +604,41 @@ abstract class Tools extends AsyncTools
return openFile($path, "a"); return openFile($path, "a");
} }
/**
* Provide a buffered reader for a file, URL or amp stream.
*
* @return Closure(int, ?Cancellation): ?string
*/
public static function openBuffered(LocalFile|RemoteUrl|ReadableStream $stream): Closure
{
if ($stream instanceof LocalFile) {
$stream = openFile($stream->file, 'r');
return fn (int $len, ?Cancellation $cancellation = null): ?string => $stream->read(cancellation: $cancellation, length: $len);
}
if ($stream instanceof RemoteUrl) {
$stream = HttpClientBuilder::buildDefault()->request(new Request($stream->url))->getBody();
}
$buffer = '';
return function (int $len, ?Cancellation $cancellation = null) use (&$buffer, $stream): ?string {
if ($buffer === null) {
return null;
}
do {
if (\strlen($buffer) >= $len) {
$piece = \substr($buffer, 0, $len);
$buffer = \substr($buffer, $len);
return $piece;
}
$chunk = $stream->read($cancellation);
if ($chunk === null) {
$buffer = null;
return null;
}
$buffer .= $chunk;
} while (true);
};
}
private const BLOCKING_FUNCTIONS = [ private const BLOCKING_FUNCTIONS = [
'file_get_contents' => 'https://github.com/amphp/file, https://github.com/amphp/http-client or $this->fileGetContents()', 'file_get_contents' => 'https://github.com/amphp/file, https://github.com/amphp/http-client or $this->fileGetContents()',
'file_put_contents' => 'https://github.com/amphp/file', 'file_put_contents' => 'https://github.com/amphp/file',

View File

@ -55,7 +55,7 @@ trait AuthKeyHandler
* *
* @param mixed $user User * @param mixed $user User
*/ */
public function requestCall(mixed $user) public function requestCall(mixed $user): VoIP
{ {
$user = ($this->getInfo($user)); $user = ($this->getInfo($user));
if (!isset($user['InputUser']) || $user['InputUser']['_'] === 'inputUserSelf') { if (!isset($user['InputUser']) || $user['InputUser']['_'] === 'inputUserSelf') {

View File

@ -19,12 +19,7 @@
namespace danog\MadelineProto; namespace danog\MadelineProto;
use Amp\ByteStream\ReadableStream; use Amp\ByteStream\ReadableStream;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use AssertionError;
use danog\MadelineProto\MTProtoTools\Crypt; use danog\MadelineProto\MTProtoTools\Crypt;
use danog\MadelineProto\Stream\Common\FileBufferedStream;
use danog\MadelineProto\Stream\ConnectionContext;
use danog\MadelineProto\VoIP\CallState; use danog\MadelineProto\VoIP\CallState;
use danog\MadelineProto\VoIP\DiscardReason; use danog\MadelineProto\VoIP\DiscardReason;
use danog\MadelineProto\VoIP\Endpoint; use danog\MadelineProto\VoIP\Endpoint;
@ -37,7 +32,6 @@ use Throwable;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use function Amp\delay; use function Amp\delay;
use function Amp\File\openFile;
/** @internal */ /** @internal */
final class VoIPController final class VoIPController
@ -104,9 +98,9 @@ final class VoIPController
private array $call; private array $call;
/** @var array<string|LocalFile|RemoteUrl|ReadableStream> */ /** @var array<LocalFile|RemoteUrl|ReadableStream> */
private array $holdFiles = []; private array $holdFiles = [];
/** @var list<string|LocalFile|RemoteUrl|ReadableStream> */ /** @var list<LocalFile|RemoteUrl|ReadableStream> */
private array $inputFiles = []; private array $inputFiles = [];
private int $holdIndex = 0; private int $holdIndex = 0;
@ -468,26 +462,25 @@ final class VoIPController
} }
private bool $muted = false; private bool $muted = false;
private bool $playingHold = false; private bool $playingHold = false;
private function pullPacket(): bool private function pullPacket(): ?string
{ {
$file = \array_shift($this->inputFiles); if ($this->packetQueue->isEmpty()) {
if ($file) { $file = \array_shift($this->inputFiles);
$this->playingHold = false; if ($file) {
} else { $this->playingHold = false;
$this->playingHold = true; } else {
if (!$this->holdFiles) { $this->playingHold = true;
return false; if (!$this->holdFiles) {
return null;
}
$file = $this->holdFiles[($this->holdIndex++) % \count($this->holdFiles)];
} }
$file = $this->holdFiles[($this->holdIndex++) % \count($this->holdFiles)]; $it = new Ogg($file);
} foreach ($it->opusPackets as $packet) {
$it = $this->openFile($file); $this->packetQueue->enqueue($packet);
foreach ($it->opusPackets as $packet) {
if (($s = \strlen($packet)) > 1024) {
throw new AssertionError("Encountered a packet with size $s > 1024, please convert the audio files using Ogg::convert to avoid issues with packet size!");
} }
$this->packetQueue->enqueue($packet);
} }
return false; return $this->packetQueue->dequeue();
} }
/** /**
* Start write loop. * Start write loop.
@ -506,26 +499,15 @@ final class VoIPController
$delay = $this->muted ? 0.2 : 0.06; $delay = $this->muted ? 0.2 : 0.06;
$t = \microtime(true) + $delay; $t = \microtime(true) + $delay;
while (true) { while (true) {
if ($this->packetQueue->isEmpty() && !$this->pullPacket()) { if ($packet = $this->pullPacket()) {
if (!$this->muted) {
$this->bestEndpoint->writeReliably([
'_' => self::PKT_STREAM_STATE,
'id' => 0,
'enabled' => false
]);
$this->muted = true;
$delay = 0.2;
}
$packet = $this->messageHandler->encryptPacket([
'_' => self::PKT_NOP
]);
} else {
if ($this->muted) { if ($this->muted) {
$this->bestEndpoint->writeReliably([ if (!$this->bestEndpoint->writeReliably([
'_' => self::PKT_STREAM_STATE, '_' => self::PKT_STREAM_STATE,
'id' => 0, 'id' => 0,
'enabled' => true 'enabled' => true
]); ])) {
return;
}
$this->muted = false; $this->muted = false;
$delay = 0.06; $delay = 0.06;
$this->opusTimestamp = 0; $this->opusTimestamp = 0;
@ -533,19 +515,38 @@ final class VoIPController
$packet = $this->messageHandler->encryptPacket([ $packet = $this->messageHandler->encryptPacket([
'_' => self::PKT_STREAM_DATA, '_' => self::PKT_STREAM_DATA,
'stream_id' => 0, 'stream_id' => 0,
'data' => $this->packetQueue->dequeue(), 'data' => $packet,
'timestamp' => $this->opusTimestamp 'timestamp' => $this->opusTimestamp
]); ]);
$this->opusTimestamp += 60; $this->opusTimestamp += 60;
} else {
if (!$this->muted) {
if (!$this->bestEndpoint->writeReliably([
'_' => self::PKT_STREAM_STATE,
'id' => 0,
'enabled' => false
])) {
return;
}
$this->muted = true;
$delay = 0.2;
}
$packet = $this->messageHandler->encryptPacket([
'_' => self::PKT_NOP
]);
} }
//Logger::log("Writing {$this->opusTimestamp} in $this!"); //Logger::log("Writing {$this->opusTimestamp} in $this!");
$cur = \microtime(true); $cur = \microtime(true);
$diff = $t - $cur; $diff = $t - $cur;
if ($diff > 0) { if ($diff > 0) {
delay($diff); delay($diff);
} else {
EventLoop::queue(Logger::log(...), "We're late while sending audio data!");
} }
$this->bestEndpoint->write($packet); if (!$this->bestEndpoint->write($packet)) {
return;
}
if ($diff > 0) { if ($diff > 0) {
$cur += $diff; $cur += $diff;
@ -555,21 +556,6 @@ final class VoIPController
$t += $delay; $t += $delay;
} }
} }
/**
* Open OGG file for reading.
*/
private function openFile(string|LocalFile|RemoteUrl|ReadableStream $file): Ogg
{
$ctx = new ConnectionContext;
$ctx->addStream(FileBufferedStream::class, match (true) {
\is_string($file) => openFile($file, 'r'),
$file instanceof LocalFile => openFile($file->file, 'r'),
$file instanceof RemoteUrl => HttpClientBuilder::buildDefault()->request(new Request($file->url))->getBody(),
$file instanceof ReadableStream => $file,
});
$stream = $ctx->getStream();
return new Ogg($stream);
}
/** /**
* Play file. * Play file.
*/ */