Skip to content

Commit 53c2132

Browse files
committed
Keep the retransmission sources of the participants out of the public API
8.15.2 added them to AbstractGroupCallParticipant (a public, persisted event object) only because the controller builds the sources it passes to GroupConnection from those objects: nobody receiving a participant needs them, and a readonly property added to a persisted class needed a ?? [] for the participants saved before. GroupCallController keeps them in a private map instead, from the same source groups (and a min update keeps them for the streams it doesn't carry). Participants saved by 8.15.2 just carry the dropped property until they're next updated. Claude-Session: https://claude.ai/code/session_01EWcRgSSwNQ2fE6pPhgTfXw
1 parent 12997a4 commit 53c2132

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)