Files
EchoChat/backend/go-service/app/meeting/service/interfaces.go
2026-05-18 19:42:55 +08:00

269 lines
12 KiB
Go
Raw Permalink 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 service 提供 meeting 模块的业务逻辑
package service
import (
"context"
"encoding/json"
authModel "github.com/echochat/backend/app/auth/model"
notifyService "github.com/echochat/backend/app/notify/service"
)
// NotifyPusher 通知推送接口
// 由 notify.service.NotifyService 隐式实现,供 meeting 模块注入使用
// 会议邀请、主持人变更、踢出会议等事件通过此接口落地通知中心
type NotifyPusher interface {
Push(ctx context.Context, payload *notifyService.PushPayload)
PushBatch(ctx context.Context, payloads []*notifyService.PushPayload)
}
// UserInfoResolver 查询用户昵称 / 头像的接口
// 由 contact.FriendshipDAO 隐式实现(已有 GetUsersByIDs 方法)
// 用于拉取会议邀请中主持人信息、成员列表头像等
type UserInfoResolver interface {
GetUsersByIDs(ctx context.Context, userIDs []int64) ([]authModel.User, error)
}
// OnlineChecker 查询用户在线状态的接口
// 由 ws.OnlineService 隐式实现(签名对齐现有实现:不返回 error查询失败按离线处理
// 用于会议邀请时判断收件人是否在线(离线则降级为仅通知入库,上线时通过未读补偿推送)
type OnlineChecker interface {
IsOnline(ctx context.Context, userID int64) bool
}
// ====== MediaOrchestratorGo → Node media-server 统一封装(设计 §6.6======
// TransportInfo Node 端创建的 Transport 元信息
// 字段与 mediasoup-client 的 Device.createSendTransport / createRecvTransport 入参兼容
type TransportInfo struct {
ID string `json:"id"` // Transport ID
IceParameters json.RawMessage `json:"iceParameters"` // mediasoup ICE 参数
IceCandidates json.RawMessage `json:"iceCandidates"` // ICE 候选列表
DtlsParameters json.RawMessage `json:"dtlsParameters"` // DTLS 指纹参数
SctpParameters json.RawMessage `json:"sctpParameters,omitempty"`
}
// ConsumerInfo Node 端创建的 Consumer 元信息
type ConsumerInfo struct {
ID string `json:"id"`
ProducerID string `json:"producerId"`
Kind string `json:"kind"` // "audio" | "video"
RtpParameters json.RawMessage `json:"rtpParameters"`
Type string `json:"type,omitempty"` // "simple" | "simulcast" | "svc"
ProducerPaused bool `json:"producerPaused,omitempty"`
}
// CreateTransportReq 创建 Transport 请求
type CreateTransportReq struct {
RoomCode string `json:"roomCode"`
UserID int64 `json:"userId"`
Direction string `json:"direction"` // "send" | "recv"
}
// CreateProducerReq 创建 Producer 请求
type CreateProducerReq struct {
RoomCode string `json:"roomCode"`
UserID int64 `json:"userId"`
TransportID string `json:"transportId"`
Kind string `json:"kind"` // "audio" | "video"
RtpParameters json.RawMessage `json:"rtpParameters"` // 直接转发给 Node由其做结构校验
// AppData 客户端自定义元信息,例如 {"screen": true} 表示该 Producer 是屏幕共享流
// Phase 3 屏幕共享引入:与摄像头/麦克风 video Producer 同走 sendTransport
// 通过 appData.screen 区分,便于服务端识别并触发 meeting.screen.* 广播
AppData json.RawMessage `json:"appData,omitempty"`
}
// CreateConsumerReq 创建 Consumer 请求
type CreateConsumerReq struct {
RoomCode string `json:"roomCode"`
UserID int64 `json:"userId"`
TransportID string `json:"transportId"`
ProducerID string `json:"producerId"`
RtpCapabilities json.RawMessage `json:"rtpCapabilities"`
}
// ====== 会议录制 RPCPhase B 引入)======
// StartRecordingReq 启动录制请求
//
// ProducerIDs 指定要录的若干 Producer由业务层决定Phase B1 通常是 host 的
// audio + video 两个Phase B3 扩展为屏幕共享 + 多人音频后会更多)
type StartRecordingReq struct {
RouterID string `json:"routerId"`
RoomCode string `json:"roomCode"`
ProducerIDs []string `json:"producerIds"`
}
// StartRecordingResp media-server 启动成功后的响应
type StartRecordingResp struct {
RecordingID string `json:"recordingId"`
OutputPath string `json:"outputPath"` // media-server 本地 mp4 路径,用于停止后回拉上传
Tracks []struct {
ProducerID string `json:"producerId"`
Kind string `json:"kind"`
} `json:"tracks"`
}
// StopRecordingResp media-server 停止录制后返回的元数据
type StopRecordingResp struct {
RecordingID string `json:"recordingId"`
OutputPath string `json:"outputPath"`
SizeBytes int64 `json:"sizeBytes"`
DurationMS int64 `json:"durationMs"`
ExitCode *int `json:"exitCode,omitempty"` // ffmpeg 退出码nil = 进程未启动到退出阶段
FailureReason string `json:"failureReason,omitempty"` // 非空表示录制最终标记为 failed
}
// MediaOrchestrator 媒体服务器编排接口(设计 §6.6 NodeClient
// Task 7 (2026-04-21) 起默认实现为 HTTPMediaOrchestrator通过 X-Internal-Token
// 调用 Node media-server 的 /internal/v1/* REST API
// NoopMediaOrchestrator 仅保留用于本地调试(无 Node 服务时可临时切换):
// - CreateRouter 返回 "noop-router-{code}";其他方法返回可解析的占位数据
// - 所有方法均幂等:重复调用不报错,符合 WS 信令重试语义
type MediaOrchestrator interface {
// CreateRouter 为会议房间创建 mediasoup Router
// Task 8 起实现需自带 sync.Map 幂等缓存roomCode 已有 Router 时直接返回缓存值
CreateRouter(ctx context.Context, roomCode string) (routerID string, err error)
// CloseRouter 关闭房间对应 Router 及其下所有资源(幂等)
CloseRouter(ctx context.Context, roomCode string) error
// ResolveRouterID 从本地缓存查询 roomCode 对应的 routerID不触发 HTTP 调用)
// Task 8 引入:用于 JoinRoom 等"仅需复用"的场景,避免重复调 CreateRouter
// 返回 (id, true) 命中;(_, false) 缺失(通常说明房间未创建 Router 或服务重启后未 reschedule
ResolveRouterID(roomCode string) (string, bool)
// ResolveRouterInfo 读取 roomCode 对应的 Router 完整信息routerID + rtpCapabilities
// Task 9 引入:前端 mediasoup-client Device.load 必须 rtpCapabilitiesJoinRoom 响应由此填充
// 返回 (id, caps, true) 命中;(_, _, false) 缺失
ResolveRouterInfo(roomCode string) (string, json.RawMessage, bool)
// CreateTransport 为用户创建 send/recv WebRTC Transport
CreateTransport(ctx context.Context, req *CreateTransportReq) (*TransportInfo, error)
// ConnectTransport Transport DTLS 握手(幂等:重复 connect 对已连接 transport 视为成功)
ConnectTransport(ctx context.Context, transportID string, dtlsParameters json.RawMessage) error
// CloseTransport 主动关闭指定 TransportTask 16 P2-1 引入)
// 场景:用户离会 / WS 断连时 cleanupUserResources 精确清理,避免等待 Router 级联
// 语义404Transport 已关闭/不存在)返回 ErrMediaResourceNotFound上层可视为"已清理"幂等成功
CloseTransport(ctx context.Context, transportID string) error
// CreateProducer 在指定 send Transport 上创建 Producer
CreateProducer(ctx context.Context, req *CreateProducerReq) (producerID string, err error)
// CloseProducer 关闭指定 Producer幂等
CloseProducer(ctx context.Context, producerID string) error
// CreateConsumer 在指定 recv Transport 上创建 Consumer订阅远端 Producer
// Node 端创建的 Consumer 默认 paused=true需在前端 track 就绪后调用 ResumeConsumer 才会开始推流
CreateConsumer(ctx context.Context, req *CreateConsumerReq) (*ConsumerInfo, error)
// ResumeConsumer 恢复 Consumer 开始接收 RTPTask 9 引入)
// 前端 recv Transport 与 track 挂载完成后由 meeting.consume.resume WS 事件触发
// Node 未找到对应 Consumer 返回 ErrMediaResourceNotFound调用方可按"已失效"处理
ResumeConsumer(ctx context.Context, consumerID string) error
// CloseConsumer 关闭指定 Consumer幂等
CloseConsumer(ctx context.Context, consumerID string) error
// ====== 录制Phase B 引入)======
// StartRecording 在 media-server 上启动一份会议录制
// 失败语义404 RouterNotFound / 409 producerId 不可消费等返回 ErrMediaServerError
// 成功返回 recordingIdhex32+ media-server 本地 mp4 输出路径
StartRecording(ctx context.Context, req *StartRecordingReq) (*StartRecordingResp, error)
// StopRecording 停止录制
// 幂等媒体侧已结束404返回 ErrMediaResourceNotFound调用方一般视为"已停止"
StopRecording(ctx context.Context, recordingID string) (*StopRecordingResp, error)
}
// NoopMediaOrchestrator 本地调试占位实现Task 7 后默认不再使用)
// 返回伪造的 ID 与固定占位数据JSON空对象 / 空数组),所有操作仅写日志不调用 Node
// Task 7 已将全局 wire 绑定切换为 HTTPMediaOrchestrator此类型仅作为无 Node 服务时
// 的临时回退(需手动修改 provider.go 的 wire.Bind 才会生效)
type NoopMediaOrchestrator struct{}
// NewNoopMediaOrchestrator 构造占位的 MediaOrchestrator
func NewNoopMediaOrchestrator() *NoopMediaOrchestrator {
return &NoopMediaOrchestrator{}
}
// CreateRouter 返回以 "noop-router-" 为前缀的伪造 RouterID
func (n *NoopMediaOrchestrator) CreateRouter(_ context.Context, roomCode string) (string, error) {
return "noop-router-" + roomCode, nil
}
// CloseRouter 占位实现:直接返回 nil
func (n *NoopMediaOrchestrator) CloseRouter(_ context.Context, _ string) error {
return nil
}
// ResolveRouterID 占位实现:始终命中,返回 noop-router-{code}
func (n *NoopMediaOrchestrator) ResolveRouterID(roomCode string) (string, bool) {
return "noop-router-" + roomCode, true
}
// ResolveRouterInfo 占位:返回空 rtpCapabilities 对象(保证前端 mediasoup-client 能通过 JSON 解析,但 Device.load 会失败;适合本地无 Node 调试)
func (n *NoopMediaOrchestrator) ResolveRouterInfo(roomCode string) (string, json.RawMessage, bool) {
return "noop-router-" + roomCode, json.RawMessage(`{}`), true
}
// CreateTransport 占位:返回以 "noop-transport-" 为前缀的伪造 ID带最小合法 JSON 结构
func (n *NoopMediaOrchestrator) CreateTransport(_ context.Context, req *CreateTransportReq) (*TransportInfo, error) {
return &TransportInfo{
ID: "noop-transport-" + req.Direction,
IceParameters: json.RawMessage(`{}`),
IceCandidates: json.RawMessage(`[]`),
DtlsParameters: json.RawMessage(`{}`),
}, nil
}
// ConnectTransport 占位:直接返回 nil
func (n *NoopMediaOrchestrator) ConnectTransport(_ context.Context, _ string, _ json.RawMessage) error {
return nil
}
// CloseTransport 占位:直接返回 nilTask 16 P2-1 引入)
func (n *NoopMediaOrchestrator) CloseTransport(_ context.Context, _ string) error {
return nil
}
// CreateProducer 占位:返回伪造 ID
func (n *NoopMediaOrchestrator) CreateProducer(_ context.Context, req *CreateProducerReq) (string, error) {
return "noop-producer-" + req.Kind, nil
}
// CloseProducer 占位:直接返回 nil
func (n *NoopMediaOrchestrator) CloseProducer(_ context.Context, _ string) error {
return nil
}
// CreateConsumer 占位:返回伪造的 ConsumerInfo含目标 producerID 回填)
func (n *NoopMediaOrchestrator) CreateConsumer(_ context.Context, req *CreateConsumerReq) (*ConsumerInfo, error) {
return &ConsumerInfo{
ID: "noop-consumer-" + req.ProducerID,
ProducerID: req.ProducerID,
Kind: "video",
RtpParameters: json.RawMessage(`{}`),
Type: "simple",
}, nil
}
// ResumeConsumer 占位:直接返回 nil
func (n *NoopMediaOrchestrator) ResumeConsumer(_ context.Context, _ string) error {
return nil
}
// CloseConsumer 占位:直接返回 nil
func (n *NoopMediaOrchestrator) CloseConsumer(_ context.Context, _ string) error {
return nil
}
// StartRecording 占位:返回伪造 recordingId 与空 outputPath
func (n *NoopMediaOrchestrator) StartRecording(_ context.Context, req *StartRecordingReq) (*StartRecordingResp, error) {
return &StartRecordingResp{
RecordingID: "noop-recording-" + req.RoomCode,
OutputPath: "",
}, nil
}
// StopRecording 占位:返回零大小元数据
func (n *NoopMediaOrchestrator) StopRecording(_ context.Context, recordingID string) (*StopRecordingResp, error) {
return &StopRecordingResp{
RecordingID: recordingID,
}, nil
}