Skip to content

Commit a9fecea

Browse files
authored
Merge pull request #1942 from danog/internal-retransmission-sources
Keep the retransmission sources of the participants out of the public API
2 parents 12997a4 + 53c2132 commit a9fecea

5 files changed

Lines changed: 68 additions & 56 deletions

File tree

‎src/API.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ final class API extends AbstractAPI
5454
*
5555
* @var string
5656
*/
57-
public const RELEASE = '8.15.2';
57+
public const RELEASE = '8.15.3';
5858
/**
5959
* We're not logged in.
6060
*

‎src/EventHandler/Calls/AbstractGroupCallParticipant.php‎

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,6 @@ public function __construct(
7171
public readonly ?int $raiseHandRating,
7272
/** Whether the participant's volume was set by an admin. */
7373
public readonly bool $volumeByAdmin,
74-
/**
75-
* The retransmission (RTX) SSRC of each of the {@see self::$videoSources} and {@see self::$presentationSources} that
76-
* has one (the second source of its FID group), signed, by SSRC: the SFU resends the lost packets of the stream on it.
77-
*
78-
* @var array<int, int>
79-
*/
80-
public readonly array $retransmissionSources = [],
8174
) {
8275
parent::__construct($peerId);
8376
}
@@ -103,20 +96,14 @@ final protected static function commonArgs(array $participant, int $peerId, ?sel
10396
$presentation = self::videoSources($participant['presentation'] ?? null);
10497
$videoEndpoint = self::endpoint($participant['video'] ?? null);
10598
$presentationEndpoint = self::endpoint($participant['presentation'] ?? null);
106-
$videoRtx = self::retransmissionSources($participant['video'] ?? null);
107-
$presentationRtx = self::retransmissionSources($participant['presentation'] ?? null);
10899
if ($min && $cached !== null) {
109-
// Participants saved by versions that didn't keep the retransmission sources have none.
110-
$cachedRtx = $cached->retransmissionSources ?? [];
111100
if (!\is_array($participant['video'] ?? null)) {
112101
$video = $cached->videoSources;
113102
$videoEndpoint = $cached->videoEndpoint;
114-
$videoRtx = array_intersect_key($cachedRtx, array_flip($video));
115103
}
116104
if (!\is_array($participant['presentation'] ?? null)) {
117105
$presentation = $cached->presentationSources;
118106
$presentationEndpoint = $cached->presentationEndpoint;
119-
$presentationRtx = array_intersect_key($cachedRtx, array_flip($presentation));
120107
}
121108
}
122109
return [
@@ -138,15 +125,14 @@ final protected static function commonArgs(array $participant, int $peerId, ?sel
138125
'about' => $participant['about'] ?? null,
139126
'raiseHandRating' => $participant['raise_hand_rating'] ?? null,
140127
'volumeByAdmin' => $participant['volume_by_admin'],
141-
'retransmissionSources' => $videoRtx + $presentationRtx,
142128
];
143129
}
144130

145131
/**
146132
* Flatten the source SSRCs of a
147133
* [groupCallParticipantVideo](https://core.telegram.org/constructor/groupCallParticipantVideo),
148134
* taking each source group's primary (first) source and every simulcast layer, deduplicated.
149-
* Retransmission (FID) pairs contribute only their main source: the other is in {@see self::retransmissionSources()}.
135+
* Retransmission (FID) pairs contribute only their main source (GroupCallController keeps the other).
150136
*
151137
* @param mixed $video The `video` or `presentation` field of a groupCallParticipant, if present.
152138
*
@@ -171,30 +157,6 @@ private static function videoSources(mixed $video): array
171157
return array_values(array_keys($sources));
172158
}
173159

174-
/**
175-
* The retransmission SSRC of each SSRC of a groupCallParticipantVideo that has one: the second source of its FID
176-
* group (tgcalls declares one per simulcast layer).
177-
*
178-
* @param mixed $video The `video` or `presentation` field of a groupCallParticipant, if present.
179-
*
180-
* @return array<int, int> Signed SSRCs, by signed SSRC.
181-
*
182-
* @psalm-pure
183-
*/
184-
private static function retransmissionSources(mixed $video): array
185-
{
186-
if (!\is_array($video) || ($video['paused'] ?? false)) {
187-
return [];
188-
}
189-
$sources = [];
190-
foreach ($video['source_groups'] ?? [] as $group) {
191-
if (($group['semantics'] ?? '') === 'FID' && isset($group['sources'][0], $group['sources'][1])) {
192-
$sources[$group['sources'][0]] = $group['sources'][1];
193-
}
194-
}
195-
return $sources;
196-
}
197-
198160
/**
199161
* The `endpoint` of a groupCallParticipantVideo, the key used to subscribe to that stream over
200162
* the colibri data channel.

‎src/EventHandler/Calls/LiveStoryParticipant.php‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ final class LiveStoryParticipant extends AbstractGroupCallParticipant
2828
*
2929
* @param list<int> $videoSources
3030
* @param list<int> $presentationSources
31-
* @param array<int, int> $retransmissionSources
3231
* @param int<1, 20000> $volume Playback volume, from 1 to 20000 where 10000 is 100%.
3332
*
3433
* @psalm-mutation-free
@@ -54,7 +53,6 @@ public function __construct(
5453
bool $volumeByAdmin,
5554
/** Total Telegram Stars donated by this participant, if any. */
5655
public readonly ?int $paidStarsTotal,
57-
array $retransmissionSources = [],
5856
) {
5957
parent::__construct(
6058
$peerId,
@@ -75,7 +73,6 @@ public function __construct(
7573
$about,
7674
$raiseHandRating,
7775
$volumeByAdmin,
78-
$retransmissionSources,
7976
);
8077
}
8178

‎src/GroupCall/GroupCallController.php‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ final class GroupCallController implements CallControllerInterface, GroupConnect
9797
private array $participants = [];
9898
/** @var array<int, int> Signed source ID => bot API peer ID. */
9999
private array $sourceToPeer = [];
100+
/**
101+
* @var array<int, array<int, int>> The retransmission (RTX) SSRC of each video and screen-share SSRC of each
102+
* participant that has one (the second source of its FID group), signed, by SSRC,
103+
* by bot API peer ID: the SFU resends the lost packets of the stream on it.
104+
*/
105+
private array $retransmission = [];
100106

101107
/** Our own audio source ID, in the signed form used by the API. */
102108
private int $source = 0;
@@ -526,6 +532,7 @@ public function applyGroupCall(array $result): void
526532
}
527533
$this->participants = [];
528534
$this->sourceToPeer = [];
535+
$this->retransmission = [];
529536
foreach ($result['participants'] as $participant) {
530537
$this->applyParticipant($participant);
531538
}
@@ -604,6 +611,35 @@ public function onParticipantsUpdate(array $participants, int $version): void
604611
$this->syncSources();
605612
}
606613

