owpengram-server/internal/rpc/phone_group_call_rtmp.go

192 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
// RTMP 直播Live StreamRPCcreateGroupCall(rtmp_stream) 建房后推流方OBS
// 用 getGroupCallStreamRtmpUrl 拿到的 url/key 推流,观众 join 后经
// upload.getFile(inputGroupCallStream) 拉 broadcast chunk。
//
// 与普通语音聊天RTC/SFU关键差异RTMP join 不建 SFU 连接、不做 ssrc 唯一性
// 媒体面校验updateGroupCallConnection.params 返回 {"stream":true,"rtmp":true}
// 让 TDesktop 切 broadcast 模式ParseJoinResponse → JoinBroadcastStream
// rtmpConnectionParams 是 RTMP 观众 join 响应的下行 JSONtgcalls broadcast 分支)。
// 可管理者附 rtmp_stream_url/key 供直播设置页展示(普通观众不下发,避免泄漏推流凭据)。
type rtmpConnectionParams struct {
Stream bool `json:"stream"`
Rtmp bool `json:"rtmp"`
RtmpStreamURL string `json:"rtmp_stream_url,omitempty"`
RtmpStreamKey string `json:"rtmp_stream_key,omitempty"`
}
func buildRtmpConnectionParams(url, key string) (string, error) {
p := rtmpConnectionParams{Stream: true, Rtmp: true, RtmpStreamURL: url, RtmpStreamKey: key}
out, err := json.Marshal(p)
if err != nil {
return "", err
}
return string(out), nil
}
// joinRtmpGroupCall 处理 RTMP 直播房间的 joinGroupCall。
func (r *Router) joinRtmpGroupCall(ctx context.Context, scope *groupCallScope, req *tg.PhoneJoinGroupCallRequest) (tg.UpdatesClass, error) {
// 房间上限RTMP 观众也计入 participants_countrejoin已在会换 ssrc不受限。
if max := r.cfg.GroupCallMaxParticipants; max > 0 && scope.call.ParticipantsCount >= max {
if p, found, _ := r.deps.GroupCalls.Participant(ctx, scope.call.ID, scope.userID); !found || p.Left {
return nil, groupCallForbiddenErr()
}
}
joinAsChannelID, err := r.groupCallJoinAsChannelID(scope, req.JoinAs)
if err != nil {
return nil, err
}
now := int(r.clock.Now().Unix())
// RTMP 观众的 join JSON 仍带 tgcalls ssrc客户端为拉流也建了本地 controller
// 解析失败/缺失不致命——直播不需要 SFU用随机 ssrc 记账保证参与者行有效。
ssrc := int64(0)
if _, s, err := parseGroupCallJoinPayload(req.Params.Data); err == nil {
ssrc = s
}
if ssrc == 0 {
ssrc = randomSSRC()
}
// RTMP 观众(含创建者,其经 OBS 独立推流,在 group call 里同样是纯观众)一律
// force-mutedIsAdmin=false 让 store 对 join_muted 房间置 muted+muted_by_admin
// self 行输出 muted=true / can_self_unmute=falseTDesktop 才会稳定停在 stream 模式。
mut, err := r.deps.GroupCalls.Join(ctx, domain.JoinGroupCallRequest{
CallID: scope.call.ID,
UserID: scope.userID,
JoinAsChannelID: joinAsChannelID,
SSRC: ssrc,
Muted: true,
IsAdmin: false,
Now: now,
})
if err != nil {
return nil, groupCallErr(err)
}
// 可管理者拿推流 url/key直播设置页展示普通观众只得 stream:true。
var url, key string
if scope.canManage() && r.deps.GroupCalls != nil {
if k, kerr := r.deps.GroupCalls.RtmpStreamKey(ctx, scope.channel.ID, false, now); kerr == nil {
key = k
url = r.rtmpIngestURL()
}
}
params, err := buildRtmpConnectionParams(url, key)
if err != nil {
return nil, internalErr()
}
channel := r.groupCallMutationFanout(ctx, scope.channel, mut)
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})
// updateGroupCall 必须先于 updateGroupCallConnectionTDesktop 按序 applyUpdates
// 处理 connection 时若还没从 groupCall 读到 stream_dc_id 会打
// "Api Error: Empty stream_dc_id" 并 fallback 主 DC。
callUpdate := &tg.UpdateGroupCall{Call: tgGroupCall(mut.Call, scope.userID, scope.canManage(), r.cfg.PublicBaseURL)}
if channel.ID != 0 {
callUpdate.SetPeer(&tg.PeerChannel{ChannelID: channel.ID})
}
out.Updates = append(out.Updates, callUpdate)
out.Updates = append(out.Updates, &tg.UpdateGroupCallConnection{Params: tg.DataJSON{Data: params}})
return out, nil
}
// onPhoneGetGroupCallStreamRtmpURL 返回频道的 RTMP 推流 url/key创建前预览、
// 直播设置页展示、revoke 轮换。仅频道管理员可调revoke=true 生成新 key旧 key
// 立即失效并断开正在进行的推流。
func (r *Router) onPhoneGetGroupCallStreamRtmpURL(ctx context.Context, req *tg.PhoneGetGroupCallStreamRtmpURLRequest) (*tg.PhoneGroupCallStreamRtmpURL, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
if r.deps.GroupCalls == nil || r.deps.Channels == nil {
return nil, notImplementedErr()
}
userID, err := r.phoneRequireUser(ctx)
if err != nil {
return nil, err
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
view, err := r.deps.Channels.GetChannel(ctx, userID, peer.ID)
if err != nil {
return nil, peerIDInvalidErr()
}
if view.Self.Status != domain.ChannelMemberActive || !channelMemberIsAdmin(view.Self) {
return nil, tgerr400("CHAT_ADMIN_REQUIRED")
}
now := int(r.clock.Now().Unix())
key, err := r.deps.GroupCalls.RtmpStreamKey(ctx, peer.ID, req.Revoke, now)
if err != nil {
return nil, groupCallErr(err)
}
if req.Revoke && r.deps.LiveStreams != nil {
// 旧 key 失效后仍在推的连接必须断开(否则用旧 key 的推流会继续被接收)。
r.deps.LiveStreams.DropChannel(peer.ID)
}
return &tg.PhoneGroupCallStreamRtmpURL{URL: r.rtmpIngestURL(), Key: key}, nil
}
// onPhoneGetGroupCallStreamChannels 返回 RTMP 直播的当前时间轴unifiedchannel=1
// scale=0供 tgcalls 决定从哪个 time_ms 起拉 chunk。无活跃推流时返回空列表
// TDesktop 据此显示"等待推流"占位并循环重试。
func (r *Router) onPhoneGetGroupCallStreamChannels(ctx context.Context, call tg.InputGroupCallClass) (*tg.PhoneGroupCallStreamChannels, error) {
scope, err := r.groupCallScopeFrom(ctx, call)
if err != nil {
return nil, err
}
out := &tg.PhoneGroupCallStreamChannels{Channels: []tg.GroupCallStreamChannel{}}
if !scope.call.RtmpStream || r.deps.LiveStreams == nil || scope.channel.ID == 0 {
return out, nil
}
for _, ch := range r.deps.LiveStreams.StreamChannels(scope.channel.ID) {
out.Channels = append(out.Channels, tg.GroupCallStreamChannel{
Channel: ch.Channel,
Scale: ch.Scale,
LastTimestampMs: ch.LastTimestampMs,
})
}
return out, nil
}
// rtmpIngestURL 返回展示给推流端的 RTMP 服务器地址。
func (r *Router) rtmpIngestURL() string {
if r.cfg.RtmpIngestURL != "" {
return r.cfg.RtmpIngestURL
}
host := r.cfg.IP
if host == "" {
host = "127.0.0.1"
}
return "rtmp://" + host + ":2400/live"
}
func randomSSRC() int64 {
var buf [4]byte
if _, err := rand.Read(buf[:]); err != nil {
return 1
}
v := int64(binary.BigEndian.Uint32(buf[:]))
if v == 0 {
v = 1
}
return v
}