2023-08-13 11:40:35 +02:00
|
|
|
<?php declare(strict_types=1);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This file is part of MadelineProto.
|
|
|
|
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
|
|
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
* See the GNU Affero General Public License for more details.
|
|
|
|
* You should have received a copy of the GNU General Public License along with MadelineProto.
|
|
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* @author Daniil Gentili <daniil@daniil.it>
|
|
|
|
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
|
|
|
|
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
|
|
|
|
* @link https://docs.madelineproto.xyz MadelineProto documentation
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Please keep the above notice the next time you copy my code, or I will sue you :)
|
2023-08-12 17:29:00 +02:00
|
|
|
|
|
|
|
namespace danog\MadelineProto;
|
|
|
|
|
2023-09-03 14:54:48 +02:00
|
|
|
use Amp\ByteStream\BufferedReader;
|
|
|
|
use Amp\ByteStream\ReadableBuffer;
|
2023-08-13 18:31:31 +02:00
|
|
|
use Amp\ByteStream\ReadableStream;
|
2023-08-19 18:29:55 +02:00
|
|
|
use danog\Loop\Loop;
|
2023-08-19 21:20:51 +02:00
|
|
|
use danog\MadelineProto\Loop\VoIP\DjLoop;
|
2023-08-12 17:29:00 +02:00
|
|
|
use danog\MadelineProto\MTProtoTools\Crypt;
|
|
|
|
use danog\MadelineProto\VoIP\CallState;
|
2023-08-12 21:47:09 +02:00
|
|
|
use danog\MadelineProto\VoIP\DiscardReason;
|
2023-08-12 17:29:00 +02:00
|
|
|
use danog\MadelineProto\VoIP\Endpoint;
|
|
|
|
use danog\MadelineProto\VoIP\MessageHandler;
|
|
|
|
use danog\MadelineProto\VoIP\VoIPState;
|
|
|
|
use phpseclib3\Math\BigInteger;
|
|
|
|
use Revolt\EventLoop;
|
|
|
|
use Throwable;
|
|
|
|
use Webmozart\Assert\Assert;
|
|
|
|
|
2023-08-20 18:36:59 +02:00
|
|
|
use function Amp\async;
|
2023-08-12 17:29:00 +02:00
|
|
|
use function Amp\delay;
|
2023-08-20 18:36:59 +02:00
|
|
|
use function Amp\Future\await;
|
2023-08-12 17:29:00 +02:00
|
|
|
|
|
|
|
/** @internal */
|
|
|
|
final class VoIPController
|
|
|
|
{
|
2023-09-02 14:53:34 +02:00
|
|
|
public const CALL_PROTOCOL = [
|
|
|
|
'_' => 'phoneCallProtocol',
|
|
|
|
'udp_p2p' => true,
|
|
|
|
'udp_reflector' => true,
|
|
|
|
'min_layer' => 65,
|
|
|
|
'max_layer' => 92,
|
2023-09-02 21:07:54 +02:00
|
|
|
'library_versions' => [
|
2023-09-02 14:53:34 +02:00
|
|
|
"2.4.4",
|
|
|
|
"2.7.7",
|
|
|
|
"5.0.0",
|
|
|
|
"6.0.0",
|
|
|
|
"7.0.0",
|
|
|
|
"8.0.0",
|
|
|
|
"9.0.0",
|
|
|
|
"10.0.0",
|
|
|
|
"11.0.0"
|
2023-09-02 21:07:54 +02:00
|
|
|
]
|
2023-09-02 14:53:34 +02:00
|
|
|
];
|
2023-08-12 17:29:00 +02:00
|
|
|
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 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 = "\xc1\xdb\xf9\x48";
|
|
|
|
const TLID_SIMPLE_AUDIO_BLOCK = "\x0d\x0e\x76\xcc";
|
|
|
|
|
|
|
|
const TLID_REFLECTOR_SELF_INFO = "\xC7\x72\x15\xc0";
|
|
|
|
const TLID_REFLECTOR_PEER_INFO = "\x1C\x37\xD9\x27";
|
|
|
|
|
|
|
|
const PROTO_ID = 'GrVP';
|
|
|
|
|
|
|
|
const PROTOCOL_VERSION = 9;
|
|
|
|
const MIN_PROTOCOL_VERSION = 9;
|
|
|
|
|
|
|
|
const STREAM_TYPE_AUDIO = 1;
|
|
|
|
const STREAM_TYPE_VIDEO = 2;
|
|
|
|
|
|
|
|
const CODEC_OPUS = 'SUPO';
|
|
|
|
|
|
|
|
private MessageHandler $messageHandler;
|
|
|
|
private VoIPState $voipState = VoIPState::CREATED;
|
|
|
|
private CallState $callState;
|
|
|
|
|
|
|
|
private array $call;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array<Endpoint>
|
|
|
|
*/
|
|
|
|
private array $sockets = [];
|
2023-08-12 21:47:09 +02:00
|
|
|
private Endpoint $bestEndpoint;
|
2023-08-12 22:13:23 +02:00
|
|
|
private ?string $pendingPing = null;
|
2023-08-12 17:29:00 +02:00
|
|
|
private ?string $timeoutWatcher = null;
|
|
|
|
private float $lastIncomingTimestamp = 0.0;
|
2023-08-12 17:45:16 +02:00
|
|
|
private float $lastOutgoingTimestamp = 0.0;
|
2023-08-12 17:29:00 +02:00
|
|
|
private int $opusTimestamp = 0;
|
2023-08-14 19:12:59 +02:00
|
|
|
|
2023-08-19 21:20:51 +02:00
|
|
|
private DjLoop $diskJockey;
|
2023-08-12 17:29:00 +02:00
|
|
|
|
|
|
|
/** Auth key */
|
|
|
|
private readonly string $authKey;
|
|
|
|
|
|
|
|
public readonly VoIP $public;
|
|
|
|
/** @var ?list{string, string, string, string} */
|
|
|
|
private ?array $visualization = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor.
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function __construct(
|
2023-08-19 18:29:55 +02:00
|
|
|
public readonly MTProto $API,
|
2023-08-12 17:29:00 +02:00
|
|
|
array $call
|
|
|
|
) {
|
|
|
|
$this->public = new VoIP($API, $call);
|
|
|
|
$call['_'] = 'inputPhoneCall';
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey = new DjLoop($this);
|
|
|
|
Assert::true($this->diskJockey->start());
|
2023-08-12 17:29:00 +02:00
|
|
|
$this->call = $call;
|
2023-08-12 21:47:09 +02:00
|
|
|
if ($this->public->outgoing) {
|
2023-08-12 17:29:00 +02:00
|
|
|
$this->callState = CallState::REQUESTED;
|
2023-08-12 21:47:09 +02:00
|
|
|
} else {
|
|
|
|
$this->callState = CallState::INCOMING;
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-14 20:17:19 +02:00
|
|
|
public function __serialize(): array
|
|
|
|
{
|
2023-08-19 21:20:51 +02:00
|
|
|
return \get_object_vars($this);
|
2023-08-14 20:17:19 +02:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Wakeup function.
|
|
|
|
*/
|
|
|
|
public function __unserialize(array $data): void
|
|
|
|
{
|
|
|
|
foreach ($data as $key => $value) {
|
|
|
|
$this->{$key} = $value;
|
|
|
|
}
|
2023-08-19 21:20:51 +02:00
|
|
|
if (!isset($this->API->logger)) {
|
|
|
|
$this->API->setupLogger();
|
|
|
|
}
|
|
|
|
$this->diskJockey ??= new DjLoop($this);
|
|
|
|
Assert::true($this->diskJockey->start());
|
2023-08-20 19:07:14 +02:00
|
|
|
EventLoop::queue(function (): void {
|
2023-08-20 18:31:21 +02:00
|
|
|
if ($this->callState === CallState::RUNNING) {
|
|
|
|
if ($this->voipState === VoIPState::CREATED) {
|
|
|
|
// No endpoints yet
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->lastIncomingTimestamp = \microtime(true);
|
|
|
|
$this->connectToAll();
|
|
|
|
if ($this->voipState === VoIPState::WAIT_PONG) {
|
|
|
|
$this->pendingPing = EventLoop::repeat(0.2, $this->ping(...));
|
|
|
|
} elseif ($this->voipState === VoIPState::WAIT_STREAM_INIT) {
|
|
|
|
$this->initStream();
|
|
|
|
} elseif ($this->voipState === VoIPState::ESTABLISHED) {
|
|
|
|
$diff = (int) ((\microtime(true) - $this->lastOutgoingTimestamp) * 1000);
|
|
|
|
$this->opusTimestamp += $diff - ($diff % 60);
|
|
|
|
$this->startWriteLoop();
|
|
|
|
}
|
2023-08-14 20:17:19 +02:00
|
|
|
}
|
2023-08-20 18:31:21 +02:00
|
|
|
});
|
2023-08-14 20:17:19 +02:00
|
|
|
}
|
|
|
|
|
2023-08-12 17:29:00 +02:00
|
|
|
/**
|
|
|
|
* Confirm requested call.
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function confirm(array $params): bool
|
|
|
|
{
|
|
|
|
if ($this->callState !== CallState::REQUESTED) {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_error_2'], $this->public->callID));
|
2023-08-12 17:29:00 +02:00
|
|
|
return false;
|
|
|
|
}
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_confirming'], $this->public->otherID), Logger::VERBOSE);
|
2023-08-12 17:29:00 +02:00
|
|
|
$dh_config = $this->API->getDhConfig();
|
|
|
|
$params['g_b'] = new BigInteger((string) $params['g_b'], 256);
|
|
|
|
Crypt::checkG($params['g_b'], $dh_config['p']);
|
|
|
|
$key = \str_pad($params['g_b']->powMod($this->call['a'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
|
|
|
|
try {
|
2023-09-02 14:53:34 +02:00
|
|
|
$res = ($this->API->methodCallAsyncRead('phone.confirmCall', [
|
|
|
|
'key_fingerprint' => \substr(\sha1($key, true), -8),
|
|
|
|
'peer' => ['id' => $params['id'], 'access_hash' => $params['access_hash'], '_' => 'inputPhoneCall'],
|
|
|
|
'g_a' => $this->call['g_a'],
|
|
|
|
'protocol' => self::CALL_PROTOCOL
|
|
|
|
]))['phone_call'];
|
2023-08-12 17:29:00 +02:00
|
|
|
} catch (RPCErrorException $e) {
|
|
|
|
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_already_accepted'], $params['id']));
|
2023-08-12 17:29:00 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(Lang::$current_lang['call_already_declined']);
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->discard(DiscardReason::HANGUP);
|
2023-08-12 17:29:00 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
$visualization = [];
|
|
|
|
$length = new BigInteger(\count(Magic::$emojis));
|
|
|
|
foreach (\str_split(\hash('sha256', $key.\str_pad($this->call['g_a'], 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
|
|
|
|
$number[0] = \chr(\ord($number[0]) & 0x7f);
|
|
|
|
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
|
|
|
|
}
|
|
|
|
$this->visualization = $visualization;
|
|
|
|
$this->authKey = $key;
|
|
|
|
$this->callState = CallState::RUNNING;
|
|
|
|
$this->messageHandler = new MessageHandler(
|
|
|
|
$this,
|
|
|
|
\substr(\hash('sha256', $key, true), -16)
|
|
|
|
);
|
|
|
|
$this->initialize($res['connections']);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Accept incoming call.
|
|
|
|
*/
|
|
|
|
public function accept(): self
|
|
|
|
{
|
|
|
|
if ($this->callState === CallState::RUNNING || $this->callState === CallState::ENDED) {
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
Assert::eq($this->callState->name, CallState::INCOMING->name);
|
|
|
|
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['accepting_call'], $this->public->otherID), Logger::VERBOSE);
|
2023-08-12 17:29:00 +02:00
|
|
|
$dh_config = $this->API->getDhConfig();
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log('Generating b...', Logger::VERBOSE);
|
2023-08-12 17:29:00 +02:00
|
|
|
$b = BigInteger::randomRange(Magic::$two, $dh_config['p']->subtract(Magic::$two));
|
|
|
|
$g_b = $dh_config['g']->powMod($b, $dh_config['p']);
|
|
|
|
Crypt::checkG($g_b, $dh_config['p']);
|
2023-08-12 21:47:09 +02:00
|
|
|
|
|
|
|
$this->callState = CallState::ACCEPTED;
|
2023-08-12 17:29:00 +02:00
|
|
|
try {
|
2023-09-02 14:53:34 +02:00
|
|
|
$this->API->methodCallAsyncRead('phone.acceptCall', [
|
|
|
|
'peer' => [
|
|
|
|
'id' => $this->call['id'],
|
|
|
|
'access_hash' => $this->call['access_hash'],
|
|
|
|
'_' => 'inputPhoneCall'
|
|
|
|
],
|
|
|
|
'g_b' => $g_b->toBytes(),
|
|
|
|
'protocol' => self::CALL_PROTOCOL
|
|
|
|
]);
|
2023-08-12 17:29:00 +02:00
|
|
|
} catch (RPCErrorException $e) {
|
|
|
|
if ($e->rpc === 'CALL_ALREADY_ACCEPTED') {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_already_accepted'], $this->public->callID));
|
2023-08-12 17:29:00 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
if ($e->rpc === 'CALL_ALREADY_DECLINED') {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(Lang::$current_lang['call_already_declined']);
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->discard(DiscardReason::HANGUP);
|
2023-08-12 17:29:00 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
$this->call['b'] = $b;
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Complete call handshake.
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function complete(array $params): bool
|
|
|
|
{
|
|
|
|
if ($this->callState !== CallState::ACCEPTED) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_completing'], $this->public->otherID), Logger::VERBOSE);
|
2023-08-12 17:29:00 +02:00
|
|
|
$dh_config = $this->API->getDhConfig();
|
2023-08-12 21:47:09 +02:00
|
|
|
if (\hash('sha256', (string) $params['g_a_or_b'], true) !== (string) $this->call['g_a_hash']) {
|
2023-08-12 17:29:00 +02:00
|
|
|
throw new SecurityException('Invalid g_a!');
|
|
|
|
}
|
|
|
|
$params['g_a_or_b'] = new BigInteger((string) $params['g_a_or_b'], 256);
|
|
|
|
Crypt::checkG($params['g_a_or_b'], $dh_config['p']);
|
|
|
|
$key = \str_pad($params['g_a_or_b']->powMod($this->call['b'], $dh_config['p'])->toBytes(), 256, \chr(0), STR_PAD_LEFT);
|
|
|
|
if (\substr(\sha1($key, true), -8) != $params['key_fingerprint']) {
|
|
|
|
throw new SecurityException(Lang::$current_lang['fingerprint_invalid']);
|
|
|
|
}
|
|
|
|
$visualization = [];
|
|
|
|
$length = new BigInteger(\count(Magic::$emojis));
|
|
|
|
foreach (\str_split(\hash('sha256', $key.\str_pad($params['g_a_or_b']->toBytes(), 256, \chr(0), STR_PAD_LEFT), true), 8) as $number) {
|
|
|
|
$number[0] = \chr(\ord($number[0]) & 0x7f);
|
|
|
|
$visualization[] = Magic::$emojis[(int) (new BigInteger($number, 256))->divide($length)[1]->toString()];
|
|
|
|
}
|
|
|
|
$this->visualization = $visualization;
|
|
|
|
$this->authKey = $key;
|
|
|
|
$this->callState = CallState::RUNNING;
|
|
|
|
$this->messageHandler = new MessageHandler(
|
|
|
|
$this,
|
|
|
|
\substr(\hash('sha256', $key, true), -16)
|
|
|
|
);
|
|
|
|
$this->initialize($params['connections']);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get call emojis (will return null if the call is not inited yet).
|
|
|
|
*
|
|
|
|
* @return ?list{string, string, string, string}
|
|
|
|
*/
|
|
|
|
public function getVisualization(): ?array
|
|
|
|
{
|
|
|
|
return $this->visualization;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Discard call.
|
2023-08-12 21:47:09 +02:00
|
|
|
*
|
|
|
|
* @param int<1, 5> $rating Call rating in stars
|
|
|
|
* @param string $comment Additional comment on call quality.
|
2023-08-12 17:29:00 +02:00
|
|
|
*/
|
2023-08-12 21:47:09 +02:00
|
|
|
public function discard(DiscardReason $reason = DiscardReason::HANGUP, ?int $rating = null, ?string $comment = null): self
|
2023-08-12 17:29:00 +02:00
|
|
|
{
|
|
|
|
if ($this->callState === CallState::ENDED) {
|
|
|
|
return $this;
|
|
|
|
}
|
2023-08-29 20:00:13 +02:00
|
|
|
$this->API->waitForInit();
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->API->cleanupCall($this->public->callID);
|
2023-08-14 20:17:19 +02:00
|
|
|
$this->callState = CallState::ENDED;
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey->discard();
|
2023-08-14 20:17:19 +02:00
|
|
|
$this->skip();
|
2023-08-12 21:47:09 +02:00
|
|
|
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Now closing $this");
|
2023-08-12 17:29:00 +02:00
|
|
|
if (isset($this->timeoutWatcher)) {
|
|
|
|
EventLoop::cancel($this->timeoutWatcher);
|
|
|
|
}
|
|
|
|
|
2023-08-12 22:13:23 +02:00
|
|
|
if (isset($this->pendingPing)) {
|
|
|
|
EventLoop::cancel($this->pendingPing);
|
|
|
|
}
|
|
|
|
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Closing all sockets in $this");
|
2023-08-12 17:29:00 +02:00
|
|
|
foreach ($this->sockets as $socket) {
|
|
|
|
$socket->disconnect();
|
|
|
|
}
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Closed all sockets, discarding $this");
|
2023-08-12 17:29:00 +02:00
|
|
|
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf(Lang::$current_lang['call_discarding'], $this->public->callID), Logger::VERBOSE);
|
2023-08-12 17:29:00 +02:00
|
|
|
try {
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->API->methodCallAsyncRead('phone.discardCall', ['peer' => $this->call, 'duration' => \time() - $this->public->date, 'connection_id' => 0, 'reason' => ['_' => match ($reason) {
|
|
|
|
DiscardReason::BUSY => 'phoneCallDiscardReasonBusy',
|
|
|
|
DiscardReason::HANGUP => 'phoneCallDiscardReasonHangup',
|
|
|
|
DiscardReason::DISCONNECTED => 'phoneCallDiscardReasonDisconnect',
|
|
|
|
DiscardReason::MISSED => 'phoneCallDiscardReasonMissed'
|
|
|
|
}]]);
|
2023-08-12 17:29:00 +02:00
|
|
|
} catch (RPCErrorException $e) {
|
|
|
|
if (!\in_array($e->rpc, ['CALL_ALREADY_DECLINED', 'CALL_ALREADY_ACCEPTED'], true)) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
}
|
2023-08-12 21:47:09 +02:00
|
|
|
if ($rating !== null) {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log(\sprintf('Setting rating for call %s...', $this->call), Logger::VERBOSE);
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->API->methodCallAsyncRead('phone.setCallRating', ['peer' => $this->call, 'rating' => $rating, 'comment' => $comment]);
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
2023-09-03 18:35:50 +02:00
|
|
|
|
2023-09-02 21:07:54 +02:00
|
|
|
private const SIGNALING_MIN_SIZE = 21;
|
|
|
|
private const SIGNALING_MAX_SIZE = 128 * 1024 * 1024;
|
2023-09-02 14:53:34 +02:00
|
|
|
public function onSignaling(string $data): void
|
|
|
|
{
|
2023-09-03 18:35:50 +02:00
|
|
|
if (\strlen($data) < self::SIGNALING_MIN_SIZE || \strlen($data) > self::SIGNALING_MAX_SIZE) {
|
2023-09-02 21:07:54 +02:00
|
|
|
Logger::log('Wrong size in signaling!', Logger::ERROR);
|
|
|
|
return;
|
|
|
|
}
|
2023-09-03 18:35:50 +02:00
|
|
|
$message_key = \substr($data, 0, 16);
|
|
|
|
$data = \substr($data, 16);
|
2023-09-02 21:07:54 +02:00
|
|
|
[$aes_key, $aes_iv, $x] = Crypt::voipKdf($message_key, $this->authKey, $this->public->outgoing, false);
|
|
|
|
$packet = Crypt::ctrEncrypt($data, $aes_key, $aes_iv);
|
|
|
|
|
|
|
|
if ($message_key != \substr(\hash('sha256', \substr($this->authKey, 88 + $x, 32).$packet, true), 8, 16)) {
|
|
|
|
Logger::log('msg_key mismatch!', Logger::ERROR);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-03 14:54:48 +02:00
|
|
|
$packet = new BufferedReader(new ReadableBuffer($packet));
|
|
|
|
|
|
|
|
$packets = [];
|
|
|
|
while ($packet->isReadable()) {
|
2023-09-03 18:35:50 +02:00
|
|
|
$seq = \unpack('N', $packet->readLength(4))[1];
|
|
|
|
$length = \unpack('N', $packet->readLength(4))[1];
|
2023-09-03 14:54:48 +02:00
|
|
|
$packets []= self::deserializeRtc($packet);
|
2023-09-02 21:07:54 +02:00
|
|
|
}
|
2023-09-03 14:54:48 +02:00
|
|
|
}
|
|
|
|
|
2023-09-03 18:35:50 +02:00
|
|
|
public static function deserializeRtc(BufferedReader $buffer): array
|
|
|
|
{
|
|
|
|
switch ($t = \ord($buffer->readLength(1))) {
|
2023-09-03 14:54:48 +02:00
|
|
|
case 1:
|
|
|
|
$candidates = [];
|
2023-09-03 18:35:50 +02:00
|
|
|
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
|
2023-09-03 14:54:48 +02:00
|
|
|
$candidates []= self::readString($buffer);
|
|
|
|
}
|
|
|
|
return [
|
|
|
|
'_' => 'candidatesList',
|
|
|
|
'ufrag' => self::readString($buffer),
|
|
|
|
'pwd' => self::readString($buffer),
|
|
|
|
];
|
|
|
|
case 2:
|
|
|
|
$formats = [];
|
2023-09-03 18:35:50 +02:00
|
|
|
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
|
2023-09-03 14:54:48 +02:00
|
|
|
$name = self::readString($buffer);
|
|
|
|
$parameters = [];
|
2023-09-03 18:35:50 +02:00
|
|
|
for ($x = \ord($buffer->readLength(1)); $x > 0; $x--) {
|
2023-09-03 14:54:48 +02:00
|
|
|
$key = self::readString($buffer);
|
|
|
|
$value = self::readString($buffer);
|
|
|
|
$parameters[$key] = $value;
|
|
|
|
}
|
|
|
|
$formats[]= [
|
|
|
|
'name' => $name,
|
|
|
|
'parameters' => $parameters
|
|
|
|
];
|
|
|
|
}
|
|
|
|
return [
|
|
|
|
'_' => 'videoFormats',
|
|
|
|
'formats' => $formats,
|
2023-09-03 18:35:50 +02:00
|
|
|
'encoders' => \ord($buffer->readLength(1)),
|
2023-09-03 14:54:48 +02:00
|
|
|
];
|
|
|
|
case 3:
|
|
|
|
return ['_' => 'requestVideo'];
|
|
|
|
case 4:
|
2023-09-03 18:35:50 +02:00
|
|
|
$state = \ord($buffer->readLength(1));
|
2023-09-03 14:54:48 +02:00
|
|
|
return ['_' => 'remoteMediaState', 'audio' => $state & 0x01, 'video' => ($state >> 1) & 0x03];
|
|
|
|
case 5:
|
|
|
|
return ['_' => 'audioData', 'data' => self::readBuffer($buffer)];
|
|
|
|
case 6:
|
|
|
|
return ['_' => 'videoData', 'data' => self::readBuffer($buffer)];
|
|
|
|
case 7:
|
|
|
|
return ['_' => 'unstructuredData', 'data' => self::readBuffer($buffer)];
|
|
|
|
case 8:
|
2023-09-03 18:35:50 +02:00
|
|
|
return ['_' => 'videoParameters', 'aspectRatio' => \unpack('V', $buffer->readLength(4))[1]];
|
2023-09-03 14:54:48 +02:00
|
|
|
case 9:
|
2023-09-03 18:35:50 +02:00
|
|
|
return ['_' => 'remoteBatteryLevelIsLow', 'isLow' => (bool) \ord($buffer->readLength(1))];
|
2023-09-03 14:54:48 +02:00
|
|
|
case 10:
|
2023-09-03 18:35:50 +02:00
|
|
|
$lowCost = (bool) \ord($buffer->readLength(1));
|
|
|
|
$isLowDataRequested = (bool) \ord($buffer->readLength(1));
|
2023-09-03 14:54:48 +02:00
|
|
|
return ['_' => 'remoteNetworkStatus', 'lowCost' => $lowCost, 'isLowDataRequested' => $isLowDataRequested];
|
|
|
|
}
|
|
|
|
return ['_' => 'unknown', 'type' => $t];
|
|
|
|
}
|
2023-09-03 18:35:50 +02:00
|
|
|
private static function readString(BufferedReader $buffer): string
|
|
|
|
{
|
|
|
|
return $buffer->readLength(\ord($buffer->readLength(1)));
|
2023-09-03 14:54:48 +02:00
|
|
|
}
|
2023-09-03 18:35:50 +02:00
|
|
|
private static function readBuffer(BufferedReader $buffer): string
|
|
|
|
{
|
|
|
|
return $buffer->readLength(\unpack('n', $buffer->readLength(2))[1]);
|
2023-09-02 14:53:34 +02:00
|
|
|
}
|
2023-08-12 17:29:00 +02:00
|
|
|
|
2023-08-19 18:29:55 +02:00
|
|
|
private function setVoipState(VoIPState $state): bool
|
|
|
|
{
|
|
|
|
if ($this->voipState->value >= $state->value) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$old = $this->voipState;
|
|
|
|
$this->voipState = $state;
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Changing state from {$old->name} to {$state->name} in $this");
|
2023-08-19 18:29:55 +02:00
|
|
|
return true;
|
|
|
|
}
|
2023-08-12 17:29:00 +02:00
|
|
|
/**
|
|
|
|
* Connect to the specified endpoints.
|
|
|
|
*/
|
|
|
|
private function initialize(array $endpoints): void
|
|
|
|
{
|
2023-08-20 16:05:52 +02:00
|
|
|
foreach ([true, false] as $udp) {
|
|
|
|
foreach ($endpoints as $endpoint) {
|
2023-09-02 14:53:34 +02:00
|
|
|
if ($endpoint['_'] !== 'phoneConnection') {
|
|
|
|
continue;
|
|
|
|
}
|
2023-08-20 16:58:35 +02:00
|
|
|
if (!isset($this->sockets[($udp ? 'udp' : 'tcp').' v6 '.$endpoint['id']])) {
|
|
|
|
$this->sockets[($udp ? 'udp' : 'tcp').' v6 '.$endpoint['id']] = new Endpoint(
|
|
|
|
$udp,
|
|
|
|
'['.$endpoint['ipv6'].']',
|
|
|
|
$endpoint['port'],
|
|
|
|
$endpoint['peer_tag'],
|
|
|
|
true,
|
|
|
|
$this->public->outgoing,
|
|
|
|
$this->authKey,
|
|
|
|
$this->messageHandler
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (!isset($this->sockets[($udp ? 'udp' : 'tcp').' v4 '.$endpoint['id']])) {
|
|
|
|
$this->sockets[($udp ? 'udp' : 'tcp').' v4 '.$endpoint['id']] = new Endpoint(
|
|
|
|
$udp,
|
|
|
|
$endpoint['ip'],
|
|
|
|
$endpoint['port'],
|
|
|
|
$endpoint['peer_tag'],
|
|
|
|
true,
|
|
|
|
$this->public->outgoing,
|
|
|
|
$this->authKey,
|
|
|
|
$this->messageHandler
|
|
|
|
);
|
|
|
|
}
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-19 18:29:55 +02:00
|
|
|
$this->setVoipState(VoIPState::WAIT_INIT);
|
2023-08-20 16:05:52 +02:00
|
|
|
$this->connectToAll();
|
2023-08-19 18:29:55 +02:00
|
|
|
}
|
2023-08-20 17:15:19 +02:00
|
|
|
private function connectToAll(): void
|
|
|
|
{
|
2023-08-20 23:54:12 +02:00
|
|
|
$this->timeoutWatcher = EventLoop::repeat(10, function (): void {
|
2023-08-20 23:56:31 +02:00
|
|
|
if (\microtime(true) - $this->lastIncomingTimestamp > 10) {
|
2023-08-20 23:54:12 +02:00
|
|
|
$this->discard(DiscardReason::DISCONNECTED);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-08-20 18:36:59 +02:00
|
|
|
$promises = [];
|
2023-08-12 17:29:00 +02:00
|
|
|
foreach ($this->sockets as $socket) {
|
2023-08-20 18:36:59 +02:00
|
|
|
$promise = async(function () use ($socket): void {
|
2023-08-20 16:05:52 +02:00
|
|
|
try {
|
|
|
|
$this->log("Connecting to $socket...");
|
|
|
|
$socket->connect();
|
|
|
|
$this->log("Successfully connected to $socket!");
|
|
|
|
$this->startReadLoop($socket);
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
$this->log("Got error while connecting to $socket: $e");
|
|
|
|
}
|
|
|
|
});
|
2023-08-20 19:07:14 +02:00
|
|
|
if ((!isset($this->bestEndpoint) && $socket->udp) || (isset($this->bestEndpoint) && $socket === $this->bestEndpoint)) {
|
2023-08-20 18:36:59 +02:00
|
|
|
$promises []= $promise;
|
|
|
|
}
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
2023-08-20 18:36:59 +02:00
|
|
|
await($promises);
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Handle incoming packet.
|
|
|
|
*/
|
|
|
|
private function handlePacket(Endpoint $socket, array $packet): void
|
|
|
|
{
|
|
|
|
switch ($packet['_']) {
|
|
|
|
case self::PKT_INIT:
|
2023-08-19 18:29:55 +02:00
|
|
|
$this->setVoipState(VoIPState::WAIT_INIT_ACK);
|
2023-08-12 17:29:00 +02:00
|
|
|
$socket->write($this->messageHandler->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]
|
|
|
|
]
|
|
|
|
]));
|
2023-08-20 16:05:52 +02:00
|
|
|
$socket->sendInit();
|
2023-08-12 17:29:00 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case self::PKT_INIT_ACK:
|
2023-08-19 18:29:55 +02:00
|
|
|
if ($this->setVoipState(VoIPState::WAIT_PONG)) {
|
2023-08-12 22:13:23 +02:00
|
|
|
$this->pendingPing = EventLoop::repeat(0.2, $this->ping(...));
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case self::PKT_STREAM_DATA:
|
|
|
|
$cnt = 1;
|
|
|
|
break;
|
|
|
|
case self::PKT_STREAM_DATA_X2:
|
|
|
|
$cnt = 2;
|
|
|
|
break;
|
|
|
|
case self::PKT_STREAM_DATA_X3:
|
|
|
|
$cnt = 3;
|
|
|
|
break;
|
|
|
|
case self::PKT_PING:
|
|
|
|
$socket->write($this->messageHandler->encryptPacket(['_' => self::PKT_PONG, 'out_seq_no' => $packet['out_seq_no']]));
|
|
|
|
break;
|
|
|
|
case self::PKT_PONG:
|
2023-08-19 18:29:55 +02:00
|
|
|
if ($this->setVoipState(VoIPState::WAIT_STREAM_INIT)) {
|
2023-08-12 22:13:23 +02:00
|
|
|
EventLoop::cancel($this->pendingPing);
|
|
|
|
$this->pendingPing = null;
|
2023-08-19 18:29:55 +02:00
|
|
|
$this->bestEndpoint ??= $socket;
|
|
|
|
$this->initStream();
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-08-19 18:29:55 +02:00
|
|
|
private function initStream(): void
|
|
|
|
{
|
|
|
|
$this->bestEndpoint->writeReliably([
|
|
|
|
'_' => self::PKT_STREAM_STATE,
|
|
|
|
'id' => 0,
|
|
|
|
'enabled' => false
|
|
|
|
]);
|
|
|
|
|
|
|
|
$this->startWriteLoop();
|
|
|
|
}
|
2023-08-12 22:13:23 +02:00
|
|
|
private function ping(): void
|
|
|
|
{
|
|
|
|
foreach ($this->sockets as $socket) {
|
|
|
|
EventLoop::queue(fn () => $socket->write($this->messageHandler->encryptPacket(['_' => self::PKT_PING])));
|
|
|
|
}
|
|
|
|
}
|
2023-08-20 16:05:52 +02:00
|
|
|
private function startReadLoop(Endpoint $endpoint): void
|
2023-08-12 17:29:00 +02:00
|
|
|
{
|
2023-08-20 16:05:52 +02:00
|
|
|
EventLoop::queue(function () use ($endpoint): void {
|
2023-08-20 17:15:19 +02:00
|
|
|
EventLoop::queue(function () use ($endpoint): void {
|
2023-08-20 17:09:14 +02:00
|
|
|
while ($this->voipState->value <= VoIPState::WAIT_INIT_ACK->value) {
|
|
|
|
$this->log("Sending PKT_INIT to $endpoint...");
|
2023-08-21 09:32:35 +02:00
|
|
|
if (!$endpoint->sendInit()) {
|
|
|
|
return;
|
|
|
|
}
|
2023-08-21 18:14:19 +02:00
|
|
|
delay(0.5);
|
2023-08-20 17:09:14 +02:00
|
|
|
}
|
|
|
|
});
|
2023-08-20 16:05:52 +02:00
|
|
|
$this->log("Started read loop in $endpoint!");
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
$payload = $endpoint->read();
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
$this->log("Got $e in $endpoint, $this!");
|
|
|
|
continue;
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
2023-08-20 16:05:52 +02:00
|
|
|
if (!$payload) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$this->lastIncomingTimestamp = \microtime(true);
|
|
|
|
EventLoop::queue($this->handlePacket(...), $endpoint, $payload);
|
|
|
|
}
|
|
|
|
$this->log("Exiting VoIP read loop in $endpoint, $this!");
|
|
|
|
});
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
2023-08-14 18:28:44 +02:00
|
|
|
|
2023-08-20 14:17:21 +02:00
|
|
|
public function log(string $message, int $level = Logger::NOTICE): void
|
2023-08-19 19:10:34 +02:00
|
|
|
{
|
2023-08-30 18:14:52 +02:00
|
|
|
$this->API->logger->logger($message, $level);
|
2023-08-19 19:10:34 +02:00
|
|
|
}
|
2023-08-19 18:29:55 +02:00
|
|
|
private bool $muted = true;
|
2023-08-12 17:29:00 +02:00
|
|
|
/**
|
|
|
|
* Start write loop.
|
|
|
|
*/
|
|
|
|
private function startWriteLoop(): void
|
|
|
|
{
|
2023-08-19 18:29:55 +02:00
|
|
|
$this->setVoipState(VoIPState::ESTABLISHED);
|
2023-08-12 17:29:00 +02:00
|
|
|
|
2023-08-12 21:47:09 +02:00
|
|
|
$delay = $this->muted ? 0.2 : 0.06;
|
|
|
|
$t = \microtime(true) + $delay;
|
2023-08-12 17:29:00 +02:00
|
|
|
while (true) {
|
2023-08-19 21:20:51 +02:00
|
|
|
if ($packet = $this->diskJockey->pullPacket()) {
|
2023-08-12 21:47:09 +02:00
|
|
|
if ($this->muted) {
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->log("Unmuting outgoing audio in $this!");
|
2023-08-14 17:16:59 +02:00
|
|
|
if (!$this->bestEndpoint->writeReliably([
|
2023-08-12 21:47:09 +02:00
|
|
|
'_' => self::PKT_STREAM_STATE,
|
|
|
|
'id' => 0,
|
|
|
|
'enabled' => true
|
2023-08-14 17:16:59 +02:00
|
|
|
])) {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Exiting write loop in $this because we could not write stream state!");
|
2023-08-14 17:16:59 +02:00
|
|
|
return;
|
|
|
|
}
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->muted = false;
|
|
|
|
$delay = 0.06;
|
|
|
|
$this->opusTimestamp = 0;
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
2023-08-12 21:47:09 +02:00
|
|
|
$packet = $this->messageHandler->encryptPacket([
|
|
|
|
'_' => self::PKT_STREAM_DATA,
|
|
|
|
'stream_id' => 0,
|
2023-08-14 17:16:59 +02:00
|
|
|
'data' => $packet,
|
2023-08-12 21:47:09 +02:00
|
|
|
'timestamp' => $this->opusTimestamp
|
|
|
|
]);
|
|
|
|
$this->opusTimestamp += 60;
|
2023-08-14 17:16:59 +02:00
|
|
|
} else {
|
|
|
|
if (!$this->muted) {
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->log("Muting outgoing audio in $this!");
|
2023-08-14 17:16:59 +02:00
|
|
|
if (!$this->bestEndpoint->writeReliably([
|
|
|
|
'_' => self::PKT_STREAM_STATE,
|
|
|
|
'id' => 0,
|
|
|
|
'enabled' => false
|
|
|
|
])) {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Exiting write loop in $this because we could not write stream state!");
|
2023-08-14 17:16:59 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
$this->muted = true;
|
|
|
|
$delay = 0.2;
|
|
|
|
}
|
|
|
|
$packet = $this->messageHandler->encryptPacket([
|
|
|
|
'_' => self::PKT_NOP
|
|
|
|
]);
|
2023-08-12 21:47:09 +02:00
|
|
|
}
|
2023-08-19 19:10:34 +02:00
|
|
|
//$this->log("Writing {$this->opusTimestamp} in $this!");
|
2023-08-12 21:47:09 +02:00
|
|
|
$cur = \microtime(true);
|
|
|
|
$diff = $t - $cur;
|
|
|
|
if ($diff > 0) {
|
|
|
|
delay($diff);
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
|
2023-08-14 17:16:59 +02:00
|
|
|
if (!$this->bestEndpoint->write($packet)) {
|
2023-08-19 19:10:34 +02:00
|
|
|
$this->log("Exiting write loop in $this!");
|
2023-08-14 17:16:59 +02:00
|
|
|
return;
|
|
|
|
}
|
2023-08-12 21:47:09 +02:00
|
|
|
|
|
|
|
if ($diff > 0) {
|
|
|
|
$cur += $diff;
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
2023-08-12 21:47:09 +02:00
|
|
|
$this->lastOutgoingTimestamp = $cur;
|
|
|
|
|
|
|
|
$t += $delay;
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Play file.
|
|
|
|
*/
|
2023-08-19 21:20:51 +02:00
|
|
|
public function play(LocalFile|RemoteUrl|ReadableStream $file): void
|
2023-08-12 17:29:00 +02:00
|
|
|
{
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey->play($file);
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
|
2023-08-14 18:28:44 +02:00
|
|
|
/**
|
|
|
|
* When called, skips to the next file in the playlist.
|
|
|
|
*/
|
|
|
|
public function skip(): void
|
|
|
|
{
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey->skip();
|
2023-08-14 18:28:44 +02:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Stops playing all files, clears the main and the hold playlist.
|
|
|
|
*/
|
|
|
|
public function stop(): void
|
|
|
|
{
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey->stopPlaying();
|
2023-08-14 18:28:44 +02:00
|
|
|
}
|
2023-08-20 18:31:21 +02:00
|
|
|
/**
|
|
|
|
* Pauses the currently playing file.
|
|
|
|
*/
|
|
|
|
public function pause(): void
|
|
|
|
{
|
|
|
|
$this->diskJockey->pausePlaying();
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Resumes the currently playing file.
|
|
|
|
*/
|
|
|
|
public function resume(): void
|
|
|
|
{
|
|
|
|
$this->diskJockey->resumePlaying();
|
|
|
|
}
|
2023-08-20 19:07:14 +02:00
|
|
|
/**
|
|
|
|
* Whether the file we're currently playing is paused.
|
|
|
|
*/
|
2023-08-21 10:11:23 +02:00
|
|
|
public function isPaused(): bool
|
2023-08-20 19:07:14 +02:00
|
|
|
{
|
2023-08-21 10:11:23 +02:00
|
|
|
return $this->diskJockey->isAudioPaused();
|
2023-08-20 19:07:14 +02:00
|
|
|
}
|
2023-08-12 17:29:00 +02:00
|
|
|
/**
|
|
|
|
* Files to play on hold.
|
|
|
|
*/
|
2023-08-19 21:20:51 +02:00
|
|
|
public function playOnHold(LocalFile|RemoteUrl|ReadableStream ...$files): void
|
2023-08-12 17:29:00 +02:00
|
|
|
{
|
2023-08-19 21:20:51 +02:00
|
|
|
$this->diskJockey->playOnHold(...$files);
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get info about the audio currently being played.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function getCurrent(): LocalFile|RemoteUrl|string|null
|
|
|
|
{
|
|
|
|
return $this->diskJockey->getCurrent();
|
2023-08-12 17:29:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get call state.
|
|
|
|
*/
|
|
|
|
public function getCallState(): CallState
|
|
|
|
{
|
|
|
|
return $this->callState;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Get VoIP state.
|
|
|
|
*/
|
|
|
|
public function getVoIPState(): VoIPState
|
|
|
|
{
|
|
|
|
return $this->voipState;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get call representation.
|
|
|
|
*/
|
|
|
|
public function __toString(): string
|
|
|
|
{
|
|
|
|
return $this->public->__toString();
|
|
|
|
}
|
|
|
|
}
|