614+
/**
615+
* The retransmission SSRC of each video and screen-share SSRC of a participant that has one: from the source
616+
* groups of the update, or kept from the previous one for a stream a min update doesn't carry.
617+
*
618+
* @param array<int, int> $previous
619+
*
620+
* @return array<int, int> Signed SSRCs, by signed SSRC.
621+
*
622+
* @psalm-pure
623+
*/
624+
private static function retransmissionSources(array $participant, AbstractGroupCallParticipant $parsed, array $previous): array
625+
{
626+
$sources = [];
627+
foreach (['video', 'presentation'] as $key) {
628+
$video = $participant[$key] ?? null;
629+
if (!\is_array($video)) {
630+
continue;
631+
}
632+
foreach ($video['source_groups'] ?? [] as $group) {
633+
if (($group['semantics'] ?? '') === 'FID' && isset($group['sources'][0], $group['sources'][1])) {
634+
$sources[$group['sources'][0]] = $group['sources'][1];
635+
}
636+
}
637+
}
638+
// Only the streams the participant sends (not a paused one, say).
639+
$current = array_flip([...$parsed->videoSources, ...$parsed->presentationSources]);
640+
return array_intersect_key($sources + $previous, $current);
641+
}
642+
607643
private function applyParticipant(array $participant): void
608644
{
609645
$peerId = $this->API->getIdInternal($participant['peer']);
@@ -615,6 +651,7 @@ private function applyParticipant(array $participant): void
615651
if ($old !== null) {
616652
unset($this->sourceToPeer[$old->source], $this->participants[$peerId]);
617653
}
654+
unset($this->retransmission[$peerId]);
618655
if (($participant['self'] ?? false)
619656
&& $this->callState === GroupCallState::JOINED
620657
&& (int) ($participant['source'] ?? 0) === $this->source
@@ -630,6 +667,7 @@ private function applyParticipant(array $participant): void
630667
? LiveStoryParticipant::fromRaw($participant, $peerId, $this->participants[$peerId] ?? null)
631668
: GroupCallParticipant::fromRaw($participant, $peerId, $this->participants[$peerId] ?? null);
632669
$this->participants[$peerId] = $parsed;
670+
$this->retransmission[$peerId] = self::retransmissionSources($participant, $parsed, $this->retransmission[$peerId] ?? []);
633671
if ($parsed->source !== 0) {
634672
$this->sourceToPeer[$parsed->source] = $peerId;
635673
$this->sourcePeers[$parsed->source] = $peerId;
@@ -650,7 +688,7 @@ private function syncSources(): void
650688
return;
651689
}
652690
$sources = [];
653-
foreach ($this->participants as $participant) {
691+
foreach ($this->participants as $peerId => $participant) {
654692
if ($participant->source !== 0 && $participant->source !== $this->source) {
655693
$sources[] = [
656694
'audio' => $participant->source,
@@ -659,8 +697,7 @@ private function syncSources(): void
659697
'presentation' => $participant->presentationSources,
660698
'videoEndpoint' => $participant->videoEndpoint,
661699
'presentationEndpoint' => $participant->presentationEndpoint,
662-
// Participants saved by versions that didn't keep the retransmission sources have none.
663-
'rtx' => $participant->retransmissionSources ?? [],
700+
'rtx' => $this->retransmission[$peerId] ?? [],
664701
];
665702
}
666703
}

tests/danog/MadelineProto/GroupCallParticipantTest.php renamed to tests/danog/MadelineProto/GroupCallRetransmissionSourcesTest.php

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
namespace danog\MadelineProto\Test;
1818

1919
use danog\MadelineProto\EventHandler\Calls\GroupCallParticipant;
20+
use danog\MadelineProto\GroupCall\GroupCallController;
2021
use PHPUnit\Framework\TestCase;
22+
use ReflectionMethod;
2123

2224
/**
23-
* The video sources of a participant, from the source groups of its groupCallParticipantVideo.
25+
* The retransmission (RTX) SSRCs of a participant's videos, kept by the controller (not by the participant objects
26+
* the event handlers get) from the FID groups of its groupCallParticipantVideo.
2427
*/
25-
final class GroupCallParticipantTest extends TestCase
28+
final class GroupCallRetransmissionSourcesTest extends TestCase
2629
{
2730
private static function raw(bool $min, ?array $video): array
2831
{
@@ -40,27 +43,40 @@ private static function raw(bool $min, ?array $video): array
4043
] + ($video === null ? [] : ['video' => $video]);
4144
}
4245

46+
/**
47+
* @param array<int, int> $previous
48+
* @return array<int, int>
49+
*/
50+
private static function sources(array $raw, GroupCallParticipant $parsed, array $previous = []): array
51+
{
52+
return (new ReflectionMethod(GroupCallController::class, 'retransmissionSources'))->invoke(null, $raw, $parsed, $previous);
53+
}
54+
4355
public function testTheRetransmissionSourceOfEachVideoSourceIsKept(): void
4456
{
45-
$participant = GroupCallParticipant::fromRaw(self::raw(false, [
57+
$raw = self::raw(false, [
4658
'endpoint' => 'e',
4759
'source_groups' => [
4860
['semantics' => 'SIM', 'sources' => [10, 20]],
4961
['semantics' => 'FID', 'sources' => [10, 11]],
5062
['semantics' => 'FID', 'sources' => [20, -21]],
5163
],
52-
]), 1);
64+
]);
65+
$participant = GroupCallParticipant::fromRaw($raw, 1);
5366
$this->assertSame([10, 20], $participant->videoSources);
54-
$this->assertSame([10 => 11, 20 => -21], $participant->retransmissionSources);
67+
$rtx = self::sources($raw, $participant);
68+
$this->assertSame([10 => 11, 20 => -21], $rtx);
5569

5670
// A min update without the video keeps the known one, with its retransmission sources.
57-
$min = GroupCallParticipant::fromRaw(self::raw(true, null), 1, $participant);
71+
$raw = self::raw(true, null);
72+
$min = GroupCallParticipant::fromRaw($raw, 1, $participant);
5873
$this->assertSame([10, 20], $min->videoSources);
59-
$this->assertSame([10 => 11, 20 => -21], $min->retransmissionSources);
74+
$this->assertSame([10 => 11, 20 => -21], self::sources($raw, $min, $rtx));
6075

61-
// The video stopping takes them away.
62-
$stopped = GroupCallParticipant::fromRaw(self::raw(false, ['endpoint' => 'e', 'paused' => true, 'source_groups' => [['semantics' => 'FID', 'sources' => [10, 11]]]]), 1, $participant);
76+
// The video pausing takes them away.
77+
$raw = self::raw(false, ['endpoint' => 'e', 'paused' => true, 'source_groups' => [['semantics' => 'FID', 'sources' => [10, 11]]]]);
78+
$stopped = GroupCallParticipant::fromRaw($raw, 1, $participant);
6379
$this->assertSame([], $stopped->videoSources);
64-
$this->assertSame([], $stopped->retransmissionSources);
80+
$this->assertSame([], self::sources($raw, $stopped, $rtx));
6581
}
6682
}

0 commit comments

Comments
 (0)