Permette upload da portale e app con ereditarietà partita→squadra→società, generazione MP4 via ffmpeg e distribuzione su home lab e nodi CPX per pause e assenza segnale. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
5.7 KiB
Swift
192 lines
5.7 KiB
Swift
import Foundation
|
|
|
|
struct User: Codable, Equatable, Sendable {
|
|
let id: String
|
|
let email: String
|
|
let name: String
|
|
let role: String
|
|
}
|
|
|
|
struct AuthTokens: Sendable {
|
|
let accessToken: String
|
|
let refreshToken: String
|
|
}
|
|
|
|
struct SportOption: Identifiable, Equatable, Sendable {
|
|
var id: String { key }
|
|
let key: String
|
|
let label: String
|
|
let board: String
|
|
let overlay: String
|
|
let allowedOverlays: [String]
|
|
let defaultRules: [String: Int]
|
|
}
|
|
|
|
struct Team: Identifiable, Equatable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let sport: String
|
|
let sportKey: String
|
|
let boardType: String
|
|
let clubName: String?
|
|
let logoUrl: String?
|
|
let primaryColor: String?
|
|
let secondaryColor: String?
|
|
let canStream: Bool
|
|
let youtubeEnabled: Bool
|
|
let youtubeConnected: Bool
|
|
let youtubeSelectable: Bool
|
|
let youtubeChannelTitle: String?
|
|
let youtubeUsesPlatformChannel: Bool
|
|
let youtubeTeamChannelTitle: String?
|
|
let planName: String?
|
|
let recordingsEnabled: Bool
|
|
let phoneDownloadEnabled: Bool
|
|
let billingUrl: String?
|
|
let staffManageUrl: String?
|
|
let customCoverEnabled: Bool
|
|
|
|
var canUseYoutube: Bool { youtubeEnabled }
|
|
var isYoutubeReady: Bool { youtubeSelectable }
|
|
|
|
var effectiveYoutubeChannel: String? {
|
|
guard isYoutubeReady else { return nil }
|
|
return youtubeUsesPlatformChannel ? "platform" : "team"
|
|
}
|
|
|
|
var youtubeDestinationLabel: String {
|
|
if youtubeUsesPlatformChannel { return L10n.t("youtube.channel.platform") }
|
|
let name = youtubeTeamChannelTitle?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
|
|
?? clubName?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty
|
|
?? L10n.t("youtube.channel.club.fallback")
|
|
return L10n.t("youtube.channel.club", name)
|
|
}
|
|
}
|
|
|
|
struct ScoringRules: Equatable, Sendable {
|
|
var pointsPerSet: Int = 25
|
|
var pointsDecidingSet: Int = 15
|
|
var minPointLead: Int = 2
|
|
var periods: Int = 4
|
|
var periodDurationSecs: Int = 600
|
|
var overtimeDurationSecs: Int = 300
|
|
}
|
|
|
|
struct Match: Identifiable, Equatable, Sendable {
|
|
let id: String
|
|
let teamId: String
|
|
let teamName: String
|
|
let opponentName: String
|
|
let location: String?
|
|
let scheduledAt: String?
|
|
let sportKey: String
|
|
let sportLabel: String?
|
|
let boardType: String
|
|
let overlayKind: String?
|
|
let effectiveOverlayKind: String
|
|
let setsToWin: Int
|
|
let category: String?
|
|
let scoringRules: ScoringRules?
|
|
let activeSessionId: String?
|
|
let activeSessionStatus: String?
|
|
let streamCompleted: Bool
|
|
let coachHubVisible: Bool?
|
|
let homePrimaryColor: String?
|
|
let homeSecondaryColor: String?
|
|
let homeLogoUrl: String?
|
|
let opponentPrimaryColor: String?
|
|
let opponentLogoUrl: String?
|
|
let effectiveCoverUrl: String?
|
|
let coverSource: String
|
|
let customCoverEnabled: Bool
|
|
|
|
var hasActiveSession: Bool { activeSessionId != nil }
|
|
|
|
var canResumeCamera: Bool {
|
|
guard let status = activeSessionStatus, activeSessionId != nil else { return false }
|
|
return ["connecting", "live", "reconnecting", "paused"].contains(status)
|
|
}
|
|
}
|
|
|
|
struct StreamSession: Identifiable, Equatable, Sendable {
|
|
let id: String
|
|
let matchId: String
|
|
let status: String
|
|
let platform: String
|
|
let rtmpIngestUrl: String?
|
|
let hlsPlaybackUrl: String?
|
|
let watchPageUrl: String?
|
|
let shareUrl: String?
|
|
let youtubeWatchUrl: String?
|
|
let youtubeReady: Bool
|
|
let privacyStatus: String
|
|
let targetBitrate: Int
|
|
let targetFps: Int
|
|
let qualityPreset: String
|
|
let minQualityPreset: String
|
|
let startedAt: String?
|
|
let score: ScoreState?
|
|
let audioMuted: Bool
|
|
|
|
var isLive: Bool { status == "live" || status == "reconnecting" }
|
|
var isPaused: Bool { status == "paused" }
|
|
var isEnded: Bool { status == "ended" || status == "error" }
|
|
|
|
func with(minQualityPreset: String) -> StreamSession {
|
|
StreamSession(
|
|
id: id,
|
|
matchId: matchId,
|
|
status: status,
|
|
platform: platform,
|
|
rtmpIngestUrl: rtmpIngestUrl,
|
|
hlsPlaybackUrl: hlsPlaybackUrl,
|
|
watchPageUrl: watchPageUrl,
|
|
shareUrl: shareUrl,
|
|
youtubeWatchUrl: youtubeWatchUrl,
|
|
youtubeReady: youtubeReady,
|
|
privacyStatus: privacyStatus,
|
|
targetBitrate: targetBitrate,
|
|
targetFps: targetFps,
|
|
qualityPreset: qualityPreset,
|
|
minQualityPreset: minQualityPreset,
|
|
startedAt: startedAt,
|
|
score: score,
|
|
audioMuted: audioMuted
|
|
)
|
|
}
|
|
|
|
func withAudioMuted(_ muted: Bool) -> StreamSession {
|
|
StreamSession(
|
|
id: id,
|
|
matchId: matchId,
|
|
status: status,
|
|
platform: platform,
|
|
rtmpIngestUrl: rtmpIngestUrl,
|
|
hlsPlaybackUrl: hlsPlaybackUrl,
|
|
watchPageUrl: watchPageUrl,
|
|
shareUrl: shareUrl,
|
|
youtubeWatchUrl: youtubeWatchUrl,
|
|
youtubeReady: youtubeReady,
|
|
privacyStatus: privacyStatus,
|
|
targetBitrate: targetBitrate,
|
|
targetFps: targetFps,
|
|
qualityPreset: qualityPreset,
|
|
minQualityPreset: minQualityPreset,
|
|
startedAt: startedAt,
|
|
score: score,
|
|
audioMuted: muted
|
|
)
|
|
}
|
|
|
|
func watchShareUrl() -> String? {
|
|
if platform == "youtube", let url = youtubeWatchUrl, !url.isEmpty { return url }
|
|
if let url = shareUrl, !url.isEmpty { return url }
|
|
if let url = watchPageUrl, !url.isEmpty { return url }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
var nilIfEmpty: String? { isEmpty ? nil : self }
|
|
}
|