diff --git a/src/Exception.php b/src/Exception.php index ec865ba79..01abc494e 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -107,6 +107,10 @@ class Exception extends \Exception { $print = function (string $s): void { Logger::log($s, Logger::FATAL_ERROR); + if (\headers_sent()) { + return; + } + \http_response_code(500); if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { echo($s.PHP_EOL); } else { diff --git a/src/MTProto.php b/src/MTProto.php index 8497e39cd..55845b5bf 100644 --- a/src/MTProto.php +++ b/src/MTProto.php @@ -116,7 +116,7 @@ final class MTProto implements TLCallback, LoggerGetter * @internal * @var int */ - const V = 170; + const V = 171; /** * Bad message error codes. * @@ -529,22 +529,11 @@ final class MTProto implements TLCallback, LoggerGetter $this->cleanupProperties(); // Start IPC server if (!$this->ipcServer) { - try { - $this->ipcServer = new Server($this); - $this->ipcServer->setSettings($this->settings->getIpc()); - $this->ipcServer->setIpcPath($this->wrapper->getSession()); - } catch (Throwable $e) { - $this->logger->logger("Error while starting IPC server: $e", Logger::FATAL_ERROR); - } - } - try { - $this->ipcServer->start(); - } catch (Throwable $e) { - if (Magic::$isIpcWorker) { - throw $e; - } - $this->logger->logger("Error while starting IPC server: $e", Logger::FATAL_ERROR); + $this->ipcServer = new Server($this); + $this->ipcServer->setSettings($this->settings->getIpc()); + $this->ipcServer->setIpcPath($this->wrapper->getSession()); } + $this->ipcServer->start(); // Load rsa keys $this->rsa_keys = []; foreach ($this->settings->getConnection()->getRSAKeys() as $key) { diff --git a/src/MTProtoTools/Files.php b/src/MTProtoTools/Files.php index 531c31840..e27f05481 100644 --- a/src/MTProtoTools/Files.php +++ b/src/MTProtoTools/Files.php @@ -1113,8 +1113,8 @@ trait Files if ($offset['part_start_at'] || $offset['part_end_at'] !== $offset['limit']) { $res['bytes'] = \substr($res['bytes'], $offset['part_start_at'], $offset['part_end_at'] - $offset['part_start_at']); } - if (!$seekable) { - $offset['previous_promise']; + if (!$seekable && $offset['previous_promise'] instanceof Future) { + $offset['previous_promise']->await(); } $len = \strlen($res['bytes']); $res = $callable($res['bytes'], $offset['offset'] + $offset['part_start_at']); diff --git a/src/MTProtoTools/ResponseInfo.php b/src/MTProtoTools/ResponseInfo.php index c8ffd0bf7..f1b795753 100644 --- a/src/MTProtoTools/ResponseInfo.php +++ b/src/MTProtoTools/ResponseInfo.php @@ -106,14 +106,14 @@ final class ResponseInfo } $seek_start = empty($seek_start) ? 0 : \abs(\intval($seek_start)); - if ($range !== '') { + /*if ($range !== '') { //Safari video streaming fix $length = ($seek_end - $seek_start + 1); $maxChunkSize = 10 * 1024 ** 2; if ($length > $maxChunkSize) { $seek_end = $seek_start + $maxChunkSize - 1; } - } + }*/ $this->serve = $method !== 'HEAD'; if ($seek_start > 0 || $seek_end < $size - 1) { diff --git a/src/Ogg.php b/src/Ogg.php new file mode 100644 index 000000000..584ecd6dd --- /dev/null +++ b/src/Ogg.php @@ -0,0 +1,631 @@ + + * @author Daniil Gentili + */ +final class Ogg +{ + private const CRC = [ + 0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9, + 0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005, + 0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61, + 0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd, + 0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9, + 0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75, + 0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011, + 0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd, + 0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039, + 0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5, + 0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81, + 0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d, + 0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49, + 0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95, + 0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1, + 0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d, + 0x34867077,0x30476dc0,0x3d044b19,0x39c556ae, + 0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072, + 0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16, + 0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca, + 0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde, + 0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02, + 0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066, + 0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba, + 0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e, + 0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692, + 0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6, + 0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a, + 0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e, + 0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2, + 0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686, + 0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a, + 0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637, + 0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb, + 0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f, + 0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53, + 0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47, + 0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b, + 0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff, + 0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623, + 0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7, + 0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b, + 0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f, + 0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3, + 0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7, + 0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b, + 0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f, + 0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3, + 0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640, + 0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c, + 0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8, + 0x68860bfd,0x6c47164a,0x61043093,0x65c52d24, + 0x119b4be9,0x155a565e,0x18197087,0x1cd86d30, + 0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec, + 0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088, + 0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654, + 0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0, + 0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c, + 0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18, + 0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4, + 0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0, + 0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c, + 0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668, + 0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4 + ]; + private const OPUS_SET_APPLICATION_REQUEST = 4000; + private const OPUS_GET_APPLICATION_REQUEST = 4001; + private const OPUS_SET_BITRATE_REQUEST = 4002; + private const OPUS_GET_BITRATE_REQUEST = 4003; + private const OPUS_SET_MAX_BANDWIDTH_REQUEST = 4004; + private const OPUS_GET_MAX_BANDWIDTH_REQUEST = 4005; + private const OPUS_SET_VBR_REQUEST = 4006; + private const OPUS_GET_VBR_REQUEST = 4007; + private const OPUS_SET_BANDWIDTH_REQUEST = 4008; + private const OPUS_GET_BANDWIDTH_REQUEST = 4009; + private const OPUS_SET_COMPLEXITY_REQUEST = 4010; + private const OPUS_GET_COMPLEXITY_REQUEST = 4011; + private const OPUS_SET_INBAND_FEC_REQUEST = 4012; + private const OPUS_GET_INBAND_FEC_REQUEST = 4013; + private const OPUS_SET_PACKET_LOSS_PERC_REQUEST = 4014; + private const OPUS_GET_PACKET_LOSS_PERC_REQUEST = 4015; + private const OPUS_SET_DTX_REQUEST = 4016; + private const OPUS_GET_DTX_REQUEST = 4017; + private const OPUS_SET_VBR_CONSTRAINT_REQUEST = 4020; + private const OPUS_GET_VBR_CONSTRAINT_REQUEST = 4021; + private const OPUS_SET_FORCE_CHANNELS_REQUEST = 4022; + private const OPUS_GET_FORCE_CHANNELS_REQUEST = 4023; + private const OPUS_SET_SIGNAL_REQUEST = 4024; + private const OPUS_GET_SIGNAL_REQUEST = 4025; + private const OPUS_GET_LOOKAHEAD_REQUEST = 4027; + private const OPUS_GET_SAMPLE_RATE_REQUEST = 4029; + private const OPUS_GET_FINAL_RANGE_REQUEST = 4031; + private const OPUS_GET_PITCH_REQUEST = 4033; + private const OPUS_SET_GAIN_REQUEST = 4034; + private const OPUS_GET_GAIN_REQUEST = 4045; + private const OPUS_SET_LSB_DEPTH_REQUEST = 4036; + private const OPUS_GET_LSB_DEPTH_REQUEST = 4037; + private const OPUS_GET_LAST_PACKET_DURATION_REQUEST = 4039; + private const OPUS_SET_EXPERT_FRAME_DURATION_REQUEST = 4040; + private const OPUS_GET_EXPERT_FRAME_DURATION_REQUEST = 4041; + private const OPUS_SET_PREDICTION_DISABLED_REQUEST = 4042; + private const OPUS_GET_PREDICTION_DISABLED_REQUEST = 4043; + private const OPUS_SET_PHASE_INVERSION_DISABLED_REQUEST = 4046; + private const OPUS_GET_PHASE_INVERSION_DISABLED_REQUEST = 4047; + private const OPUS_GET_IN_DTX_REQUEST = 4049; + + /* Values for the various encoder CTLs */ + private const OPUS_AUTO = -1000 /** + */ + public readonly iterable $opusPackets; + + /** + * Constructor. + * + * @param BufferedStreamInterface $stream The stream + */ + public function __construct(BufferedStreamInterface $stream) + { + $this->stream = $stream->getReadBuffer($l); + $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', + ]; + + $this->packFormat = \implode( + '/', + \array_map( + fn (string $v, string $k): string => $v.$k, + $pack_format, + \array_keys($pack_format), + ), + ); + $it = $this->read(); + $it->current(); + $this->opusPackets = $it; + } + + /** + * Read OPUS length. + */ + 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. + * + * @psalm-suppress InvalidArrayOffset + */ + 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; + } + $this->frameDuration ??= $frameDuration; + + $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); + } + if (!$selfDelimited) { + $sizes []= ($len - ($offset + $padding)); + } + } 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); + /** @psalm-suppress InvalidArgument */ + $this->opusPayload .= \substr($content, $preOffset, (int) (($offset - $preOffset) + $sum + $paddingLen)); + if ($this->currentDuration === $this->frameDuration) { + yield $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->opusPayload; + $this->opusPayload = ''; + $this->currentDuration = 0; + } + } + $offset += $paddingLen; + } + } + + /** + * Read frames. + * + * @return \Generator + */ + private function read(): \Generator + { + $state = self::STATE_READ_HEADER; + $content = ''; + $granule = 0; + $ignoredStreams = []; + + while (true) { + $capture = $this->stream->bufferRead(4); + if ($capture !== self::CAPTURE_PATTERN) { + if ($capture === null) { + return; + } + throw new Exception('Bad capture pattern: '.\bin2hex($capture)); + } + + $headers = \unpack( + $this->packFormat, + $this->stream->bufferRead(23) + ); + $ignore = \in_array($headers['bitstream_serial_number'], $ignoredStreams, true); + + 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'] & self::BOS); + $lastPage = (bool) ($headers['header_type_flag'] & self::EOS); + + $segments = \unpack( + 'C*', + $this->stream->bufferRead($headers['number_page_segments']), + ); + + //$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) { + $piece = $this->stream->bufferRead($sizeAccumulated); + $sizeAccumulated = 0; + if ($ignore) { + continue; + } + $content .= $piece; + if ($state === self::STATE_STREAMING) { + yield $content; + //yield from $this->opusStateMachine($content); + } elseif ($state === self::STATE_READ_HEADER) { + Assert::true($firstPage); + $head = \substr($content, 0, 8); + if ($head !== 'OpusHead') { + $ignoredStreams[]= $headers['bitstream_serial_number']; + $content = ''; + $ignore = true; + continue; + } + $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']; + if ($opus_head['sample_rate'] !== 48000) { + throw new AssertionError("The sample rate must be 48khz, got {$opus_head['sample_rate']}"); + } + $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 = ''; + } + } + } + } + + public static function convert(string $wavIn, string $oggOut): void + { + $opus = FFI::cdef(' + typedef struct OpusEncoder OpusEncoder; + + OpusEncoder *opus_encoder_create( + int32_t Fs, + int channels, + int application, + int *error + ); + + int opus_encoder_ctl(OpusEncoder *st, int request, int arg); + + int32_t opus_encode( + OpusEncoder *st, + const char *pcm, + int frame_size, + const char *data, + int32_t max_data_bytes + ); + void opus_encoder_destroy(OpusEncoder *st); + const char *opus_strerror(int error); + const char *opus_get_version_string(void); + + ', 'libopus.so.0.9.0'); + $err = FFI::new('int'); + $encoder = $opus->opus_encoder_create(48000, 2, self::OPUS_APPLICATION_AUDIO, FFI::addr($err)); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_COMPLEXITY_REQUEST, 10); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_PACKET_LOSS_PERC_REQUEST, 1); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_INBAND_FEC_REQUEST, 1); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_SIGNAL_REQUEST, self::OPUS_SIGNAL_MUSIC); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_BANDWIDTH_REQUEST, self::OPUS_BANDWIDTH_FULLBAND); + $opus->opus_encoder_ctl($encoder, self::OPUS_SET_BITRATE_REQUEST, 130*1000); + + $in = openFile($wavIn, 'r'); + Assert::eq($in->read(length: 4), 'RIFF'); + $totalLength = \unpack('V', $in->read(length: 4))[1]; + Assert::eq($in->read(length: 4), 'WAVE'); + do { + $type = $in->read(length: 4); + $length = \unpack('V', $in->read(length: 4))[1]; + if ($type === 'fmt ') { + Assert::eq($length, 16); + $contents = $in->read(length: $length + ($length % 2)); + $header = \unpack('vaudioFormat/vchannels/VsampleRate/VbyteRate/vblockAlign/vbitsPerSample', $contents); + Assert::eq($header['audioFormat'], 1); + Assert::eq($header['sampleRate'], 48000); + } elseif ($type === 'data') { + break; + } else { + $in->seek($length, Whence::Current); + } + } while (true); + + $sampleCount = 0.06 * $header['sampleRate']; + $chunkSize = (int) $sampleCount * $header['channels'] * ($header['bitsPerSample'] >> 3); + $shift = (int) \log($header['channels'] * ($header['bitsPerSample'] >> 3), 2); + + $out = openFile($oggOut, 'w'); + + $writePage = function (int $header_type_flag, int $granule, int $streamId, int &$streamSeqno, string $packet) use ($out): void { + Assert::true(\strlen($packet) < 65025); + $segments = [ + ...\array_fill(0, (int) (\strlen($packet) / 255), 255), + \strlen($packet) % 255 + ]; + $data = 'OggS'.\pack( + 'CCPVVVCC*', + 0, // stream_structure_version + $header_type_flag, + $granule, + $streamId, + $streamSeqno++, + 0, + \count($segments), + ...$segments + ).$packet; + + $c = 0; + for ($i = 0; $i < \strlen($data); $i++) { + $c = ($c<<8)^self::CRC[(($c >> 24)&0xFF)^(\ord($data[$i]))]; + } + $crc = \pack('V', $c); + + $data = \substr_replace( + $data, + $crc, + 22, + 4 + ); + $out->write($data); + }; + + $streamId = \unpack('V', Tools::random(4))[1]; + $seqno = 0; + + $writePage( + Ogg::BOS, + 0, + $streamId, + $seqno, + 'OpusHead'.\pack( + 'CCvVvC', + 1, + $header['channels'], + 312, + $header['sampleRate'], + 0, + 0, + ) + ); + + $tags = 'OpusTags'; + $writeTag = function (string $tag) use (&$tags): void { + $tags .= \pack('V', \strlen($tag)).$tag; + }; + $writeTag("MadelineProto ".API::RELEASE.", ".$opus->opus_get_version_string()); + $tags .= \pack('V', 2); + $writeTag("ENCODER=MadelineProto ".API::RELEASE." with ".$opus->opus_get_version_string()); + $writeTag('See https://docs.madelineproto.xyz/docs/VOIP.html for more info'); + $writePage( + 0, + 0, + $streamId, + $seqno, + $tags + ); + + $granule = 0; + $buf = FFI::cast(FFI::type('char*'), FFI::addr($opus->new('char[1024]'))); + while (!$in->eof()) { + $chunk = $in->read(length: $chunkSize); + $granuleDiff = \strlen($chunk) >> $shift; + $len = $opus->opus_encode($encoder, $chunk, $granuleDiff, $buf, 1024); + $writePage( + $in->eof() ? self::EOS : 0, + $granule += $granuleDiff, + $streamId, + $seqno, + FFI::string($buf, $len) + ); + } + $opus->opus_encoder_destroy($encoder); + unset($buf, $encoder, $opus); + + $out->close(); + } +} diff --git a/src/Stream/Ogg/Ogg.php b/src/Stream/Ogg/Ogg.php deleted file mode 100644 index b76d7c0bb..000000000 --- a/src/Stream/Ogg/Ogg.php +++ /dev/null @@ -1,338 +0,0 @@ - - * @author Daniil Gentili - */ -final 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. - * @param iterable - */ - private iterable $emitter; - - private function __construct() - { - } - /** - * Constructor. - * - * @param BufferedStreamInterface $stream The stream - * @param int $frameDuration Required frame duration, microseconds - */ - public static function init(BufferedStreamInterface $stream, int $frameDuration): self - { - $self = new self; - $self->frameDuration = $frameDuration; - $self->stream = $stream->getReadBuffer($l); - $self->emitter = $self->read(); - $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. - */ - 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. - * - * @psalm-suppress InvalidArrayOffset - */ - 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); - } - if (!$selfDelimited) { - $sizes []= ($len - ($offset + $padding)); - } - } 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); - /** @psalm-suppress InvalidArgument */ - $this->opusPayload .= \substr($content, $preOffset, ($offset - $preOffset) + $sum + $paddingLen); - if ($this->currentDuration === $this->frameDuration) { - yield $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->opusPayload; - $this->opusPayload = ''; - $this->currentDuration = 0; - } - } - $offset += $paddingLen; - } - } - - /** - * Read frames. - * - * @return iterable - */ - private function read(): \Generator - { - $state = self::STATE_READ_HEADER; - $content = ''; - - while (true) { - $init = $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*', - $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 .= $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']; - $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 iterable - */ - public function getEmitter(): iterable - { - return $this->emitter; - } -} diff --git a/src/VoIP.php b/src/VoIP.php index 44c9f6452..b88a4c566 100644 --- a/src/VoIP.php +++ b/src/VoIP.php @@ -18,7 +18,6 @@ namespace danog\MadelineProto; use danog\MadelineProto\MTProto\PermAuthKey; use danog\MadelineProto\Stream\Common\FileBufferedStream; use danog\MadelineProto\Stream\ConnectionContext; -use danog\MadelineProto\Stream\Ogg\Ogg; use danog\MadelineProto\VoIP\AckHandler; use danog\MadelineProto\VoIP\Endpoint; use danog\MadelineProto\VoIP\MessageHandler; @@ -99,11 +98,11 @@ final class VoIP 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_DECRYPTED_AUDIO_BLOCK = "\xc1\xdb\xf9\x48"; + const TLID_SIMPLE_AUDIO_BLOCK = "\x0d\x0e\x76\xcc"; - const TLID_REFLECTOR_SELF_INFO_HEX = 'c01572c7'; - const TLID_REFLECTOR_PEER_INFO_HEX = '27D9371C'; + const TLID_REFLECTOR_SELF_INFO = "\xC7\x72\x15\xc0"; + const TLID_REFLECTOR_PEER_INFO = "\x1C\x37\xD9\x27"; const PROTO_ID = 'GrVP'; @@ -115,21 +114,15 @@ final class VoIP const CODEC_OPUS = 'SUPO'; - private $TLID_DECRYPTED_AUDIO_BLOCK; - private $TLID_SIMPLE_AUDIO_BLOCK; - private $TLID_REFLECTOR_SELF_INFO; - private $TLID_REFLECTOR_PEER_INFO; - private MTProto $MadelineProto; - public MTProto $madeline; - 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 = []; + public array $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 array $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 int $session_out_seq_no = 0; + public int $session_in_seq_no = 0; + public int $voip_state = 0; + public array $configuration = ['endpoints' => [], 'shared_config' => []]; + public array $storage = []; + public array $internalStorage = []; private $signal = 0; private ?int $callState = null; private $callID; @@ -152,10 +145,17 @@ final class VoIP */ private array $sockets = []; + private ?Endpoint $bestEndpoint = null; + + private bool $pendingPing = true; /** * Timeout watcher. */ private ?string $timeoutWatcher = null; + /** + * Ping watcher. + */ + private ?string $pingWatcher = null; /** * Last incoming timestamp. @@ -182,7 +182,7 @@ final class VoIP public function __sleep(): array { $vars = \get_object_vars($this); - unset($vars['sockets'], $vars['timeoutWatcher']); + unset($vars['sockets'], $vars['bestEndpoint'], $vars['timeoutWatcher']); return \array_keys($vars); } @@ -203,13 +203,9 @@ final class VoIP { $this->creator = $creator; $this->otherID = $otherID; - $this->madeline = $this->MadelineProto = $MadelineProto; + $this->MadelineProto = $MadelineProto; $this->callState = $callState; $this->packetQueue = new SplQueue; - $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)); } /** @@ -325,20 +321,25 @@ final class VoIP $this->authKey->setAuthKey($this->configuration['auth_key']); foreach ($this->configuration['endpoints'] as $endpoint) { - $this->sockets['v6 '.$endpoint['id']] = new Endpoint('['.$endpoint['ipv6'].']', $endpoint['port'], $endpoint['peer_tag'], true, $this); - $this->sockets['v4 '.$endpoint['id']] = new Endpoint($endpoint['ip'], $endpoint['port'], $endpoint['peer_tag'], true, $this); - } - foreach ($this->sockets as $k => $socket) { try { - $socket->connect(); - } catch (Throwable $e) { - unset($this->sockets[$k]); + $this->sockets['v6 '.$endpoint['id']] = new Endpoint('['.$endpoint['ipv6'].']', $endpoint['port'], $endpoint['peer_tag'], true, $this); + } catch (Throwable) { + } + try { + $this->sockets['v4 '.$endpoint['id']] = new Endpoint($endpoint['ip'], $endpoint['port'], $endpoint['peer_tag'], true, $this); + } catch (Throwable) { } } foreach ($this->sockets as $socket) { - $this->send_message(['_' => self::PKT_INIT, 'protocol' => self::PROTOCOL_VERSION, 'min_protocol' => self::MIN_PROTOCOL_VERSION, 'audio_streams' => [self::CODEC_OPUS], 'video_streams' => []], $socket); + $socket->write($this->encryptPacket([ + '_' => self::PKT_INIT, + 'protocol' => self::PROTOCOL_VERSION, + 'min_protocol' => self::MIN_PROTOCOL_VERSION, + 'audio_streams' => [self::CODEC_OPUS], + 'video_streams' => [] + ], false)); EventLoop::queue(function () use ($socket): void { - while ($payload = $this->recv_message($socket)) { + while ($payload = $socket->read()) { $this->lastIncomingTimestamp = \microtime(true); EventLoop::queue($this->handlePacket(...), $socket, $payload); } @@ -356,12 +357,32 @@ final class VoIP 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]]], $socket); + $this->peerVersion = $packet['protocol']; + $socket->write( + $this->encryptPacket([ + '_' => 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] + ] + ]) + ); - $this->startWriteLoop($socket); - break; + // no break case self::PKT_INIT_ACK: - $this->startWriteLoop($socket); + if (!$this->bestEndpoint) { + $this->bestEndpoint = $socket; + $this->pingWatcher = EventLoop::delay(1.0, function (): void { + $this->pendingPing = true; + foreach ($this->sockets as $socket) { + //$socket->udpPing(); + $packet = $this->encryptPacket(['_' => self::PKT_PING]); + EventLoop::queue(fn () => $socket->write($packet)); + } + }); + $this->startWriteLoop(); + } break; case self::PKT_STREAM_DATA: $cnt = 1; @@ -372,19 +393,26 @@ final class VoIP case self::PKT_STREAM_DATA_X3: $cnt = 3; break; - } - if (isset($cnt)) { + case self::PKT_PONG: + if ($this->pendingPing) { + $this->pendingPing = false; + if ($this->bestEndpoint !== $socket) { + Logger::log("Changing best endpoint from {$this->bestEndpoint} to $socket"); + $this->bestEndpoint = $socket; + } + } + break; + default: + \var_dump($packet); } } /** * Start write loop. */ - private function startWriteLoop(Endpoint $socket): void + private function startWriteLoop(): void { - if ($this->voip_state === self::STATE_ESTABLISHED) { - return; - } $this->voip_state = self::STATE_ESTABLISHED; + Logger::log("Call established, sending OPUS data!"); $this->tempHoldFiles = []; while (true) { @@ -399,51 +427,34 @@ final class VoIP $file = \array_shift($this->tempHoldFiles); } $it = $this->openFile($file); - if ($this->MadelineProto->getSettings()->getVoip()->getPreloadAudio()) { - while ($it->advance()) { - $this->packetQueue->enqueue($it->getCurrent()); + foreach ($it->opusPackets as $packet) { + $this->packetQueue->enqueue($packet); + } + $t = \microtime(true) + 0.060; + while (!$this->packetQueue->isEmpty()) { + $packet = $this->encryptPacket(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->timestamp]); + + //Logger::log("Writing {$this->timestamp} in $this!"); + $diff = $t - \microtime(true); + if ($diff > 0) { + delay($diff); } - $t = \microtime(true) + 0.060; - while (!$this->packetQueue->isEmpty()) { - if (!$this->send_message(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $this->packetQueue->dequeue(), 'timestamp' => $this->timestamp], $socket)) { - Logger::log("Exiting VoIP write loop in $this!"); - return; - } + $this->bestEndpoint->write($packet); + $t += 0.060; - //Logger::log("Writing {$this->timestamp} in $this!"); - delay($t - \microtime(true)); - $t = \microtime(true) + 0.060; - - $this->timestamp += 60; - } - } else { - $t = \microtime(true) + 0.060; - while ($it->advance()) { - if (!$this->send_message(['_' => self::PKT_STREAM_DATA, 'stream_id' => 0, 'data' => $it->getCurrent(), 'timestamp' => $this->timestamp], $socket)) { - Logger::log("Exiting VoIP write loop in $this!"); - return; - } - - //Logger::log("Writing {$this->timestamp} in $this!"); - delay($t - \microtime(true)); - $t = \microtime(true) + 0.060; - - $this->timestamp += 60; - } + $this->timestamp += 60; } } } /** * Open OGG file for reading. */ - private function openFile(string $file) + private function openFile(string $file): Ogg { $ctx = new ConnectionContext; $ctx->addStream(FileBufferedStream::class, openFile($file, 'r')); $stream = $ctx->getStream(); - $ogg = Ogg::init($stream, 60000); - $it = $ogg->getEmitter(); - return $it; + return new Ogg($stream); } /** * Play file. @@ -500,7 +511,7 @@ final class VoIP */ public function setMadeline(MTProto $MadelineProto): void { - $this->MadelineProto = $this->madeline = $MadelineProto; + $this->MadelineProto = $MadelineProto; } /** diff --git a/src/VoIP/AckHandler.php b/src/VoIP/AckHandler.php index ddeb43d5f..751971500 100644 --- a/src/VoIP/AckHandler.php +++ b/src/VoIP/AckHandler.php @@ -22,11 +22,11 @@ use danog\MadelineProto\Logger; */ trait AckHandler { - private function seqgt($s1, $s2) + private function seqgt(int $s1, int $s2): bool { return $s1 > $s2; } - public function received_packet($last_ack_id, $packet_seq_no, $ack_mask) + public function received_packet(int $last_ack_id, int $packet_seq_no, int $ack_mask): bool { if ($this->seqgt($packet_seq_no, $this->session_in_seq_no)) { $diff = $packet_seq_no - $this->session_in_seq_no; diff --git a/src/VoIP/AuthKeyHandler.php b/src/VoIP/AuthKeyHandler.php index 0dd2169bf..cd0c02466 100644 --- a/src/VoIP/AuthKeyHandler.php +++ b/src/VoIP/AuthKeyHandler.php @@ -282,6 +282,9 @@ trait AuthKeyHandler $this->logger->logger(\sprintf('Saving debug data for call %s...', $call['id']), Logger::VERBOSE); $this->methodCallAsyncRead('phone.saveCallDebug', ['peer' => $call, 'debug' => $this->calls[$call['id']]->getDebugLog()]); } + if (!isset($this->calls[$call['id']])) { + return null; + } $c = $this->calls[$call['id']]; unset($this->calls[$call['id']]); return $c; diff --git a/src/VoIP/Endpoint.php b/src/VoIP/Endpoint.php index c5d042aa0..37a1ff8dc 100644 --- a/src/VoIP/Endpoint.php +++ b/src/VoIP/Endpoint.php @@ -64,16 +64,13 @@ final class Endpoint $this->instance = $instance; $this->creator = $instance->isCreator(); $this->authKey = $instance->getAuthKey(); - } - - /** - * Connect to endpoint. - */ - public function connect(): void - { $this->socket = connect("udp://{$this->ip}:{$this->port}"); } + public function __toString(): string + { + return "{$this->ip}:{$this->port}"; + } /** * Disconnect from endpoint. */ @@ -84,64 +81,283 @@ final class Endpoint $this->socket = null; } } + + private static function unpack_string($stream): string + { + $l = \ord(\stream_get_contents($stream, 1)); + if ($l > 254) { + throw new Exception(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 = Tools::posmod(-$long_len, 4); + if ($resto > 0) { + \stream_get_contents($stream, $resto); + } + } else { + $x = \stream_get_contents($stream, $l); + $resto = Tools::posmod(-($l + 1), 4); + if ($resto > 0) { + \stream_get_contents($stream, $resto); + } + } + return $x; + } /** * Read packet. */ - public function read() + public function read(): ?array { - do { - $packet = $this->socket->read(); - if ($packet === null) { - return null; - } + $packet = $this->socket->read(); + if ($packet === null) { + return null; + } + do { $payload = \fopen('php://memory', 'rw+b'); \fwrite($payload, $packet); \fseek($payload, 0); - - $hasPeerTag = false; + $pos = 0; if ($this->instance->getPeerVersion() < 9 || $this->reflector) { - $hasPeerTag = true; - if (\stream_get_contents($payload, 16) !== $this->peerTag) { + /*if (\fread($payload, 16) !== $this->peerTag) { Logger::log('Received packet has wrong peer tag', Logger::ERROR); continue; - } + }*/ + $pos = 16; } - if (\stream_get_contents($payload, 12) === "\0\0\0\0\0\0\0\0\0\0\0\0") { - $payload = \stream_get_contents($payload); + if (\fread($payload, 12) === "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") { + switch ($crc = \fread($payload, 4)) { + case VoIP::TLID_REFLECTOR_SELF_INFO: + $result['_'] = 'reflectorSelfInfo'; + $result['date'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + $result['query_id'] = Tools::unpackSignedLong(\stream_get_contents($payload, 8)); + $result['my_ip'] = \stream_get_contents($payload, 16); + $result['my_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + return $result; + case VoIP::TLID_REFLECTOR_PEER_INFO: + $result['_'] = 'reflectorPeerInfo'; + $result['my_address'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + $result['my_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + $result['peer_address'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + $result['peer_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); + return $result; + default: + Logger::log('Unknown unencrypted packet received: '.\bin2hex($crc), Logger::ERROR); + continue 2; + } } else { - \fseek($payload, $hasPeerTag ? 16 : 0); - $message_key = \stream_get_contents($payload, 16); + fseek($payload, $pos); + $message_key = \fread($payload, 16); [$aes_key, $aes_iv] = Crypt::aesCalculate($message_key, $this->authKey->getAuthKey(), !$this->creator); $encrypted_data = \stream_get_contents($payload); $packet = Crypt::igeDecrypt($encrypted_data, $aes_key, $aes_iv); if ($message_key != \substr(\hash('sha256', \substr($this->authKey->getAuthKey(), 88 + ($this->creator ? 8 : 0), 32).$packet, true), 8, 16)) { Logger::log('msg_key mismatch!', Logger::ERROR); - return false; + continue; } $innerLen = \unpack('v', \substr($packet, 0, 2))[1]; if ($innerLen > \strlen($packet)) { Logger::log('Received packet has wrong inner length!', Logger::ERROR); - return false; + continue; } $packet = \substr($packet, 2); } - $stream = \fopen('php://memory', 'rw+b'); - \fwrite($stream, $packet); - \fseek($stream, 0); + $payload = \fopen('php://memory', 'rw+b'); + \fwrite($payload, $packet); + \fseek($payload, 0); - return $stream; + $result = []; + switch ($crc = \stream_get_contents($payload, 4)) { + case VoIP::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->instance->configuration['call_id']) { + Logger::log('Call ID mismatch', Logger::ERROR); + continue 2; + } + } + if ($flags & 16) { + $in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; + $out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; + } + if ($flags & 32) { + $ack_mask = \unpack('V', \stream_get_contents($payload, 4))[1]; + } + if ($flags & 8) { + if (\stream_get_contents($payload, 4) !== VoIP::PROTO_ID) { + Logger::log('Protocol mismatch', Logger::ERROR); + continue 2; + } + } + if ($flags & 2) { + $result['extra'] = $this->unpack_string($payload); + } + $message = \fopen('php://memory', 'rw+b'); + + if ($flags & 1) { + \fwrite($message, $this->unpack_string($payload)); + \fseek($message, 0); + } + break; + case VoIP::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($payload)); + \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; + default: + if ($this->instance->getPeerVersion() >= 8 || (!$this->instance->getPeerVersion())) { + \fseek($payload, -4, SEEK_CUR); + $result['_'] = \ord(\stream_get_contents($payload, 1)); + $in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; + $out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; + $ack_mask = \unpack('V', \stream_get_contents($payload, 4))[1]; + $flags = \ord(\stream_get_contents($payload, 1)); + if ($flags & 1) { + $result['extra'] = []; + $count = \ord(\stream_get_contents($payload, 1)); + for ($x = 0; $x < $count; $x++) { + $len = \ord(\stream_get_contents($payload, 1)); + $result['extra'][]= \stream_get_contents($payload, $len); + } + } + $message = \fopen('php://memory', 'rw+b'); + + \fwrite($message, \stream_get_contents($payload)); + \fseek($message, 0); + } else { + Logger::log('Unknown packet received: '.\bin2hex($crc), Logger::ERROR); + continue 2; + } + } + if (!$this->instance->received_packet($in_seq_no, $out_seq_no, $ack_mask)) { + return $this->read(); + } + 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 VoIP::PKT_INIT: + $result['protocol'] = Tools::unpackSignedInt(\stream_get_contents($message, 4)); + $result['min_protocol'] = Tools::unpackSignedInt(\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] = \stream_get_contents($message, 4); + } + 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 VoIP::PKT_INIT_ACK: + $result['protocol'] = Tools::unpackSignedInt(\stream_get_contents($message, 4)); + $result['min_protocol'] = Tools::unpackSignedInt(\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'] = \stream_get_contents($message, 4); + $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 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 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 VoIP::PKT_PONG: + if (\fstat($payload)['size'] - \ftell($payload)) { + $result['out_seq_no'] = \unpack('V', \stream_get_contents($payload, 4))[1]; + } + break; + case 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 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 VoIP::PKT_LAN_ENDPOINT: + $result['address'] = \unpack('V', \stream_get_contents($payload, 4))[1]; + $result['port'] = \unpack('V', \stream_get_contents($payload, 4))[1]; + break; + // packetNetworkChanged#B flags:# data_saving_enabled:flags.0?true = Packet; + case VoIP::PKT_NETWORK_CHANGED: + $result['data_saving_enabled'] = (bool) (\unpack('V', \stream_get_contents($payload, 4))[1] & 1); + break; + // packetSwitchPreferredRelay#C relay_id:long = Packet; + case VoIP::PKT_SWITCH_PREF_RELAY: + $result['relay_id'] = Tools::unpackSignedLong(\stream_get_contents($payload, 8)); + break; + /*case \danog\MadelineProto\VoIP::PKT_SWITCH_TO_P2P: + break; + case \danog\MadelineProto\VoIP::PKT_NOP: + break;*/ + default: + Logger::log('Unknown packet received: '.$result['_'], Logger::ERROR); + continue 2; + } + return $result; } while (true); } /** * Write data. */ - public function write(string $payload): void + public function write(string $payload): bool { if ($this->socket === null) { - return; + return false; } $plaintext = \pack('v', \strlen($payload)).$payload; $padding = 16 - (\strlen($plaintext) % 16); @@ -158,6 +374,15 @@ final class Endpoint } $this->socket->write($payload); + return true; + } + public function udpPing(): bool + { + if ($this->socket === null) { + return false; + } + $this->socket->write($this->peerTag.Tools::packSignedLong(-1).Tools::packSignedInt(-1).Tools::packSignedInt(-2).Tools::random(8)); + return true; } /** * Get peer tag. diff --git a/src/VoIP/MessageHandler.php b/src/VoIP/MessageHandler.php index 965cd6660..579d52d85 100644 --- a/src/VoIP/MessageHandler.php +++ b/src/VoIP/MessageHandler.php @@ -15,9 +15,6 @@ If not, see . namespace danog\MadelineProto\VoIP; -use danog\MadelineProto\Lang; -use danog\MadelineProto\Logger; -use danog\MadelineProto\TL\Exception; use danog\MadelineProto\Tools; use danog\MadelineProto\VoIP; @@ -28,7 +25,7 @@ use danog\MadelineProto\VoIP; */ trait MessageHandler { - public function pack_string($object) + private static function pack_string(string $object): string { $l = \strlen($object); $concat = ''; @@ -45,33 +42,8 @@ trait MessageHandler return $concat; } - public function unpack_string($stream) + private function encryptPacket(array $args, bool $increase_seqno = true): string { - $l = \ord(\stream_get_contents($stream, 1)); - if ($l > 254) { - throw new Exception(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 = Tools::posmod(-$long_len, 4); - if ($resto > 0) { - \stream_get_contents($stream, $resto); - } - } else { - $x = \stream_get_contents($stream, $l); - $resto = Tools::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, \reset($this->sockets)); - } $message = ''; switch ($args['_']) { // streamTypeSimple codec:int8 = StreamType; @@ -200,7 +172,7 @@ trait MessageHandler $payload .= \chr(0); $payload .= $message; } elseif (\in_array($this->voip_state, [VoIP::STATE_WAIT_INIT, VoIP::STATE_WAIT_INIT_ACK], true)) { - $payload = $this->TLID_DECRYPTED_AUDIO_BLOCK; + $payload = VoIP::TLID_DECRYPTED_AUDIO_BLOCK; $payload .= Tools::random(8); $payload .= \chr(7); $payload .= Tools::random(7); @@ -225,7 +197,7 @@ trait MessageHandler $payload .= $this->pack_string($message); } } else { - $payload = $this->TLID_SIMPLE_AUDIO_BLOCK; + $payload = VoIP::TLID_SIMPLE_AUDIO_BLOCK; $payload .= Tools::random(8); $payload .= \chr(7); $payload .= Tools::random(7); @@ -233,208 +205,10 @@ trait MessageHandler $payload .= $this->pack_string($message); } - $this->session_out_seq_no++; - - return $datacenter->write($payload); - } - - /** - * Reading connection and receiving message from server. - */ - public function recv_message(Endpoint $endpoint) - { - if (!$payload = $endpoint->read()) { - return null; + if ($increase_seqno) { + $this->session_out_seq_no++; } - $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']) { - Logger::log('Call ID mismatch', Logger::ERROR); - return false; - } - } - if ($flags & 16) { - $in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; - $out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; - } - if ($flags & 32) { - $ack_mask = \unpack('V', \stream_get_contents($payload, 4))[1]; - } - if ($flags & 8) { - if (\stream_get_contents($payload, 4) !== VoIP::PROTO_ID) { - Logger::log('Protocol mismatch', Logger::ERROR); - return false; - } - } - if ($flags & 2) { - $result['extra'] = $this->unpack_string($payload); - } - $message = \fopen('php://memory', 'rw+b'); - - if ($flags & 1) { - \fwrite($message, $this->unpack_string($payload)); - \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($payload)); - \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'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - $result['query_id'] = Tools::unpackSignedLong(\stream_get_contents($payload, 8)); - $result['my_ip'] = \stream_get_contents($payload, 16); - $result['my_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - return $result; - case $this->TLID_REFLECTOR_PEER_INFO: - $result['my_address'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - $result['my_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - $result['peer_address'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - $result['peer_port'] = Tools::unpackSignedInt(\stream_get_contents($payload, 4)); - return $result; - default: - if ($this->peerVersion >= 8 || (!$this->peerVersion)) { - \fseek($payload, 0); - $result['_'] = \ord(\stream_get_contents($payload, 1)); - $in_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; - $out_seq_no = \unpack('V', \stream_get_contents($payload, 4))[1]; - $ack_mask = \unpack('V', \stream_get_contents($payload, 4))[1]; - $flags = \ord(\stream_get_contents($payload, 1)); - if ($flags & 1) { - $result['extra'] = []; - $count = \ord(\stream_get_contents($payload, 1)); - for ($x = 0; $x < $count; $x++) { - $len = \ord(\stream_get_contents($payload, 1)); - $result['extra'][]= \stream_get_contents($payload, $len); - } - } - $message = \fopen('php://memory', 'rw+b'); - - \fwrite($message, \stream_get_contents($payload)); - \fseek($message, 0); - } else { - Logger::log('Unknown packet received: '.\bin2hex($crc), Logger::ERROR); - return false; - } - } - if (!$this->received_packet($in_seq_no, $out_seq_no, $ack_mask)) { - return $this->recv_message($endpoint); - } - 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 VoIP::PKT_INIT: - $result['protocol'] = $this->peerVersion = Tools::unpackSignedInt(\stream_get_contents($message, 4)); - $result['min_protocol'] = Tools::unpackSignedInt(\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] = \stream_get_contents($message, 4); - } - 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 VoIP::PKT_INIT_ACK: - $result['protocol'] = Tools::unpackSignedInt(\stream_get_contents($message, 4)); - $result['min_protocol'] = Tools::unpackSignedInt(\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'] = \stream_get_contents($message, 4); - $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 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 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 VoIP::PKT_PONG: - if (\fstat($payload)['size'] - \ftell($payload)) { - $result['out_seq_no'] = \unpack('V', \stream_get_contents($payload, 4))[1]; - } - break; - case 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 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 VoIP::PKT_LAN_ENDPOINT: - $result['address'] = \unpack('V', \stream_get_contents($payload, 4))[1]; - $result['port'] = \unpack('V', \stream_get_contents($payload, 4))[1]; - break; - // packetNetworkChanged#B flags:# data_saving_enabled:flags.0?true = Packet; - case VoIP::PKT_NETWORK_CHANGED: - $result['data_saving_enabled'] = (bool) (\unpack('V', \stream_get_contents($payload, 4))[1] & 1); - break; - // packetSwitchPreferredRelay#C relay_id:long = Packet; - case VoIP::PKT_SWITCH_PREF_RELAY: - $result['relay_id'] = Tools::unpackSignedLong(\stream_get_contents($payload, 8)); - break; - /*case \danog\MadelineProto\VoIP::PKT_SWITCH_TO_P2P: - break; - case \danog\MadelineProto\VoIP::PKT_NOP: - break;*/ - } - return $result; + return $payload; } }