From d0189b1ef2c20ea299bf1d4af298a7ec6bef62c1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 26 Oct 2020 21:38:23 +0100 Subject: [PATCH] Add OGG OPUS parser --- .../Stream/Common/FileBufferedStream.php | 206 ++++++++ .../Stream/Common/SimpleBufferedRawStream.php | 3 +- src/danog/MadelineProto/Stream/Ogg/Ogg.php | 335 +++++++++++++ src/danog/MadelineProto/VoIP.php | 386 +++++++++++++++ src/danog/MadelineProto/VoIP/AckHandler.php | 73 +++ .../MadelineProto/VoIP/MessageHandler.php | 459 ++++++++++++++++++ 6 files changed, 1460 insertions(+), 2 deletions(-) create mode 100644 src/danog/MadelineProto/Stream/Common/FileBufferedStream.php create mode 100644 src/danog/MadelineProto/Stream/Ogg/Ogg.php create mode 100644 src/danog/MadelineProto/VoIP.php create mode 100644 src/danog/MadelineProto/VoIP/AckHandler.php create mode 100644 src/danog/MadelineProto/VoIP/MessageHandler.php diff --git a/src/danog/MadelineProto/Stream/Common/FileBufferedStream.php b/src/danog/MadelineProto/Stream/Common/FileBufferedStream.php new file mode 100644 index 00000000..8b9758ef --- /dev/null +++ b/src/danog/MadelineProto/Stream/Common/FileBufferedStream.php @@ -0,0 +1,206 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\Stream\Common; + +use Amp\ByteStream\ClosedException; +use Amp\File\File; +use Amp\Promise; +use Amp\Socket\Socket; +use Amp\Success; +use danog\MadelineProto\Exception; +use danog\MadelineProto\Stream\Async\RawStream; +use danog\MadelineProto\Stream\BufferedStreamInterface; +use danog\MadelineProto\Stream\BufferInterface; +use danog\MadelineProto\Stream\ConnectionContext; +use danog\MadelineProto\Stream\ProxyStreamInterface; +use danog\MadelineProto\Stream\RawStreamInterface; + +/** + * Buffered raw stream. + * + * @author Daniil Gentili + */ +class FileBufferedStream implements BufferedStreamInterface, BufferInterface, ProxyStreamInterface, RawStreamInterface +{ + private File $stream; + private int $append_after; + private string $append; + /** + * Connect + * + * @param ConnectionContext $ctx + * @param string $header + * @return \Generator + */ + public function connect(ConnectionContext $ctx, string $header = ''): \Generator + { + if ($header !== '') { + yield $this->stream->write($header); + } + } + /** + * Async chunked read. + * + * @return Promise + */ + public function read(): Promise + { + if (!$this->stream) { + throw new ClosedException("MadelineProto stream was disconnected"); + } + return $this->stream->read(); + } + /** + * Async write. + * + * @param string $data Data to write + * + * @return Promise + */ + public function write(string $data): Promise + { + if (!$this->stream) { + throw new ClosedException("MadelineProto stream was disconnected"); + } + return $this->stream->write($data); + } + /** + * Async write. + * + * @param string $data Data to write + * + * @return Promise + */ + public function end(string $finalData = ''): Promise + { + if (!$this->stream) { + throw new ClosedException("MadelineProto stream was disconnected"); + } + return $this->stream->end($finalData); + } + /** + * Async close. + * + * @return void + */ + public function disconnect() + { + if ($this->stream) { + $this->stream = null; + } + } + /** + * Get read buffer asynchronously. + * + * @param int $length Length of payload, as detected by this layer + * + * @return Promise + */ + public function getReadBuffer(&$length): Promise + { + if (!$this->stream) { + throw new ClosedException("MadelineProto stream was disconnected"); + } + return new \Amp\Success($this); + } + /** + * Get write buffer asynchronously. + * + * @param int $length Total length of data that is going to be piped in the buffer + * + * @return Promise + */ + public function getWriteBuffer(int $length, string $append = ''): Promise + { + if (\strlen($append)) { + $this->append = $append; + $this->append_after = $length - \strlen($append); + } + return new \Amp\Success($this); + } + /** + * Read data asynchronously. + * + * @param int $length Amount of data to read + * + * @return Promise + */ + public function bufferRead(int $length): Promise + { + return $this->stream->read($length); + } + /** + * Async write. + * + * @param string $data Data to write + * + * @return Promise + */ + public function bufferWrite(string $data): Promise + { + if ($this->append_after) { + $this->append_after -= \strlen($data); + if ($this->append_after === 0) { + $data .= $this->append; + $this->append = ''; + } elseif ($this->append_after < 0) { + $this->append_after = 0; + $this->append = ''; + throw new Exception('Tried to send too much out of frame data, cannot append'); + } + } + return $this->write($data); + } + /** + * Set file handle + * + * @param File $extra + * @return void + */ + public function setExtra($extra) + { + $this->stream = $extra; + } + /** + * {@inheritDoc} + * + * @return RawStreamInterface + */ + public function getStream(): RawStreamInterface + { + throw new \RuntimeException("Can't get underlying RawStreamInterface, is a File handle!"); + } + /** + * {@inheritDoc} + */ + public function getSocket(): Socket + { + throw new \RuntimeException("Can't get underlying socket, is a File handle!"); + } + /** + * Get class name. + * + * @return string + */ + public static function getName(): string + { + return __CLASS__; + } +} \ No newline at end of file diff --git a/src/danog/MadelineProto/Stream/Common/SimpleBufferedRawStream.php b/src/danog/MadelineProto/Stream/Common/SimpleBufferedRawStream.php index edd9f716..f8890838 100644 --- a/src/danog/MadelineProto/Stream/Common/SimpleBufferedRawStream.php +++ b/src/danog/MadelineProto/Stream/Common/SimpleBufferedRawStream.php @@ -24,7 +24,7 @@ use danog\MadelineProto\Stream\BufferInterface; use danog\MadelineProto\Stream\RawStreamInterface; /** - * Buffered raw stream. + * Buffered raw stream, that simply returns less data on EOF instead of throwing. * * @author Daniil Gentili */ @@ -48,7 +48,6 @@ class SimpleBufferedRawStream extends BufferedRawStream implements BufferedStrea while ($buffer_length < $length) { $chunk = yield $this->read(); if ($chunk === null) { - \fseek($this->memory_stream, $offset); break; } \fwrite($this->memory_stream, $chunk); diff --git a/src/danog/MadelineProto/Stream/Ogg/Ogg.php b/src/danog/MadelineProto/Stream/Ogg/Ogg.php new file mode 100644 index 00000000..344e566e --- /dev/null +++ b/src/danog/MadelineProto/Stream/Ogg/Ogg.php @@ -0,0 +1,335 @@ + + * @author Daniil Gentili + */ +class Ogg +{ + private const CAPTURE_PATTERN = "\x4f\x67\x67\x53"; // ASCII encoded "OggS" string + private const BOS = 2; + private const EOS = 4; + + const STATE_READ_HEADER = 0; + const STATE_READ_COMMENT = 1; + const STATE_STREAMING = 3; + const STATE_END = 4; + + /** + * Required frame duration in microseconds. + */ + private int $frameDuration = 60000; + /** + * Current total frame duration in microseconds. + */ + private int $currentDuration = 0; + + /** + * Current OPUS payload. + */ + private string $opusPayload = ''; + + /** + * OGG Stream count. + */ + private int $streamCount; + + /** + * Buffered stream interface. + */ + private BufferInterface $stream; + + /** + * Pack format. + */ + private string $packFormat; + + /** + * OPUS packet emitter. + */ + private Emitter $emitter; + + private function __construct() {} + /** + * Constructor. + * + * @param BufferedStreamInterface $stream The stream + * @param int $frameDuration Required frame duration, microseconds + * + * @return \Generator + * @psalm-return \Generator + */ + public static function init(BufferedStreamInterface $stream, int $frameDuration): \Generator + { + $self = new self; + $self->frameDuration = $frameDuration; + $self->stream = yield $stream->getReadBuffer($l); + $self->emitter = new Emitter; + $pack_format = [ + 'stream_structure_version' => 'C', + 'header_type_flag' => 'C', + 'granule_position' => 'P', + 'bitstream_serial_number' => 'V', + 'page_sequence_number' => 'V', + 'CRC_checksum' => 'V', + 'number_page_segments' => 'C' + ]; + + $self->packFormat = \implode( + '/', + \array_map( + fn (string $v, string $k): string => $v.$k, + $pack_format, + \array_keys($pack_format) + ) + ); + + return $self; + } + + /** + * Read OPUS length. + * + * @param string $content + * @param integer $offset + * @return integer + */ + private function readLen(string $content, int &$offset): int + { + $len = \ord($content[$offset++]); + if ($len > 251) { + $len += \ord($content[$offset++]) << 2; + } + return $len; + } + /** + * OPUS state machine. + * + * @param string $content + * @return \Generator + */ + private function opusStateMachine(string $content): \Generator + { + $curStream = 0; + $offset = 0; + $len = \strlen($content); + while ($offset < $len) { + $selfDelimited = $curStream++ < $this->streamCount - 1; + $sizes = []; + + $preOffset = $offset; + + $toc = \ord($content[$offset++]); + $stereo = $toc & 4; + $conf = $toc >> 3; + $c = $toc & 3; + + if ($conf < 12) { + $frameDuration = $conf % 4; + if ($frameDuration === 0) { + $frameDuration = 10000; + } else { + $frameDuration *= 20000; + } + } elseif ($conf < 16) { + $frameDuration = 2**($conf % 2) * 10000; + } else { + $frameDuration = 2**($conf % 4) * 2500; + } + + $paddingLen = 0; + if ($c === 0) { + // Exactly 1 frame + $sizes []= $selfDelimited + ? $this->readLen($content, $offset) + : $len - $offset; + } elseif ($c === 1) { + // Exactly 2 frames, equal size + $size = $selfDelimited + ? $this->readLen($content, $offset) + : ($len - $offset)/2; + $sizes []= $size; + $sizes []= $size; + } elseif ($c === 2) { + // Exactly 2 frames, different size + $size = $this->readLen($content, $offset); + $sizes []= $size; + $sizes []= $selfDelimited + ? $this->readLen($content, $offset) + : $len - ($offset + $size); + } else { + // Arbitrary number of frames + $ch = \ord($content[$offset++]); + $len--; + $count = $ch & 0x3F; + $vbr = $ch & 0x80; + $padding = $ch & 0x40; + if ($padding) { + $paddingLen = $padding = \ord($content[$offset++]); + while ($padding === 255) { + $padding = \ord($content[$offset++]); + $paddingLen += $padding - 1; + } + } + if ($vbr) { + if (!$selfDelimited) { + $count -= 1; + } + for ($x = 0; $x < $count; $x++) { + $sizes[]= $this->readLen($content, $offset); + } + } else { // CBR + $size = $selfDelimited + ? $this->readLen($content, $offset) + : ($len - ($offset + $padding)) / $count; + \array_push($sizes, ...\array_fill(0, $count, $size)); + } + } + + $totalDuration = \count($sizes) * $frameDuration; + if (!$selfDelimited && $totalDuration + $this->currentDuration <= $this->frameDuration) { + $this->currentDuration += $totalDuration; + $sum = array_sum($sizes); + $this->opusPayload .= \substr($content, $preOffset, ($offset - $preOffset) + $sum + $paddingLen); + if ($this->currentDuration === $this->frameDuration) { + yield $this->emitter->emit($this->opusPayload); + $this->opusPayload = ''; + $this->currentDuration = 0; + } + $offset += $sum; + $offset += $paddingLen; + continue; + } + + foreach ($sizes as $size) { + $this->opusPayload .= chr($toc & ~3); + $this->opusPayload .= substr($content, $offset, $size); + $offset += $size; + $this->currentDuration += $frameDuration; + if ($this->currentDuration >= $this->frameDuration) { + if ($this->currentDuration > $this->frameDuration) { + Logger::log("Emitting packet with duration {$this->currentDuration} but need {$this->frameDuration}, please reconvert the OGG file with a proper frame size.", Logger::WARNING); + } + yield $this->emitter->emit($this->opusPayload); + $this->opusPayload = ''; + $this->currentDuration = 0; + } + } + $offset += $paddingLen; + } + } + + /** + * Read frames. + * + * @return \Generator + */ + public function read(): \Generator + { + $state = self::STATE_READ_HEADER; + $content = ''; + + while (true) { + $init = yield $this->stream->bufferRead(4+23); + if (empty($init)) { + return false; // EOF + } + if (\substr($init, 0, 4) !== self::CAPTURE_PATTERN) { + throw new Exception("Bad capture pattern"); + } + + /*$headers = \unpack( + $this->packFormat, + \substr($init, 4) + ); + + if ($headers['stream_structure_version'] != 0x00) { + throw new Exception("Bad stream version"); + } + $granule_diff = $headers['granule_position'] - $granule; + $granule = $headers['granule_position']; + + $continuation = (bool) ($headers['header_type_flag'] & 0x01); + $firstPage = (bool) ($headers['header_type_flag'] & 0x02); + $lastPage = (bool) ($headers['header_type_flag'] & 0x04); + */ + + $segments = \unpack( + 'C*', + yield $this->stream->bufferRead(\ord($init[26])) + ); + + //$serial = $headers['bitstream_serial_number']; + /*if ($headers['header_type_flag'] & Ogg::BOS) { + $this->emit('ogg:stream:start', [$serial]); + } elseif ($headers['header_type_flag'] & Ogg::EOS) { + $this->emit('ogg:stream:end', [$serial]); + } else { + $this->emit('ogg:stream:continue', [$serial]); + }*/ + $sizeAccumulated = 0; + foreach ($segments as $segment_size) { + $sizeAccumulated += $segment_size; + if ($segment_size < 255) { + $content .= yield $this->stream->bufferRead($sizeAccumulated); + if ($state === self::STATE_STREAMING) { + yield from $this->opusStateMachine($content); + } elseif ($state === self::STATE_READ_HEADER) { + if (\substr($content, 0, 8) !== 'OpusHead') { + throw new \RuntimeException("This is not an OPUS stream!"); + } + $opus_head = \unpack('Cversion/Cchannel_count/vpre_skip/Vsample_rate/voutput_gain/Cchannel_mapping_family/', \substr($content, 8)); + if ($opus_head['channel_mapping_family']) { + $opus_head['channel_mapping'] = \unpack('Cstream_count/Ccoupled_count/C*channel_mapping', \substr($content, 19)); + } else { + $opus_head['channel_mapping'] = [ + 'stream_count' => 1, + 'coupled_count' => $opus_head['channel_count'] - 1, + 'channel_mapping' => [0] + ]; + if ($opus_head['channel_count'] === 2) { + $opus_head['channel_mapping']['channel_mapping'][] = 1; + } + } + $this->streamCount = $opus_head['channel_mapping']['stream_count']; + \var_dump($opus_head); + $state = self::STATE_READ_COMMENT; + } elseif ($state === self::STATE_READ_COMMENT) { + $vendor_string_length = \unpack('V', \substr($content, 8, 4))[1]; + $result = []; + $result['vendor_string'] = \substr($content, 12, $vendor_string_length); + $comment_count = \unpack('V', \substr($content, 12+$vendor_string_length, 4))[1]; + $offset = 16+$vendor_string_length; + for ($x = 0; $x < $comment_count; $x++) { + $length = \unpack('V', \substr($content, $offset, 4))[1]; + $result['comments'][$x] = \substr($content, $offset += 4, $length); + $offset += $length; + } + $state = self::STATE_STREAMING; + } + $content = ''; + $sizeAccumulated = 0; + } + } + } + } + + /** + * Get OPUS packet emitter. + * + * @return Emitter + */ + public function getEmitter(): Emitter + { + return $this->emitter; + } +} diff --git a/src/danog/MadelineProto/VoIP.php b/src/danog/MadelineProto/VoIP.php new file mode 100644 index 00000000..ad21a430 --- /dev/null +++ b/src/danog/MadelineProto/VoIP.php @@ -0,0 +1,386 @@ +. +*/ + +namespace danog\MadelineProto; + +if (\extension_loaded('php-libtgvoip')) { + return; +} + +class VoIP extends Tools +{ + use \danog\MadelineProto\VoIP\MessageHandler; + use \danog\MadelineProto\VoIP\AckHandler; + + const PHP_LIBTGVOIP_VERSION = '1.1.2'; + const STATE_CREATED = 0; + const STATE_WAIT_INIT = 1; + const STATE_WAIT_INIT_ACK = 2; + const STATE_ESTABLISHED = 3; + const STATE_FAILED = 4; + const STATE_RECONNECTING = 5; + + const TGVOIP_ERROR_UNKNOWN = 0; + const TGVOIP_ERROR_INCOMPATIBLE = 1; + const TGVOIP_ERROR_TIMEOUT = 2; + const TGVOIP_ERROR_AUDIO_IO = 3; + + const NET_TYPE_UNKNOWN = 0; + const NET_TYPE_GPRS = 1; + const NET_TYPE_EDGE = 2; + const NET_TYPE_3G = 3; + const NET_TYPE_HSPA = 4; + const NET_TYPE_LTE = 5; + const NET_TYPE_WIFI = 6; + const NET_TYPE_ETHERNET = 7; + const NET_TYPE_OTHER_HIGH_SPEED = 8; + const NET_TYPE_OTHER_LOW_SPEED = 9; + const NET_TYPE_DIALUP = 10; + const NET_TYPE_OTHER_MOBILE = 11; + + const DATA_SAVING_NEVER = 0; + const DATA_SAVING_MOBILE = 1; + const DATA_SAVING_ALWAYS = 2; + + const PROXY_NONE = 0; + const PROXY_SOCKS5 = 1; + + const AUDIO_STATE_NONE = -1; + const AUDIO_STATE_CREATED = 0; + const AUDIO_STATE_CONFIGURED = 1; + const AUDIO_STATE_RUNNING = 2; + + const CALL_STATE_NONE = -1; + const CALL_STATE_REQUESTED = 0; + const CALL_STATE_INCOMING = 1; + const CALL_STATE_ACCEPTED = 2; + const CALL_STATE_CONFIRMED = 3; + const CALL_STATE_READY = 4; + const CALL_STATE_ENDED = 5; + + const PKT_INIT = 1; + const PKT_INIT_ACK = 2; + const PKT_STREAM_STATE = 3; + const PKT_STREAM_DATA = 4; + const PKT_UPDATE_STREAMS = 5; + const PKT_PING = 6; + const PKT_PONG = 7; + const PKT_STREAM_DATA_X2 = 8; + const PKT_STREAM_DATA_X3 = 9; + const PKT_LAN_ENDPOINT = 10; + const PKT_NETWORK_CHANGED = 11; + const PKT_SWITCH_PREF_RELAY = 12; + const PKT_SWITCH_TO_P2P = 13; + const PKT_NOP = 14; + + const TLID_DECRYPTED_AUDIO_BLOCK_HEX = 'dbf948c1'; + const TLID_SIMPLE_AUDIO_BLOCK_HEX = 'cc0d0e76'; + + const TLID_REFLECTOR_SELF_INFO_HEX = 'c01572c7'; + const TLID_REFLECTOR_PEER_INFO_HEX = '27D9371C'; + + const PROTO_ID = 'GrVP'; + + const PROTOCOL_VERSION = 3; + const MIN_PROTOCOL_VERSION = 3; + + const STREAM_TYPE_AUDIO = 1; + const STREAM_TYPE_VIDEO = 2; + + const CODEC_OPUS = 1; + + + private $TLID_DECRYPTED_AUDIO_BLOCK; + private $TLID_SIMPLE_AUDIO_BLOCK; + private $TLID_REFLECTOR_SELF_INFO; + private $TLID_REFLECTOR_PEER_INFO; + + private $MadelineProto; + public $received_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + public $remote_ack_timestamp_map = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + public $session_out_seq_no = 0; + public $session_in_seq_no = 0; + public $voip_state = 0; + public $configuration = ['endpoints' => [], 'shared_config' => []]; + public $storage = []; + public $internalStorage = []; + private $signal = 0; + private $callState; + private $callID; + private $creatorID; + private $otherID; + private $protocol; + private $visualization; + private $holdFiles = []; + private $inputFiles; + private $outputFile; + private $isPlaying = false; + + private $connection_settings = []; + private $dclist = []; + + private $datacenter; + + public function __construct(bool $creator, int $otherID, $callID, MTProto $MadelineProto, $callState, $protocol) + { + $this->creator = $creator; + $this->otherID = $otherID; + $this->callID = $callID; + $this->MadelineProto = $MadelineProto; + $this->callState = $callState; + $this->protocol = $protocol; + $this->TLID_REFLECTOR_SELF_INFO = \strrev(\hex2bin(self::TLID_REFLECTOR_SELF_INFO_HEX)); + $this->TLID_REFLECTOR_PEER_INFO = \strrev(\hex2bin(self::TLID_REFLECTOR_PEER_INFO_HEX)); + $this->TLID_DECRYPTED_AUDIO_BLOCK = \strrev(\hex2bin(self::TLID_DECRYPTED_AUDIO_BLOCK_HEX)); + $this->TLID_SIMPLE_AUDIO_BLOCK = \strrev(\hex2bin(self::TLID_SIMPLE_AUDIO_BLOCK_HEX)); + } + + public function deInitVoIPController() + { + } + + public function setVisualization($visualization) + { + $this->visualization = $visualization; + } + + public function getVisualization() + { + return $this->visualization; + } + + public function discard($reason = ['_' => 'phoneCallDiscardReasonDisconnect'], $rating = [], $debug = false) + { + if ($this->callState === self::CALL_STATE_ENDED || empty($this->configuration)) { + return false; + } + $this->deinitVoIPController(); + + return Tools::callFork($this->MadelineProto->discardCall($this->callID, $reason, $rating, $debug)); + } + + public function accept() + { + if ($this->callState !== self::CALL_STATE_INCOMING) { + return false; + } + $this->callState = self::CALL_STATE_ACCEPTED; + + Tools::call($this->MadelineProto->acceptCall($this->callID))->onResolve(function ($e, $res) { + if ($e || !$res) { + $this->discard(['_' => 'phoneCallDiscardReasonDisconnect']); + } + }); + + return $this; + } + + public function close() + { + $this->deinitVoIPController(); + } + + public function startTheMagic() + { + while (true) { + $waiting = $this->datacenter->select(); + foreach ($waiting as $dc) { + if ($packet = $this->recv_message($dc)) { + $this->handlePacket($dc, $packet); + } + } + } + return $this; + } + public function handlePacket($datacenter, $packet) + { + \var_dump($packet); + switch ($packet['_']) { + case self::PKT_INIT: + $this->voip_state = self::STATE_WAIT_INIT_ACK; + $this->send_message(['_' => self::PKT_INIT_ACK, 'protocol' => self::PROTOCOL_VERSION, 'min_protocol' => self::MIN_PROTOCOL_VERSION, 'all_streams' => [['id' => 0, 'type' => self::STREAM_TYPE_AUDIO, 'codec' => self::CODEC_OPUS, 'frame_duration' => 60, 'enabled' => 1]]], $datacenter); + //$a = fopen('paloma.opus', 'rb'); + //(new Ogg($a, [$this, 'oggCallback']))->run(); + break; + case self::PKT_INIT_ACK: + $this->voip_state = self::STATE_ESTABLISHED; + $a = \fopen('paloma.opus', 'rb'); + (new Ogg($a, [$this, 'oggCallback']))->run(); + + break; + } + } + public $timestamp = 0; + public function oggCallback($data) + { + \var_dump(\strlen($data)); + $this->send_message(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $data, 'timestamp' => $this->timestamp]); + $this->timestamp += 60; + } + public function play($file) + { + $this->inputFiles[] = $file; + + return $this; + } + + public function then($file) + { + $this->inputFiles[] = $file; + + return $this; + } + + public function playOnHold($files) + { + $this->holdFiles = $files; + + return $this; + } + + public function setOutputFile($file) + { + $this->outputFile = $file; + + return $this; + } + + public function unsetOutputFile() + { + $this->outputFile = null; + } + + public function setMadeline($MadelineProto) + { + $this->MadelineProto = $MadelineProto; + } + + public function getProtocol() + { + return $this->protocol; + } + + public function getOtherID() + { + return $this->otherID; + } + + public function getCallID() + { + return $this->callID; + } + + public function isCreator() + { + return $this->creator; + } + + public function whenCreated() + { + return isset($this->internalStorage['created']) ? $this->internalStorage['created'] : false; + } + + public function parseConfig() + { + if (\count($this->configuration['endpoints'])) { + $this->connection_settings['all'] = $this->MadelineProto->settings['connection_settings']['all']; + $this->connection_settings['all']['protocol'] = 'obfuscated2'; + $this->connection_settings['all']['timeout'] = 1; + $this->connection_settings['all']['do_not_retry'] = true; + + $test = $this->connection_settings['all']['test_mode'] ? 'test' : 'main'; + foreach ($this->configuration['endpoints'] as $endpoint) { + $this->dclist[$test]['ipv6'][$endpoint['id']] = ['ip_address' => $endpoint['ipv6'], 'port' => $endpoint['port'], 'peer_tag' => $endpoint['peer_tag']]; + $this->dclist[$test]['ipv4'][$endpoint['id']] = ['ip_address' => $endpoint['ip'], 'port' => $endpoint['port'], 'peer_tag' => $endpoint['peer_tag']]; + } + if (!isset($this->datacenter)) { + $this->datacenter = new DataCenter($this->dclist, $this->connection_settings); + } + //$this->datacenter->__construct($this->dclist, $this->connection_settings); + + foreach ($this->datacenter->get_dcs() as $new_dc) { + try { + $this->datacenter->dc_connect($new_dc); + } catch (\danog\MadelineProto\Exception $e) { + } + } + $this->init_all(); + foreach ($this->datacenter->get_dcs(false) as $new_dc) { + try { + $this->datacenter->dc_connect($new_dc); + } catch (\danog\MadelineProto\Exception $e) { + } + } + $this->init_all(); + } + } + + private function init_all() + { + $test = $this->connection_settings['all']['test_mode'] ? 'test' : 'main'; + foreach ($this->datacenter->sockets as $dc_id => $socket) { + if ($socket->auth_key === null) { + $socket->auth_key = ['id' => $this->configuration['auth_key_id'], 'auth_key' => $this->configuration['auth_key'], 'connection_inited' => false]; + } + if ($socket->type === Connection::API_ENDPOINT) { + $socket->type = Connection::VOIP_TCP_REFLECTOR_ENDPOINT; + } + if ($socket->peer_tag === null) { + switch ($socket->type) { + case Connection::VOIP_TCP_REFLECTOR_ENDPOINT: + case Connection::VOIP_UDP_REFLECTOR_ENDPOINT: + $socket->peer_tag = $this->dclist[$test]['ipv4'][$dc_id]['peer_tag']; + break; + default: + $socket->peer_tag = $this->configuration['call_id']; + } + } + //if ($this->voip_state === self::STATE_CREATED) { + $this->send_message(['_' => self::PKT_INIT, 'protocol' => self::PROTOCOL_VERSION, 'min_protocol' => self::MIN_PROTOCOL_VERSION, 'audio_streams' => [self::CODEC_OPUS], 'video_streams' => []], $dc_id); + $this->voip_state = self::STATE_WAIT_INIT; + //} + if (isset($this->datacenter->sockets[$dc_id])) { + $this->send_message(['_' => self::PKT_PING], $dc_id); + } + } + } + + public function getCallState() + { + return $this->callState; + } + + public function getVersion() + { + return 'libponyvoip-1.0'; + } + + public function getPreferredRelayID() + { + return 0; + } + + public function getLastError() + { + return ''; + } + + public function getDebugLog() + { + return ''; + } + + public function getSignalBarsCount() + { + return $this->signal; + } +} diff --git a/src/danog/MadelineProto/VoIP/AckHandler.php b/src/danog/MadelineProto/VoIP/AckHandler.php new file mode 100644 index 00000000..a37ba1c9 --- /dev/null +++ b/src/danog/MadelineProto/VoIP/AckHandler.php @@ -0,0 +1,73 @@ +. +*/ + +namespace danog\MadelineProto\VoIP; + +trait AckHandler +{ + public function seqgt($s1, $s2) + { + return $s1 > $s2; + } + public function received_packet($datacenter, $last_ack_id, $packet_seq_no, $ack_mask) + { + if ($this->seqgt($packet_seq_no, $this->session_in_seq_no)) { + $diff = $packet_seq_no - $this->session_in_seq_no; + if ($diff > 31) { + $this->received_timestamp_map = \array_fill(0, 32, 0); + } else { + $remaining = 32-$diff; + for ($x = 0; $x < $remaining; $x++) { + $this->received_timestamp_map[$diff+$x] = $this->received_timestamp_map[$x]; + } + for ($x = 1; $x < $diff; $x++) { + $this->received_timestamp_map[$x] = 0; + } + $this->received_timestamp_map[0] = \microtime(true); + } + $this->session_in_seq_no = $packet_seq_no; + } elseif (($diff = $this->session_in_seq_no - $packet_seq_no) < 32) { + if (!$this->received_timestamp_map[$diff]) { + \danog\MadelineProto\Logger::log("Got duplicate $packet_seq_no"); + return false; + } + $this->received_timestamp_map[$diff] = \microtime(true); + } else { + \danog\MadelineProto\Logger::log("Packet $packet_seq_no is out of order and too late"); + return false; + } + if ($this->seqgt($last_ack_id, $this->session_out_seq_no)) { + $diff = $last_ack_id - $this->session_out_seq_no; + if ($diff > 31) { + $this->remote_ack_timestamp_map = \array_fill(0, 32, 0); + } else { + $remaining = 32-$diff; + for ($x = 0; $x < $remaining; $x++) { + $this->remote_ack_timestamp_map[$diff+$x] = $this->remote_ack_timestamp_map[$x]; + } + for ($x = 1; $x < $diff; $x++) { + $this->remote_ack_timestamp_map[$x] = 0; + } + $this->remote_ack_timestamp_map[0] = \microtime(true); + } + $this->session_out_seq_no = $last_ack_id; + + for ($x = 1; $x < 32; $x++) { + if (!$this->remote_ack_timestamp_map[$x] && ($ack_mask >> 32-$x) & 1) { + $this->remote_ack_timestamp_map[$x] = \microtime(true); + } + } + } + return true; + } +} diff --git a/src/danog/MadelineProto/VoIP/MessageHandler.php b/src/danog/MadelineProto/VoIP/MessageHandler.php new file mode 100644 index 00000000..8d830cf3 --- /dev/null +++ b/src/danog/MadelineProto/VoIP/MessageHandler.php @@ -0,0 +1,459 @@ +. +*/ + +namespace danog\MadelineProto\VoIP; + +/** + * Manages packing and unpacking of messages, and the list of sent and received messages. + */ +trait MessageHandler +{ + public function pack_string($object) + { + $l = \strlen($object); + $concat = ''; + if ($l <= 253) { + $concat .= \chr($l); + $concat .= $object; + $concat .= \pack('@'.$this->posmod(-$l - 1, 4)); + } else { + $concat .= \chr(254); + $concat .= \substr($this->pack_signed_int($l), 0, 3); + $concat .= $object; + $concat .= \pack('@'.$this->posmod(-$l, 4)); + } + + return $concat; + } + public function unpack_string($stream) + { + $l = \ord(\stream_get_contents($stream, 1)); + if ($l > 254) { + throw new Exception(\danog\MadelineProto\Lang::$current_lang['length_too_big']); + } + if ($l === 254) { + $long_len = \unpack('V', \stream_get_contents($stream, 3).\chr(0))[1]; + $x = \stream_get_contents($stream, $long_len); + $resto = $this->posmod(-$long_len, 4); + if ($resto > 0) { + \stream_get_contents($stream, $resto); + } + } else { + $x = \stream_get_contents($stream, $l); + $resto = $this->posmod(-($l + 1), 4); + if ($resto > 0) { + \stream_get_contents($stream, $resto); + } + } + return $x; + } + public function send_message($args, $datacenter = null) + { + if ($datacenter === null) { + return $this->send_message($args, \key($this->datacenter->sockets)); + } + $message = ''; + switch ($args['_']) { + // streamTypeSimple codec:int8 = StreamType; + // + // packetInit#1 protocol:int min_protocol:int flags:# data_saving_enabled:flags.0?true audio_streams:byteVector video_streams:byteVector = Packet; + case \danog\MadelineProto\VoIP::PKT_INIT: + $message .= $this->pack_signed_int($args['protocol']); + $message .= $this->pack_signed_int($args['min_protocol']); + $flags = 0; + $flags = isset($args['data_saving_enabled']) && $args['data_saving_enabled'] ? $flags | 1 : $flags & ~1; + $message .= $this->pack_unsigned_int($flags); + $message .= \chr(\count($args['audio_streams'])); + foreach ($args['audio_streams'] as $codec) { + $message .= \chr($codec); + } + $message .= \chr(\count($args['video_streams'])); + foreach ($args['video_streams'] as $codec) { + $message .= \chr($codec); + } + break; + // streamType id:int8 type:int8 codec:int8 frame_duration:int16 enabled:int8 = StreamType; + // + // packetInitAck#2 protocol:int min_protocol:int all_streams:byteVector = Packet; + case \danog\MadelineProto\VoIP::PKT_INIT_ACK: + $message .= $this->pack_signed_int($args['protocol']); + $message .= $this->pack_signed_int($args['min_protocol']); + $message .= \chr(\count($args['all_streams'])); + foreach ($args['all_streams'] as $stream) { + $message .= \chr($stream['id']); + $message .= \chr($stream['type']); + $message .= \chr($stream['codec']); + $message .= \pack('v', $stream['frame_duration']); + $message .= \chr($stream['enabled']); + } + break; + // streamTypeState id:int8 enabled:int8 = StreamType; + // packetStreamState#3 state:streamTypeState = Packet; + case \danog\MadelineProto\VoIP::PKT_STREAM_STATE: + $message .= \chr($args['id']); + $message .= \chr($args['enabled']); + break; + // streamData flags:int2 stream_id:int6 has_more_flags:flags.1?true length:(flags.0?int16:int8) timestamp:int data:byteArray = StreamData; + // packetStreamData#4 stream_data:streamData = Packet; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA: + $length = \strlen($args['data']); + $flags = 0; + $flags = $length > 255 ? $flags | 1 : $flags & ~1; + $flags = isset($args['has_more_flags']) && $args['has_more_flags'] ? $flags | 2 : $flags & ~2; + $flags = $flags << 6; + $flags = $flags | $args['stream_id']; + $message .= \chr($flags); + $message .= $length > 255 ? \pack('v', $length) : \chr($length); + $message .= $this->pack_unsigned_int($args['timestamp']); + $message .= $args['data']; + break; + /*case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS: + break; + case \danog\MadelineProto\VoIP::PKT_PING: + break;*/ + case \danog\MadelineProto\VoIP::PKT_PONG: + $message .= $this->pack_unsigned_int($args['out_seq_no']); + break; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA_X2: + for ($x = 0; $x < 2; $x++) { + $length = \strlen($args[$x]['data']); + $flags = 0; + $flags = $length > 255 ? $flags | 1 : $flags & ~1; + $flags = isset($args[$x]['has_more_flags']) && $args[$x]['has_more_flags'] ? $flags | 2 : $flags & ~2; + $flags = $flags << 6; + $flags = $flags | $args[$x]['stream_id']; + $message .= \chr($flags); + $message .= $length > 255 ? \pack('v', $length) : \chr($length); + $message .= $this->pack_unsigned_int($args[$x]['timestamp']); + $message .= $args[$x]['data']; + } + break; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA_X3: + for ($x = 0; $x < 3; $x++) { + $length = \strlen($args[$x]['data']); + $flags = 0; + $flags = $length > 255 ? $flags | 1 : $flags & ~1; + $flags = isset($args[$x]['has_more_flags']) && $args[$x]['has_more_flags'] ? $flags | 2 : $flags & ~2; + $flags = $flags << 6; + $flags = $flags | $args[$x]['stream_id']; + $message .= \chr($flags); + $message .= $length > 255 ? \pack('v', $length) : \chr($length); + $message .= $this->pack_unsigned_int($args[$x]['timestamp']); + $message .= $args[$x]['data']; + } + break; + // packetLanEndpoint#A address:int port:int = Packet; + case \danog\MadelineProto\VoIP::PKT_LAN_ENDPOINT: + $message .= $this->pack_signed_int($args['address']); + $message .= $this->pack_signed_int($args['port']); + break; + // packetNetworkChanged#B flags:# data_saving_enabled:flags.0?true = Packet; + case \danog\MadelineProto\VoIP::PKT_NETWORK_CHANGED: + $message .= $this->pack_signed_int(isset($args['data_saving_enabled']) && $args['data_saving_enabled'] ? 1 : 0); + break; + // packetSwitchPreferredRelay#C relay_id:long = Packet; + case \danog\MadelineProto\VoIP::PKT_SWITCH_PREF_RELAY: + $message .= $this->pack_signed_long($args['relay_d']); + break; + /*case \danog\MadelineProto\VoIP::PKT_SWITCH_TO_P2P: + break; + case \danog\MadelineProto\VoIP::PKT_NOP: + break;*/ + } + + $ack_mask = 0; + for ($x=0;$x<32;$x++) { + if ($this->received_timestamp_map[$x]>0) { + $ack_mask|=1; + } + if ($x<31) { + $ack_mask<<=1; + } + } + + if (\in_array($this->voip_state, [\danog\MadelineProto\VoIP::STATE_WAIT_INIT, \danog\MadelineProto\VoIP::STATE_WAIT_INIT_ACK])) { + $payload = $this->TLID_DECRYPTED_AUDIO_BLOCK; + $payload .= $this->random(8); + $payload .= \chr(7); + $payload .= $this->random(7); + $flags = 0; + $flags = $flags | 4; // call_id + $flags = $flags | 16; // seqno + $flags = $flags | 32; // ack mask + $flags = $flags | 8; // proto + $flags = isset($args['extra']) ? $flags | 2 : $flags & ~2; // extra + $flags = \strlen($message) ? $flags | 1 : $flags & ~1; // raw_data + $flags = $flags | ($args['_'] << 24); + $payload .= $this->pack_unsigned_int($flags); + $payload .= $this->configuration['call_id']; + $payload .= $this->pack_unsigned_int($this->session_in_seq_no); + $payload .= $this->pack_unsigned_int($this->session_out_seq_no); + $payload .= $this->pack_unsigned_int($ack_mask); + $payload .= \danog\MadelineProto\VoIP::PROTO_ID; + if ($flags & 2) { + $payload .= $this->pack_string($args['extra']); + } + if ($flags & 1) { + $payload .= $this->pack_string($message); + } + } else { + $payload = $this->TLID_SIMPLE_AUDIO_BLOCK; + $payload .= $this->random(8); + $payload .= \chr(7); + $payload .= $this->random(7); + $message = \chr($args['_']).$this->pack_unsigned_int($this->session_in_seq_no).$this->pack_unsigned_int($this->session_out_seq_no).$this->pack_unsigned_int($ack_mask).$message; + + $payload .= $this->pack_string($message); + } + $this->session_out_seq_no++; + + $payload = $this->pack_unsigned_int(\strlen($payload)).$payload; + $payload_key = \substr(\sha1($payload, true), -16); + list($aes_key, $aes_iv) = $this->old_aes_calculate($payload_key, $this->datacenter->sockets[$datacenter]->auth_key['auth_key'], $this->creator); + $payload .= $this->random($this->posmod(-\strlen($payload), 16)); + $payload = $this->datacenter->sockets[$datacenter]->peer_tag.$this->datacenter->sockets[$datacenter]->auth_key['id'].$payload_key.$this->ige_encrypt($payload, $aes_key, $aes_iv); + try { + $this->datacenter->sockets[$datacenter]->send_message($payload); + } catch (\danog\MadelineProto\Exception $e) { + unset($this->datacenter->sockets[$datacenter]); + } + } + + /** + * Reading connection and receiving message from server. + */ + public function recv_message($datacenter) + { + $payload = \fopen('php://memory', 'rw+b'); + \fwrite($payload, $this->datacenter->sockets[$datacenter]->read_message()); + \fseek($payload, 0); + + + if (\stream_get_contents($payload, 16) !== $this->datacenter->sockets[$datacenter]->peer_tag) { + \danog\MadelineProto\Logger::log("Received packet has wrong peer tag", \danog\MadelineProto\Logger::ERROR); + return false; + } + if (\stream_get_contents($payload, 12) === "\0\0\0\0\0\0\0\0\0\0\0\0") { + $payload = \stream_get_contents($payload); + } else { + \fseek($payload, 16); + if (\stream_get_contents($payload, 8) !== $this->datacenter->sockets[$datacenter]->auth_key['id']) { + \danog\MadelineProto\Logger::log('Wrong auth key ID', \danog\MadelineProto\Logger::ERROR); + return false; + } + $message_key = \stream_get_contents($payload, 16); + list($aes_key, $aes_iv) = $this->old_aes_calculate($message_key, $this->datacenter->sockets[$datacenter]->auth_key['auth_key'], !$this->creator); + $encrypted_data = \stream_get_contents($payload); + if (\strlen($encrypted_data) % 16 != 0) { + \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['length_not_divisible_16'], \danog\MadelineProto\Logger::ERROR); + } + + $decrypted_data = $this->ige_decrypt($encrypted_data, $aes_key, $aes_iv); + $message_data_length = \unpack('V', \substr($decrypted_data, 0, 4))[1]; + $payload = \substr($decrypted_data, 4, $message_data_length); + + if ($message_data_length > \strlen($decrypted_data)) { + \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['msg_data_length_too_big'], \danog\MadelineProto\Logger::ERROR); + return false; + } + if ($message_key != \substr(\sha1(\substr($decrypted_data, 0, 4 + $message_data_length), true), -16)) { + \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['msg_key_mismatch'], \danog\MadelineProto\Logger::ERROR); + return false; + } + if (\strlen($decrypted_data) - 4 - $message_data_length > 15) { + \danog\MadelineProto\Logger::log('difference between message_data_length and the length of the remaining decrypted buffer is too big', \danog\MadelineProto\Logger::ERROR); + } + if (\strlen($decrypted_data) % 16 != 0) { + \danog\MadelineProto\Logger::log(\danog\MadelineProto\Lang::$current_lang['length_not_divisible_16'], \danog\MadelineProto\Logger::ERROR); + } + } + $stream = \fopen('php://memory', 'rw+b'); + \fwrite($stream, $payload); + $payload = $stream; + \fseek($payload, 0); + + $result = []; + switch ($crc = \stream_get_contents($payload, 4)) { + case $this->TLID_DECRYPTED_AUDIO_BLOCK: + \stream_get_contents($payload, 8); + $this->unpack_string($payload); + $flags = \unpack('V', \stream_get_contents($payload, 4))[1]; + $result['_'] = $flags >> 24; + if ($flags & 4) { + if (\stream_get_contents($payload, 16) !== $this->configuration['call_id']) { + \danog\MadelineProto\Logger::log('Call ID mismatch', \danog\MadelineProto\Logger::ERROR); + return false; + } + } + if ($flags & 16) { + $in_seq_no = \unpack('V', \stream_get_contents($stream, 4))[1]; + $out_seq_no = \unpack('V', \stream_get_contents($stream, 4))[1]; + } + if ($flags & 32) { + $ack_mask = \unpack('V', \stream_get_contents($stream, 4))[1]; + } + if ($flags & 8) { + if (\stream_get_contents($stream, 4) !== \danog\MadelineProto\VoIP::PROTO_ID) { + \danog\MadelineProto\Logger::log('Protocol mismatch', \danog\MadelineProto\Logger::ERROR); + return false; + } + } + if ($flags & 2) { + $result['extra'] = $this->unpack_string($stream); + } + $message = \fopen('php://memory', 'rw+b'); + + if ($flags & 1) { + \fwrite($message, $this->unpack_string($stream)); + \fseek($message, 0); + } + break; + case $this->TLID_SIMPLE_AUDIO_BLOCK: + \stream_get_contents($payload, 8); + $this->unpack_string($payload); + $flags = \unpack('V', \stream_get_contents($payload, 4))[1]; + + $message = \fopen('php://memory', 'rw+b'); + \fwrite($message, $this->unpack_string($stream)); + \fseek($message, 0); + $result['_'] = \ord(\stream_get_contents($message, 1)); + $in_seq_no = \unpack('V', \stream_get_contents($message, 4))[1]; + $out_seq_no = \unpack('V', \stream_get_contents($message, 4))[1]; + $ack_mask = \unpack('V', \stream_get_contents($message, 4))[1]; + + break; + case $this->TLID_REFLECTOR_SELF_INFO: + $result['date'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + $result['query_id'] = $this->unpack_signed_long(\stream_get_contents($payload, 8)); + $result['my_ip'] = \stream_get_contents($payload, 16); + $result['my_port'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + return $result; + case $this->TLID_REFLECTOR_PEER_INFO: + $result['my_address'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + $result['my_port'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + $result['peer_address'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + $result['peer_port'] = $this->unpack_signed_int(\stream_get_contents($payload, 4)); + return $result; + default: + \danog\MadelineProto\Logger::log('Unknown packet received: '.\bin2hex($crc), \danog\MadelineProto\Logger::ERROR); + return false; + } + if (!$this->received_packet($datacenter, $in_seq_no, $out_seq_no, $ack_mask)) { + return false; + } + switch ($result['_']) { + // streamTypeSimple codec:int8 = StreamType; + // + // packetInit#1 protocol:int min_protocol:int flags:# data_saving_enabled:flags.0?true audio_streams:byteVector video_streams:byteVector = Packet; + case \danog\MadelineProto\VoIP::PKT_INIT: + $result['protocol'] = $this->unpack_signed_int(\stream_get_contents($message, 4)); + $result['min_protocol'] = $this->unpack_signed_int(\stream_get_contents($message, 4)); + $flags = \unpack('V', \stream_get_contents($message, 4))[1]; + $result['data_saving_enabled'] = (bool) ($flags & 1); + $result['audio_streams'] = []; + $length = \ord(\stream_get_contents($message, 1)); + for ($x = 0; $x < $length; $x++) { + $result['audio_streams'][$x] = \ord(\stream_get_contents($message, 1)); + } + $result['video_streams'] = []; + $length = \ord(\stream_get_contents($message, 1)); + for ($x = 0; $x < $length; $x++) { + $result['video_streams'][$x] = \ord(\stream_get_contents($message, 1)); + } + break; + // streamType id:int8 type:int8 codec:int8 frame_duration:int16 enabled:int8 = StreamType; + // + // packetInitAck#2 protocol:int min_protocol:int all_streams:byteVector = Packet; + case \danog\MadelineProto\VoIP::PKT_INIT_ACK: + $result['protocol'] = $this->unpack_signed_int(\stream_get_contents($message, 4)); + $result['min_protocol'] = $this->unpack_signed_int(\stream_get_contents($message, 4)); + $result['all_streams'] = []; + $length = \ord(\stream_get_contents($message, 1)); + for ($x = 0; $x < $length; $x++) { + $result['all_streams'][$x]['id'] = \ord(\stream_get_contents($message, 1)); + $result['all_streams'][$x]['type'] = \ord(\stream_get_contents($message, 1)); + $result['all_streams'][$x]['codec'] = \ord(\stream_get_contents($message, 1)); + $result['all_streams'][$x]['frame_duration'] = \unpack('v', \stream_get_contents($message, 2))[1]; + $result['all_streams'][$x]['enabled'] = \ord(\stream_get_contents($message, 1)); + } + + break; + // streamTypeState id:int8 enabled:int8 = StreamType; + // packetStreamState#3 state:streamTypeState = Packet; + case \danog\MadelineProto\VoIP::PKT_STREAM_STATE: + $result['id'] = \ord(\stream_get_contents($message, 1)); + $result['enabled'] = \ord(\stream_get_contents($message, 1)); + break; + // streamData flags:int2 stream_id:int6 has_more_flags:flags.1?true length:(flags.0?int16:int8) timestamp:int data:byteArray = StreamData; + // packetStreamData#4 stream_data:streamData = Packet; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA: + $flags = \ord(\stream_get_contents($message, 1)); + $result['stream_id'] = $flags & 0x3F; + $flags = ($flags & 0xC0) >> 6; + $result['has_more_flags'] = (bool) ($flags & 2); + $length = $flags & 1 ? \unpack('v', \stream_get_contents($message, 2))[1] : \ord(\stream_get_contents($message, 1)); + $result['timestamp'] = \unpack('V', \stream_get_contents($message, 4))[1]; + $result['data'] = \stream_get_contents($message, $length); + break; + /*case \danog\MadelineProto\VoIP::PKT_UPDATE_STREAMS: + break; + case \danog\MadelineProto\VoIP::PKT_PING: + break;*/ + case \danog\MadelineProto\VoIP::PKT_PONG: + if (\fstat($stream)['size'] - \ftell($stream)) { + $result['out_seq_no'] = \unpack('V', \stream_get_contents($stream, 4))[1]; + } + break; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA_X2: + for ($x = 0; $x < 2; $x++) { + $flags = \ord(\stream_get_contents($message, 1)); + $result[$x]['stream_id'] = $flags & 0x3F; + $flags = ($flags & 0xC0) >> 6; + $result[$x]['has_more_flags'] = (bool) ($flags & 2); + $length = $flags & 1 ? \unpack('v', \stream_get_contents($message, 2))[1] : \ord(\stream_get_contents($message, 1)); + $result[$x]['timestamp'] = \unpack('V', \stream_get_contents($message, 4))[1]; + $result[$x]['data'] = \stream_get_contents($message, $length); + } + break; + case \danog\MadelineProto\VoIP::PKT_STREAM_DATA_X3: + for ($x = 0; $x < 3; $x++) { + $flags = \ord(\stream_get_contents($message, 1)); + $result[$x]['stream_id'] = $flags & 0x3F; + $flags = ($flags & 0xC0) >> 6; + $result[$x]['has_more_flags'] = (bool) ($flags & 2); + $length = $flags & 1 ? \unpack('v', \stream_get_contents($message, 2))[1] : \ord(\stream_get_contents($message, 1)); + $result[$x]['timestamp'] = \unpack('V', \stream_get_contents($message, 4))[1]; + $result[$x]['data'] = \stream_get_contents($message, $length); + } + break; + // packetLanEndpoint#A address:int port:int = Packet; + case \danog\MadelineProto\VoIP::PKT_LAN_ENDPOINT: + $result['address'] = \unpack('V', \stream_get_contents($stream, 4))[1]; + $result['port'] = \unpack('V', \stream_get_contents($stream, 4))[1]; + break; + // packetNetworkChanged#B flags:# data_saving_enabled:flags.0?true = Packet; + case \danog\MadelineProto\VoIP::PKT_NETWORK_CHANGED: + $result['data_saving_enabled'] = (bool) (\unpack('V', \stream_get_contents($stream, 4))[1] & 1); + break; + // packetSwitchPreferredRelay#C relay_id:long = Packet; + case \danog\MadelineProto\VoIP::PKT_SWITCH_PREF_RELAY: + $result['relay_id'] = $this->unpack_signed_long(\stream_get_contents($stream, 8)); + break; + /*case \danog\MadelineProto\VoIP::PKT_SWITCH_TO_P2P: + break; + case \danog\MadelineProto\VoIP::PKT_NOP: + break;*/ + } + return $result; + } +}