chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,145 @@
package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
"telesrv/internal/sfu"
)
// 屏幕共享(presentation):同一参与者的第二条媒体连接(独立 ICE/DTLS/ssrc 集),
// TL 层落在同一 participant 行的 presentation 字段。硬契约(读客户端源码定):
// - 响应 Updates 里的 updateGroupCallConnection 必须置 presentation flag
// (bit0),否则被主实例误食;DrKLO 只在响应里就地找它,不能只走推送;
// - presentation 字段必须带 audio_source:DrKLO 用它做屏幕 mySource 并塞进
// checkGroupCall,缺失会退化取首个视频 ssrc 致 4s 循环重建;
// - 主连接 rejoin / 整体离会从不补发 leaveGroupCallPresentation:join 整体
// 替换 video_json 时清 presentation_json,SFU 侧主 endpoint 拆除联动拆屏幕。
func (r *Router) onPhoneJoinGroupCallPresentation(ctx context.Context, req *tg.PhoneJoinGroupCallPresentationRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
scope, err := r.groupCallScopeFrom(ctx, req.Call)
if err != nil {
return nil, err
}
if !scope.call.Active() {
return nil, groupCallAlreadyDiscardedErr()
}
// 前置:必须已通过主 join 在会(客户端也只在主 join ack 后才发本 RPC)。
self, found, err := r.deps.GroupCalls.Participant(ctx, scope.call.ID, scope.userID)
if err != nil {
return nil, internalErr()
}
if !found || self.Left {
return nil, groupCallJoinMissingErr()
}
offer, ssrc, err := parseGroupCallJoinPayload(req.Params.Data)
if err != nil {
r.log.Warn("group call presentation payload", zap.Error(err))
return nil, groupCallInvalidErr()
}
endpoint := groupCallEndpointID(sfu.EndpointPresentation, offer.AudioSSRC)
state := participantVideoState{
Endpoint: endpoint,
SourceGroups: groupCallSsrcGroupsFromOffer(offer),
Active: true, // joinGroupCallPresentation 即开始共享(无 video_stopped 概念)
AudioSource: ssrc,
}
// 媒体面先行:独立第二 endpoint(重复 join 替换式幂等——rejoinPresentation
// 会带同一套 ufrag/ssrc 重发)。
sfuService := r.deps.SFU
if sfuService == nil {
sfuService = sfu.Disabled()
}
answer, err := sfuService.Join(ctx, scope.call.ID, scope.userID, sfu.EndpointPresentation, offer)
if err != nil {
r.log.Warn("group call presentation sfu join", zap.Error(err))
return nil, internalErr()
}
raw := encodeVideoState(state)
update := domain.GroupCallParticipantUpdate{PresentationJSON: &raw, Now: int(r.clock.Now().Unix())}
mut, changed, err := r.deps.GroupCalls.UpdateParticipant(ctx, scope.call.ID, scope.userID, update)
if err != nil {
_ = sfuService.Leave(ctx, scope.call.ID, scope.userID, sfu.EndpointPresentation)
return nil, groupCallErr(err)
}
params, err := buildGroupCallConnectionParams(answer, endpoint)
if err != nil {
_ = sfuService.Leave(ctx, scope.call.ID, scope.userID, sfu.EndpointPresentation)
return nil, internalErr()
}
var channel domain.Channel
if changed {
channel = r.groupCallMutationFanout(ctx, scope.channel, mut)
} else {
// 同参数重放(幂等 rejoin):无状态变化也要返回完整连接参数。
channel = scope.channel
mut = domain.GroupCallMutation{Call: scope.call, Participant: self}
}
out := r.groupCallUpdateContainer(ctx, scope.userID, channel,
&tg.UpdateGroupCallParticipants{
Call: &tg.InputGroupCall{ID: mut.Call.ID, AccessHash: mut.Call.AccessHash},
Participants: tgGroupCallParticipants([]domain.GroupCallParticipant{mut.Participant}, scope.userID),
Version: mut.Call.Version,
}, []int64{scope.userID})
conn := &tg.UpdateGroupCallConnection{Params: tg.DataJSON{Data: params}}
conn.SetPresentation(true)
out.Updates = append(out.Updates, conn)
return out, nil
}
func (r *Router) onPhoneLeaveGroupCallPresentation(ctx context.Context, call tg.InputGroupCallClass) (tg.UpdatesClass, error) {
scope, err := r.groupCallScopeFrom(ctx, call)
if err != nil {
return nil, err
}
if r.deps.SFU != nil {
_ = r.deps.SFU.Leave(ctx, scope.call.ID, scope.userID, sfu.EndpointPresentation)
}
// 幂等快照:未在共享/已清理时返回当前态而非报错(屏幕共享既已下架即达成目的)。
idempotentSnapshot := func() (tg.UpdatesClass, error) {
return r.groupCallUpdateContainer(ctx, scope.userID, scope.channel,
groupCallUpdateFor(scope.channel, scope.call, scope.userID, scope.canManage()), nil), nil
}
self, found, err := r.deps.GroupCalls.Participant(ctx, scope.call.ID, scope.userID)
if err != nil {
return nil, internalErr()
}
if !found || self.Left || len(self.PresentationJSON) == 0 {
return idempotentSnapshot()
}
empty := []byte(nil)
mut, changed, err := r.deps.GroupCalls.UpdateParticipant(ctx, scope.call.ID, scope.userID,
domain.GroupCallParticipantUpdate{PresentationJSON: &empty, Now: int(r.clock.Now().Unix())})
if err != nil {
// 与并发的 leaveGroupCall/discard 撞车:presentation 随主连接离会被级联清掉后,
// 本显式 leave 在写时发现 participant 已 Left(ErrGroupCallNotJoined)或 call 已
// discarded。屏幕共享既已下架,按幂等成功返回快照而非 GROUPCALL_JOIN_MISSING 400
// (客户端此刻正同时离会,会忽略该 400,但报错污染日志且非正确语义)。
if errors.Is(err, domain.ErrGroupCallNotJoined) || errors.Is(err, domain.ErrGroupCallDiscarded) {
return idempotentSnapshot()
}
return nil, groupCallErr(err)
}
if !changed {
mut = domain.GroupCallMutation{Call: scope.call, Participant: self}
}
channel := scope.channel
if changed {
channel = r.groupCallMutationFanout(ctx, scope.channel, mut)
}
// 其他端靠本行 presentation 字段消失把屏幕画面下架。
return r.groupCallUpdateContainer(ctx, scope.userID, channel,
&tg.UpdateGroupCallParticipants{
Call: &tg.InputGroupCall{ID: mut.Call.ID, AccessHash: mut.Call.AccessHash},
Participants: tgGroupCallParticipants([]domain.GroupCallParticipant{mut.Participant}, scope.userID),
Version: mut.Call.Version,
}, []int64{scope.userID}), nil
}