package jitsi import ( "context" "fmt" "time" ) // ======================================== // Breakpilot Drive Game Meeting Types // ======================================== // GameMeetingMode represents different game video call modes type GameMeetingMode string const ( GameMeetingCoop GameMeetingMode = "coop" // Co-Op voice/video GameMeetingChallenge GameMeetingMode = "challenge" // 1v1 face-off GameMeetingClassRace GameMeetingMode = "class_race" // Teacher supervises GameMeetingTeamHuddle GameMeetingMode = "team_huddle" // Quick team sync ) // GameMeetingConfig holds configuration for game video meetings type GameMeetingConfig struct { SessionID string `json:"session_id"` Mode GameMeetingMode `json:"mode"` HostID string `json:"host_id"` HostName string `json:"host_name"` Players []GamePlayer `json:"players"` EnableVideo bool `json:"enable_video"` EnableVoice bool `json:"enable_voice"` TeacherID string `json:"teacher_id,omitempty"` TeacherName string `json:"teacher_name,omitempty"` ClassName string `json:"class_name,omitempty"` } // GamePlayer represents a player in the meeting type GamePlayer struct { ID string `json:"id"` Name string `json:"name"` IsModerator bool `json:"is_moderator,omitempty"` } // GameMeetingLink extends MeetingLink with game-specific info type GameMeetingLink struct { *MeetingLink SessionID string `json:"session_id"` Mode GameMeetingMode `json:"mode"` Players []string `json:"players"` } // ======================================== // Game Meeting Creation // ======================================== // CreateCoopMeeting creates a video call for Co-Op gameplay (2-4 players) func (s *JitsiService) CreateCoopMeeting(ctx context.Context, config GameMeetingConfig) (*GameMeetingLink, error) { roomName := fmt.Sprintf("bp-coop-%s", config.SessionID[:8]) meeting := Meeting{ RoomName: roomName, DisplayName: config.HostName, Subject: "Breakpilot Drive - Co-Op Session", Moderator: true, Config: &MeetingConfig{ StartWithAudioMuted: !config.EnableVoice, StartWithVideoMuted: !config.EnableVideo, RequireDisplayName: true, EnableLobby: false, // Direct join for co-op DisableDeepLinking: true, }, } link, err := s.CreateMeetingLink(ctx, meeting) if err != nil { return nil, fmt.Errorf("failed to create co-op meeting: %w", err) } playerIDs := make([]string, len(config.Players)) for i, p := range config.Players { playerIDs[i] = p.ID } return &GameMeetingLink{ MeetingLink: link, SessionID: config.SessionID, Mode: GameMeetingCoop, Players: playerIDs, }, nil } // CreateChallengeMeeting creates a 1v1 video call for challenges func (s *JitsiService) CreateChallengeMeeting(ctx context.Context, config GameMeetingConfig, challengerName string, opponentName string) (*GameMeetingLink, error) { roomName := fmt.Sprintf("bp-challenge-%s", config.SessionID[:8]) meeting := Meeting{ RoomName: roomName, DisplayName: challengerName, Subject: fmt.Sprintf("Challenge: %s vs %s", challengerName, opponentName), Moderator: false, // Both players are equal Config: &MeetingConfig{ StartWithAudioMuted: false, // Voice enabled for trash talk StartWithVideoMuted: !config.EnableVideo, RequireDisplayName: true, EnableLobby: false, DisableDeepLinking: true, }, } link, err := s.CreateMeetingLink(ctx, meeting) if err != nil { return nil, fmt.Errorf("failed to create challenge meeting: %w", err) } return &GameMeetingLink{ MeetingLink: link, SessionID: config.SessionID, Mode: GameMeetingChallenge, Players: []string{config.HostID}, }, nil } // CreateClassRaceMeeting creates a video call for teacher-supervised class races func (s *JitsiService) CreateClassRaceMeeting(ctx context.Context, config GameMeetingConfig) (*GameMeetingLink, error) { roomName := fmt.Sprintf("bp-klasse-%s-%s", s.sanitizeRoomName(config.ClassName), time.Now().Format("150405")) // Teacher is moderator meeting := Meeting{ RoomName: roomName, DisplayName: config.TeacherName, Subject: fmt.Sprintf("Klassenrennen: %s", config.ClassName), Moderator: true, Config: &MeetingConfig{ StartWithAudioMuted: true, // Students muted by default StartWithVideoMuted: true, // Video off for performance RequireDisplayName: true, EnableLobby: true, // Teacher admits students EnableRecording: false, // No recording for minors DisableDeepLinking: true, }, Features: &MeetingFeatures{ Recording: false, Transcription: false, }, } link, err := s.CreateMeetingLink(ctx, meeting) if err != nil { return nil, fmt.Errorf("failed to create class race meeting: %w", err) } playerIDs := make([]string, len(config.Players)) for i, p := range config.Players { playerIDs[i] = p.ID } return &GameMeetingLink{ MeetingLink: link, SessionID: config.SessionID, Mode: GameMeetingClassRace, Players: playerIDs, }, nil } // CreateTeamHuddleMeeting creates a quick sync meeting for teams func (s *JitsiService) CreateTeamHuddleMeeting(ctx context.Context, config GameMeetingConfig, teamName string) (*GameMeetingLink, error) { roomName := fmt.Sprintf("bp-team-%s-%s", s.sanitizeRoomName(teamName), config.SessionID[:8]) meeting := Meeting{ RoomName: roomName, DisplayName: config.HostName, Subject: fmt.Sprintf("Team %s - Huddle", teamName), Duration: 5, // Short 5-minute huddles Moderator: true, Config: &MeetingConfig{ StartWithAudioMuted: false, // Voice on for quick sync StartWithVideoMuted: true, // Video optional RequireDisplayName: true, EnableLobby: false, DisableDeepLinking: true, }, } link, err := s.CreateMeetingLink(ctx, meeting) if err != nil { return nil, fmt.Errorf("failed to create team huddle: %w", err) } playerIDs := make([]string, len(config.Players)) for i, p := range config.Players { playerIDs[i] = p.ID } return &GameMeetingLink{ MeetingLink: link, SessionID: config.SessionID, Mode: GameMeetingTeamHuddle, Players: playerIDs, }, nil } // ======================================== // Game-Specific Meeting Configurations // ======================================== // GetGameEmbedConfig returns optimized config for embedding in Unity WebGL func (s *JitsiService) GetGameEmbedConfig(enableVideo bool, enableVoice bool) *MeetingConfig { return &MeetingConfig{ StartWithAudioMuted: !enableVoice, StartWithVideoMuted: !enableVideo, RequireDisplayName: true, EnableLobby: false, DisableDeepLinking: true, // Important for iframe embedding } } // BuildGameEmbedURL creates a URL optimized for Unity WebGL embedding func (s *JitsiService) BuildGameEmbedURL(roomName string, playerName string, enableVideo bool, enableVoice bool) string { config := s.GetGameEmbedConfig(enableVideo, enableVoice) return s.BuildEmbedURL(roomName, playerName, config) } // BuildUnityIFrameParams returns parameters for Unity's WebGL iframe func (s *JitsiService) BuildUnityIFrameParams(link *GameMeetingLink, playerName string) map[string]interface{} { return map[string]interface{}{ "domain": s.extractDomain(), "roomName": link.RoomName, "displayName": playerName, "jwt": link.JWT, "configOverwrite": map[string]interface{}{ "startWithAudioMuted": false, "startWithVideoMuted": true, "disableDeepLinking": true, "prejoinPageEnabled": false, "enableWelcomePage": false, "enableClosePage": false, "disableInviteFunctions": true, }, "interfaceConfigOverwrite": map[string]interface{}{ "DISABLE_JOIN_LEAVE_NOTIFICATIONS": true, "MOBILE_APP_PROMO": false, "SHOW_CHROME_EXTENSION_BANNER": false, "TOOLBAR_BUTTONS": []string{ "microphone", "camera", "hangup", "chat", }, }, } } // ======================================== // Spectator Mode (for teachers/parents) // ======================================== // CreateSpectatorLink creates a view-only link for observers func (s *JitsiService) CreateSpectatorLink(ctx context.Context, roomName string, spectatorName string) (*MeetingLink, error) { meeting := Meeting{ RoomName: roomName, DisplayName: fmt.Sprintf("[Zuschauer] %s", spectatorName), Moderator: false, Config: &MeetingConfig{ StartWithAudioMuted: true, StartWithVideoMuted: true, DisableDeepLinking: true, }, } return s.CreateMeetingLink(ctx, meeting) } // ======================================== // Helper Functions // ======================================== // extractDomain extracts the domain from baseURL func (s *JitsiService) extractDomain() string { // Remove protocol prefix domain := s.baseURL if len(domain) > 8 && domain[:8] == "https://" { domain = domain[8:] } else if len(domain) > 7 && domain[:7] == "http://" { domain = domain[7:] } // Remove port if present for i, c := range domain { if c == ':' || c == '/' { domain = domain[:i] break } } return domain } // ValidateGameMeetingConfig validates configuration before creating meeting func ValidateGameMeetingConfig(config GameMeetingConfig) error { if config.SessionID == "" { return fmt.Errorf("session_id is required") } if config.Mode == "" { return fmt.Errorf("mode is required") } if config.HostID == "" { return fmt.Errorf("host_id is required") } if config.HostName == "" { return fmt.Errorf("host_name is required") } switch config.Mode { case GameMeetingCoop: if len(config.Players) < 2 || len(config.Players) > 4 { return fmt.Errorf("co-op mode requires 2-4 players") } case GameMeetingChallenge: if len(config.Players) != 2 { return fmt.Errorf("challenge mode requires exactly 2 players") } case GameMeetingClassRace: if config.TeacherID == "" || config.TeacherName == "" { return fmt.Errorf("class race mode requires teacher info") } if config.ClassName == "" { return fmt.Errorf("class race mode requires class name") } case GameMeetingTeamHuddle: if len(config.Players) < 2 { return fmt.Errorf("team huddle requires at least 2 players") } default: return fmt.Errorf("unknown game meeting mode: %s", config.Mode) } return nil }