From a303a9467180462ff8382c786249aae6cfe30aaf Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Fri, 19 Jun 2026 18:48:29 +0200 Subject: [PATCH] Fix HLS browser in prod e stabilizza broadcast RTMP su iOS. Il proxy Rails gestisce il redirect MediaMTX su /live/match_* e riscrive le playlist; edge nginx reindirizza /live/ sotto /hls/. Su iOS, SessionBuilder HaishinKit, pipeline mixer corretta e refresh token. Co-authored-by: Cursor --- .../app/controllers/hls_proxy_controller.rb | 100 ++- backend/config/environments/production.rb | 6 +- backend/config/routes.rb | 10 + backend/spec/requests/hls_proxy_spec.rb | 36 + infra/nginx-edge/nginx.conf | 25 + .../ios/MatchLiveTv.xcodeproj/project.pbxproj | 847 +++++++++++++----- .../xcschemes/MatchLiveTv.xcscheme | 10 +- native/ios/MatchLiveTv/Data/API/ApiDtos.swift | 4 + .../MatchLiveTv/Data/API/MatchLiveAPI.swift | 25 +- .../ios/MatchLiveTv/Data/AppContainer.swift | 7 +- .../Data/Repository/AuthRepository.swift | 7 +- .../Streaming/BroadcastVideoOrientation.swift | 31 + .../Streaming/LiveBroadcastEngine.swift | 274 +++++- .../UI/Broadcast/BroadcastScreen.swift | 16 +- .../MatchLiveTv/UI/Login/LoginScreen.swift | 8 +- .../UI/System/ScreenOrientation.swift | 5 + 16 files changed, 1092 insertions(+), 319 deletions(-) create mode 100644 backend/spec/requests/hls_proxy_spec.rb create mode 100644 native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift diff --git a/backend/app/controllers/hls_proxy_controller.rb b/backend/app/controllers/hls_proxy_controller.rb index 65d8dca..e877057 100644 --- a/backend/app/controllers/hls_proxy_controller.rb +++ b/backend/app/controllers/hls_proxy_controller.rb @@ -1,38 +1,98 @@ class HlsProxyController < ActionController::Base - # Proxy pubblico verso MediaMTX HLS (NPM inoltra tutto a Rails :3000). + # Proxy pubblico verso MediaMTX HLS. + # Dev: tutto passa da Rails (/hls/*). + # Prod: edge inoltra /hls/ a MediaMTX; il redirect cookie di MediaMTX punta a + # /live/match_{uuid}/… che deve essere gestito da #live_match (fallback). def show - upstream_path = params[:path].to_s + proxy_mediamtx_path(params[:path].to_s) + end + + def live_match + session_id = params[:session_id].to_s + return head :not_found unless session_id.match?(/\A[0-9a-f-]{36}\z/i) + + segments = params[:segments].presence || "index.m3u8" + proxy_mediamtx_path("live/match_#{session_id}/#{segments}") + end + + private + + def proxy_mediamtx_path(upstream_path) return head :not_found if upstream_path.blank? upstream = "#{MatchLiveTv.mediamtx_hls_url}/#{upstream_path}" upstream = "#{upstream}?#{request.query_string}" if request.query_string.present? cookie = request.headers["Cookie"].presence || "cookieCheck=1" - response = hls_conn.get(upstream) { |req| req.headers["Cookie"] = cookie } - if response.status == 302 - location = response.headers["location"].to_s - follow_url = if location.start_with?("http") - location - else - "#{MatchLiveTv.mediamtx_hls_url.sub(%r{/$}, "")}#{location}" - end - set_cookie = response.headers["set-cookie"].to_s - cookie = set_cookie.presence || cookie - response = hls_conn.get(follow_url) { |req| req.headers["Cookie"] = cookie } + 5.times do + response = hls_conn.get(upstream) { |req| req.headers["Cookie"] = cookie } + + if response.status.in?([301, 302, 307, 308]) + location = response.headers["location"].to_s + cookie = cookie_from_set_header(response.headers["set-cookie"]).presence || cookie + upstream = resolve_upstream_url(location) + next + end + + return render_proxied_response(response) end - response.headers.each do |key, value| - next if %w[transfer-encoding connection].include?(key.downcase) - - headers[key] = value - end - render body: response.body, status: response.status + head :bad_gateway rescue Faraday::Error => e Rails.logger.warn("[HlsProxy] #{upstream_path}: #{e.message}") head :bad_gateway end - private + def resolve_upstream_url(location) + return location if location.start_with?("http://", "https://") + + base = MatchLiveTv.mediamtx_hls_url.sub(%r{/$}, "") + location.start_with?("/") ? "#{base}#{location}" : "#{base}/#{location}" + end + + def render_proxied_response(response) + body = response.body.to_s + content_type = response.headers["content-type"].to_s + + if m3u8_playlist?(content_type, body) + public_hls_base = "#{request.base_url}/hls" + body = body.gsub(%r{(?<=["'(\s])/live/(match_[0-9a-f-]{36}/)}, "/hls/live/\\1") + body = body.gsub(%r{\A/live/(match_[0-9a-f-]{36}/)}, "/hls/live/\\1") + end + + response.headers.each do |key, value| + next if %w[transfer-encoding connection content-length content-encoding].include?(key.downcase) + + if key.downcase == "location" + headers[key] = rewrite_public_location(value) + else + headers[key] = value + end + end + + render body: body, status: response.status, content_type: content_type.presence + end + + def rewrite_public_location(location) + loc = location.to_s + if loc.start_with?("/live/match_") + return loc.sub(%r{\A/live/}, "/hls/") + end + if loc.include?("/live/match_") + return loc.gsub(%r{/(live/match_[0-9a-f-]{36}/)}, "/hls/live/\\1") + end + loc + end + + def m3u8_playlist?(content_type, body) + return true if content_type.include?("mpegurl") || content_type.include?("m3u8") + + body.lstrip.start_with?("#EXTM3U") + end + + def cookie_from_set_header(set_cookie_header) + set_cookie_header.to_s[/cookieCheck=[^;]+/] + end def hls_conn @hls_conn ||= Faraday.new do |f| diff --git a/backend/config/environments/production.rb b/backend/config/environments/production.rb index aa805eb..1450c19 100644 --- a/backend/config/environments/production.rb +++ b/backend/config/environments/production.rb @@ -13,7 +13,8 @@ Rails.application.configure do config.ssl_options = { redirect: { exclude: ->(request) { - request.path == "/up" || request.path == "/up/deep" || request.path == "/cable" || request.path.start_with?("/hls/") + request.path == "/up" || request.path == "/up/deep" || request.path == "/cable" || + request.path.start_with?("/hls/") || request.path.match?(%r{\A/live/match_[0-9a-f-]{36}/}i) } } } @@ -34,7 +35,8 @@ Rails.application.configure do config.host_authorization = { exclude: ->(request) { - request.path == "/up" || request.path == "/up/deep" || request.path.start_with?("/cable") || request.path.start_with?("/hls/") + request.path == "/up" || request.path == "/up/deep" || request.path.start_with?("/cable") || + request.path.start_with?("/hls/") || request.path.match?(%r{\A/live/match_[0-9a-f-]{36}/}i) } } diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 4180943..2cc7575 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -109,6 +109,16 @@ Rails.application.routes.draw do end get "hls/*path", to: "hls_proxy#show", format: false, constraints: { path: /.+/ } + # MediaMTX HLS cookie redirect → /live/match_{uuid}/… (edge o browser dopo 302). + get "live/match_:session_id", + to: "hls_proxy#live_match", + format: false, + constraints: { session_id: /[0-9a-f-]{36}/i }, + defaults: { segments: "index.m3u8" } + get "live/match_:session_id/*segments", + to: "hls_proxy#live_match", + format: false, + constraints: { session_id: /[0-9a-f-]{36}/i, segments: /.+/ } get "live", to: "public/live#index", as: :public_live_index get "live/:id", to: "public/live#show", as: :public_live diff --git a/backend/spec/requests/hls_proxy_spec.rb b/backend/spec/requests/hls_proxy_spec.rb new file mode 100644 index 0000000..6fe91aa --- /dev/null +++ b/backend/spec/requests/hls_proxy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "HLS proxy", type: :request do + let(:mediamtx_hls) { "http://mediamtx.test:8888" } + let(:playlist) do + <<~M3U8 + #EXTM3U + #EXT-X-VERSION:3 + #EXT-X-TARGETDURATION:1 + #EXTINF:1.0, + /live/match_#{session_id}/segment0.ts + M3U8 + end + let(:session_id) { "cc7e90b2-c672-4401-b5d3-51fdcdc7a214" } + + before do + allow(MatchLiveTv).to receive(:mediamtx_hls_url).and_return(mediamtx_hls) + end + + describe "GET /live/match_:session_id/*" do + it "proxies MediaMTX redirect and rewrites playlist paths to /hls/" do + stub_request(:get, "#{mediamtx_hls}/live/match_#{session_id}/index.m3u8") + .to_return(status: 302, headers: { "Location" => "/live/match_#{session_id}/index.m3u8?cookieCheck=1" }) + stub_request(:get, "#{mediamtx_hls}/live/match_#{session_id}/index.m3u8?cookieCheck=1") + .to_return(status: 200, body: playlist, headers: { "Content-Type" => "application/vnd.apple.mpegurl" }) + + get "/live/match_#{session_id}/index.m3u8" + + expect(response).to have_http_status(:ok) + expect(response.body).to include("/hls/live/match_#{session_id}/segment0.ts") + expect(response.body).not_to include("/live/match_#{session_id}/segment0.ts") + end + end +end diff --git a/infra/nginx-edge/nginx.conf b/infra/nginx-edge/nginx.conf index 6d0230b..d5e23f8 100644 --- a/infra/nginx-edge/nginx.conf +++ b/infra/nginx-edge/nginx.conf @@ -67,6 +67,31 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + # MediaMTX risponde 302 verso /live/match_{uuid}/… — riscrive per restare sotto /hls/ + proxy_redirect /live/ /hls/live/; + proxy_redirect ~^/live/(.*)$ /hls/live/$1; + } + + # Fallback: se il browser segue un redirect assoluto a /live/match_{uuid}/… (m3u8/ts). + location ~ ^/live/match_[0-9a-f-]+/ { + limit_except GET HEAD { + deny all; + } + + rewrite ^/live/(.*)$ /$1 break; + proxy_pass http://mediamtx:8888; + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + proxy_connect_timeout 5s; + proxy_read_timeout 3600s; + proxy_set_header Host $host; + proxy_set_header Cookie $hls_cookie; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect /live/ /hls/live/; + proxy_redirect ~^/live/(.*)$ /hls/live/$1; } location = /edge-health { diff --git a/native/ios/MatchLiveTv.xcodeproj/project.pbxproj b/native/ios/MatchLiveTv.xcodeproj/project.pbxproj index 015cb03..047b732 100644 --- a/native/ios/MatchLiveTv.xcodeproj/project.pbxproj +++ b/native/ios/MatchLiveTv.xcodeproj/project.pbxproj @@ -1,240 +1,625 @@ // !$*UTF8*$! { archiveVersion = 1; - classes = {}; + classes = { + }; objectVersion = 60; objects = { - 2C1637685ECE4A02B5681E5F /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = ""; }; - 56472459E69946E8AE093248 /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = ""; }; - 059D2F48669B4073BFE72E8D /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = ""; }; - 8F12B9A34CFD41029CD74A18 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = ""; }; - 6D1122BE2CAB4C6C9D6A12EB /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = ""; }; - 1FD3F6FCA8B54DDA858ABB6D /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = ""; }; - 22FD8122DEC74182A2F4CD69 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = ""; }; - 58C37FC10F34409EB32CAB94 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = ""; }; - 229A7B6E2AF14254BE5E1BE7 /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = ""; }; - 92CE832D69994A86B2CA85AD /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = ""; }; - 9AB29B550B774276AA6508CB /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = ""; }; - 9F97D0F58B0C4232827A6FEA /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = ""; }; - BE6073F255774BD1AB25D01A /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = ""; }; - 6E79EB14DEDE4BBBB30734E1 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = ""; }; - 606456B638DD4D8F8FEAF57B /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = ""; }; - 562CD8B2B0DA4275BAA14FE3 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = ""; }; - C9718FB64F514448BB5F8498 /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = ""; }; - 149E69E5291642339CFAC14B /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = ""; }; - 4C53F1C734CB4745BCB4BBD0 /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = ""; }; - DF85D2BF2D574EEF97215552 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = ""; }; - AA323B2E80B74C8DBEE4DA45 /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = ""; }; - 4BF8D73247554EAF8506B4C8 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = ""; }; - 7AE2FC4135BB4D0A82B4DFFC /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = ""; }; - 126835B4E56A4AD0B3119429 /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = ""; }; - 31DA29870CD24D749044570D /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = ""; }; - 4B0C6EE1A67440D5B5B8EC2F /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = ""; }; - 88CCBA95967F429093C28846 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = ""; }; - E3EA9302512B4A5AB283D88E /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = ""; }; - F7E85AC8EC7348F4A5B21F94 /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = ""; }; - 87D71E37017C47799B602656 /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = ""; }; - 73D8BF04B48E405BABA5395C /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = ""; }; - 8854A01335904882AADB4938 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = ""; }; - 6CA512B24B2048DAB9876C7F /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = ""; }; - 0DA1CA2637BC43338153CC1D /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = ""; }; - 58668C3993F243FD9A5621C6 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = ""; }; - E1722E92B37E4FB5BFA0F64F /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = ""; }; - 6407B9289F0048778B2A6057 /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = ""; }; - E44F2780AEDA44F8B16091E3 /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = ""; }; - 106A45AE33B94E778FDEDED7 /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = ""; }; - 31FAA2DF5F114464977D89B0 /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = ""; }; - 3CA6FD7B0F1E412FA56AB8A3 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = ""; }; - 9545AF93C34540748A1C6E33 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = ""; }; - 8AD4558FC0EB4A5D99F41E4D /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = ""; }; - 091497DB283544DFA9C7E736 /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = ""; }; - F93A551F9C8E4A71A007F25C /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = ""; }; - 66AF9547CF434A4D9C1E8335 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = ""; }; - F32269FB35364D969738C23A /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = ""; }; - 00BDEB10F16C4F968EEEF50E /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = ""; }; - A99587B489BF4900B86754ED /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = ""; }; - E013E12B974B4FCCA3C74151 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = ""; }; - 576978DEF79042848BCA07DC /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = ""; }; - BEC9E21F02BC41D58E006F66 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = ""; }; - 64BB42C4455341CBBEF7ADB1 /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = ""; }; - 7AFF6FEE9E2C420DB0B69CDC /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = ""; }; - EBAE4EB3BE0141AD982E1D5E /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = ""; }; - 6CE9AE7967074219B69DFEE4 /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = ""; }; - 40229139B1934E469D211B4F /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = ""; }; - 7F4BEC0641C14E6FBCAF4163 /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = ""; }; - 494C6DACBCA34021BAB851B9 /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = ""; }; - AEB1E4F84BBB438AA1FE8BEC /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = ""; }; - CF166707120B4B589F342EC4 /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = ""; }; - 5C742326421B4176855F5E64 /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = ""; }; - EA7B4BCC9EAA48CF94ADF09F /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = ""; }; - 596D2E6129214E95874E4E1F /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = ""; }; - 82C7043D46EB4FBCB41BFB87 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = ""; }; - 1F95F2F95BBB45589765B6C1 /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = ""; }; - 7B6C9A5E9BA84A6BBCBF9B3F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = ""; }; - D1A10874BEB246908DF27543 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = ""; }; - 9989F3DEB021437FB1D79792 /* MatchLiveTv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchLiveTv.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 6F0D757BA4DF4A8BAA37E5A1 /* MatchLiveTvTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 1794E4AFEC524A2391E1F43A /* HaishinKit */ = {isa = XCSwiftPackageProductDependency; package = 144AB99B1DAD4D48925A7103; productName = HaishinKit; }; - 5A2B53779BB342F0A4EB1D50 /* RTMPHaishinKit */ = {isa = XCSwiftPackageProductDependency; package = 144AB99B1DAD4D48925A7103; productName = RTMPHaishinKit; }; - 63AA6E164A9D46F08CE75059 /* MatchLiveTvApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1637685ECE4A02B5681E5F /* MatchLiveTvApp.swift */; }; - 9F4B82FECE1B4479BD5C70EA /* ApiInstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56472459E69946E8AE093248 /* ApiInstant.swift */; }; - 9F22BE9935F84C4F9B6DA971 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 059D2F48669B4073BFE72E8D /* AppConfig.swift */; }; - 6EB025E38C2E45C985C7E315 /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F12B9A34CFD41029CD74A18 /* ColorHex.swift */; }; - EE457ABEF9B74DA28A2E2FFD /* DeviceTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D1122BE2CAB4C6C9D6A12EB /* DeviceTelemetry.swift */; }; - 2D699C6ABD2840B385194046 /* MediaUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD3F6FCA8B54DDA858ABB6D /* MediaUrl.swift */; }; - 2BF572CC8FBB420CAE651946 /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FD8122DEC74182A2F4CD69 /* TokenStore.swift */; }; - 71B88EC9E30A4866B3F7E20E /* UserFacingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C37FC10F34409EB32CAB94 /* UserFacingError.swift */; }; - 0461E337AAD54BBFA537E7E1 /* ApiDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229A7B6E2AF14254BE5E1BE7 /* ApiDtos.swift */; }; - 1BFEEBA978A54FC8A1A38D7D /* MatchLiveAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CE832D69994A86B2CA85AD /* MatchLiveAPI.swift */; }; - 4A170EAF1F114BA1BD512C77 /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB29B550B774276AA6508CB /* AppContainer.swift */; }; - 8FCD261732D245B69839DAEB /* ActionCableClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F97D0F58B0C4232827A6FEA /* ActionCableClient.swift */; }; - A967247B226E4E86811D86DA /* SessionCableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6073F255774BD1AB25D01A /* SessionCableService.swift */; }; - 25360BD296FA4D26AB936847 /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E79EB14DEDE4BBBB30734E1 /* AuthRepository.swift */; }; - 77FF5239FE2A458F815117E5 /* MatchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606456B638DD4D8F8FEAF57B /* MatchRepository.swift */; }; - 4CE31F1DC2A141BB97BBBF77 /* MatchSessionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562CD8B2B0DA4275BAA14FE3 /* MatchSessionLauncher.swift */; }; - C741D7BBAEF842B1A5CE79F5 /* ScoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9718FB64F514448BB5F8498 /* ScoreRepository.swift */; }; - 033E9E19FA1D41ECBC6FE954 /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149E69E5291642339CFAC14B /* SessionRepository.swift */; }; - 9708F347932F4ED9B25F30D4 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C53F1C734CB4745BCB4BBD0 /* ScoreController.swift */; }; - 6F21F50CE3394618BBDA9DB9 /* WizardSessionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF85D2BF2D574EEF97215552 /* WizardSessionHolder.swift */; }; - E1EBDFB0867C415FBC6D1267 /* MatchHubFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA323B2E80B74C8DBEE4DA45 /* MatchHubFilter.swift */; }; - 43B0ABA8C73547FBBA33F681 /* MatchScoringRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF8D73247554EAF8506B4C8 /* MatchScoringRules.swift */; }; - 9B29D4ECB04641B386D43176 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE2FC4135BB4D0A82B4DFFC /* Models.swift */; }; - 9FD1B70E6BC94A5B963C84E3 /* ScoreState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 126835B4E56A4AD0B3119429 /* ScoreState.swift */; }; - 53A1BD84C77844F9A5322D71 /* BroadcastModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DA29870CD24D749044570D /* BroadcastModels.swift */; }; - 55822A3378244F328DEC930B /* LiveBroadcastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0C6EE1A67440D5B5B8EC2F /* LiveBroadcastCoordinator.swift */; }; - 8C3A6D2934054AD685C6B17B /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CCBA95967F429093C28846 /* LiveBroadcastEngine.swift */; }; - BB282D2B90F442FAA19957DE /* CompactScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EA9302512B4A5AB283D88E /* CompactScoreboardElement.swift */; }; - 12D1F6F6647E4A02B5FAE6A5 /* OverlayCanvasRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E85AC8EC7348F4A5B21F94 /* OverlayCanvasRenderer.swift */; }; - 259CD50B331D435CAA934791 /* OverlayLogoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D71E37017C47799B602656 /* OverlayLogoCache.swift */; }; - 597F0E27F4794C5B8EB4421D /* OverlayMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D8BF04B48E405BABA5395C /* OverlayMappings.swift */; }; - 4CB7FA4311CD496DBAF590D2 /* OverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8854A01335904882AADB4938 /* OverlayRenderer.swift */; }; - 124E11045F024DB79725B158 /* OverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA512B24B2048DAB9876C7F /* OverlayState.swift */; }; - 00E66D28E2C44685B08D3862 /* ScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DA1CA2637BC43338153CC1D /* ScoreboardElement.swift */; }; - 0D823575D3C34A6AB0B1ADE7 /* SponsorElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58668C3993F243FD9A5621C6 /* SponsorElement.swift */; }; - 6826A0C8849B468089F89095 /* WatermarkElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1722E92B37E4FB5BFA0F64F /* WatermarkElement.swift */; }; - 85DD60C1258B4454879F3958 /* LivePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407B9289F0048778B2A6057 /* LivePreviewView.swift */; }; - 33E82511BEF2435A832885B6 /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44F2780AEDA44F8B16091E3 /* BroadcastControlsOverlay.swift */; }; - 63A3704385234F25B6B4D9DE /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106A45AE33B94E778FDEDED7 /* BroadcastScreen.swift */; }; - DF93C479029448FB98BB26D4 /* LiveScoreActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FAA2DF5F114464977D89B0 /* LiveScoreActions.swift */; }; - 1E7B2D233349405B9D8850B8 /* MatchLiveWordmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA6FD7B0F1E412FA56AB8A3 /* MatchLiveWordmark.swift */; }; - 573B97F0E4D74DCD90962950 /* MatchPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9545AF93C34540748A1C6E33 /* MatchPrimaryButton.swift */; }; - 4B7C1844987C4C4A9331EFF6 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD4558FC0EB4A5D99F41E4D /* MatchScreenScaffold.swift */; }; - 5791C4F50F0948E7993907B3 /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091497DB283544DFA9C7E736 /* MatchSecondaryButton.swift */; }; - 2E8C344A733B462A87720E91 /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93A551F9C8E4A71A007F25C /* MatchStatusBadge.swift */; }; - 2FB16045C6BD4A5E889DD484 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AF9547CF434A4D9C1E8335 /* LoginScreen.swift */; }; - 029149C11EA642BEBEA697D8 /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32269FB35364D969738C23A /* MatchesScreen.swift */; }; - C9B7DFB707744641BD6CED07 /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00BDEB10F16C4F968EEEF50E /* AppNavHost.swift */; }; - A4D5F108E5374A108B5860B1 /* ModalRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99587B489BF4900B86754ED /* ModalRoutes.swift */; }; - F04BF893D3C248C2AF7A546A /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E013E12B974B4FCCA3C74151 /* Routes.swift */; }; - 7B19B54BCD0047F980097559 /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576978DEF79042848BCA07DC /* BroadcastPermissions.swift */; }; - 814C7713F99942A896722EEE /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEC9E21F02BC41D58E006F66 /* SplashScreen.swift */; }; - BE9617BC3F2748A2B78AFF9A /* KeepScreenOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BB42C4455341CBBEF7ADB1 /* KeepScreenOn.swift */; }; - 1341F3BB59E24B7DA0B3F492 /* ScreenOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFF6FEE9E2C420DB0B69CDC /* ScreenOrientation.swift */; }; - DA875E6F04BA46C6BB5193DB /* MatchColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBAE4EB3BE0141AD982E1D5E /* MatchColors.swift */; }; - B95B2FF16A294D2A82E74BC9 /* StepMatchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CE9AE7967074219B69DFEE4 /* StepMatchScreen.swift */; }; - 0AB2C536294B411EA229CFFF /* StepNetworkTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40229139B1934E469D211B4F /* StepNetworkTestScreen.swift */; }; - 4EAEC68278E6487496AC81EE /* StepTransmissionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4BEC0641C14E6FBCAF4163 /* StepTransmissionScreen.swift */; }; - C39AC5E9671941D89416F412 /* TeamBrandingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494C6DACBCA34021BAB851B9 /* TeamBrandingEditor.swift */; }; - 68C7A87D2C5E49F2A7BCEF99 /* TeamColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB1E4F84BBB438AA1FE8BEC /* TeamColorPicker.swift */; }; - 85563DC5408842109B436A49 /* WizardComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF166707120B4B589F342EC4 /* WizardComponents.swift */; }; - 5DE5E7C500304E16915076E3 /* WizardShellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C742326421B4176855F5E64 /* WizardShellScreen.swift */; }; - 0CB7A2DEBA8041A5B75A2F74 /* ApiInstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7B4BCC9EAA48CF94ADF09F /* ApiInstantTests.swift */; }; - B97B7A37B48540A8BB2653A7 /* MatchScoringRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596D2E6129214E95874E4E1F /* MatchScoringRulesTests.swift */; }; - 34968BB5B4FD4727A1DB7DA6 /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C7043D46EB4FBCB41BFB87 /* ScoreActionDecodeTests.swift */; }; - 1FB1035FC78C4034BBC93FEF /* ScoreControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F95F2F95BBB45589765B6C1 /* ScoreControllerTests.swift */; }; - 5A54FCC515114BE19610F99F /* Assets in Resources */ = {isa = PBXBuildFile; fileRef = 7B6C9A5E9BA84A6BBCBF9B3F; }; - 9E10ADDF29394FD6B200D2A7 = {isa = PBXGroup; children = (3181CAF4010E480AB325DA30, 61A4D30FDB7644FEBBCB6AC7, 948F72C223D64471ABFCFE2F); sourceTree = ""; }; - 3181CAF4010E480AB325DA30 = {isa = PBXGroup; children = (2C1637685ECE4A02B5681E5F, 56472459E69946E8AE093248, 059D2F48669B4073BFE72E8D, 8F12B9A34CFD41029CD74A18, 6D1122BE2CAB4C6C9D6A12EB, 1FD3F6FCA8B54DDA858ABB6D, 22FD8122DEC74182A2F4CD69, 58C37FC10F34409EB32CAB94, 229A7B6E2AF14254BE5E1BE7, 92CE832D69994A86B2CA85AD, 9AB29B550B774276AA6508CB, 9F97D0F58B0C4232827A6FEA, BE6073F255774BD1AB25D01A, 6E79EB14DEDE4BBBB30734E1, 606456B638DD4D8F8FEAF57B, 562CD8B2B0DA4275BAA14FE3, C9718FB64F514448BB5F8498, 149E69E5291642339CFAC14B, 4C53F1C734CB4745BCB4BBD0, DF85D2BF2D574EEF97215552, AA323B2E80B74C8DBEE4DA45, 4BF8D73247554EAF8506B4C8, 7AE2FC4135BB4D0A82B4DFFC, 126835B4E56A4AD0B3119429, 31DA29870CD24D749044570D, 4B0C6EE1A67440D5B5B8EC2F, 88CCBA95967F429093C28846, E3EA9302512B4A5AB283D88E, F7E85AC8EC7348F4A5B21F94, 87D71E37017C47799B602656, 73D8BF04B48E405BABA5395C, 8854A01335904882AADB4938, 6CA512B24B2048DAB9876C7F, 0DA1CA2637BC43338153CC1D, 58668C3993F243FD9A5621C6, E1722E92B37E4FB5BFA0F64F, 6407B9289F0048778B2A6057, E44F2780AEDA44F8B16091E3, 106A45AE33B94E778FDEDED7, 31FAA2DF5F114464977D89B0, 3CA6FD7B0F1E412FA56AB8A3, 9545AF93C34540748A1C6E33, 8AD4558FC0EB4A5D99F41E4D, 091497DB283544DFA9C7E736, F93A551F9C8E4A71A007F25C, 66AF9547CF434A4D9C1E8335, F32269FB35364D969738C23A, 00BDEB10F16C4F968EEEF50E, A99587B489BF4900B86754ED, E013E12B974B4FCCA3C74151, 576978DEF79042848BCA07DC, BEC9E21F02BC41D58E006F66, 64BB42C4455341CBBEF7ADB1, 7AFF6FEE9E2C420DB0B69CDC, EBAE4EB3BE0141AD982E1D5E, 6CE9AE7967074219B69DFEE4, 40229139B1934E469D211B4F, 7F4BEC0641C14E6FBCAF4163, 494C6DACBCA34021BAB851B9, AEB1E4F84BBB438AA1FE8BEC, CF166707120B4B589F342EC4, 5C742326421B4176855F5E64, 7B6C9A5E9BA84A6BBCBF9B3F, D1A10874BEB246908DF27543); name = MatchLiveTv; sourceTree = ""; }; - 61A4D30FDB7644FEBBCB6AC7 = {isa = PBXGroup; children = (EA7B4BCC9EAA48CF94ADF09F, 596D2E6129214E95874E4E1F, 82C7043D46EB4FBCB41BFB87, 1F95F2F95BBB45589765B6C1); name = MatchLiveTvTests; sourceTree = ""; }; - 948F72C223D64471ABFCFE2F = {isa = PBXGroup; children = (9989F3DEB021437FB1D79792, 6F0D757BA4DF4A8BAA37E5A1); name = Products; sourceTree = ""; }; - 6C16E72ECFC141DABE1BF97D = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (63AA6E164A9D46F08CE75059, 9F4B82FECE1B4479BD5C70EA, 9F22BE9935F84C4F9B6DA971, 6EB025E38C2E45C985C7E315, EE457ABEF9B74DA28A2E2FFD, 2D699C6ABD2840B385194046, 2BF572CC8FBB420CAE651946, 71B88EC9E30A4866B3F7E20E, 0461E337AAD54BBFA537E7E1, 1BFEEBA978A54FC8A1A38D7D, 4A170EAF1F114BA1BD512C77, 8FCD261732D245B69839DAEB, A967247B226E4E86811D86DA, 25360BD296FA4D26AB936847, 77FF5239FE2A458F815117E5, 4CE31F1DC2A141BB97BBBF77, C741D7BBAEF842B1A5CE79F5, 033E9E19FA1D41ECBC6FE954, 9708F347932F4ED9B25F30D4, 6F21F50CE3394618BBDA9DB9, E1EBDFB0867C415FBC6D1267, 43B0ABA8C73547FBBA33F681, 9B29D4ECB04641B386D43176, 9FD1B70E6BC94A5B963C84E3, 53A1BD84C77844F9A5322D71, 55822A3378244F328DEC930B, 8C3A6D2934054AD685C6B17B, BB282D2B90F442FAA19957DE, 12D1F6F6647E4A02B5FAE6A5, 259CD50B331D435CAA934791, 597F0E27F4794C5B8EB4421D, 4CB7FA4311CD496DBAF590D2, 124E11045F024DB79725B158, 00E66D28E2C44685B08D3862, 0D823575D3C34A6AB0B1ADE7, 6826A0C8849B468089F89095, 85DD60C1258B4454879F3958, 33E82511BEF2435A832885B6, 63A3704385234F25B6B4D9DE, DF93C479029448FB98BB26D4, 1E7B2D233349405B9D8850B8, 573B97F0E4D74DCD90962950, 4B7C1844987C4C4A9331EFF6, 5791C4F50F0948E7993907B3, 2E8C344A733B462A87720E91, 2FB16045C6BD4A5E889DD484, 029149C11EA642BEBEA697D8, C9B7DFB707744641BD6CED07, A4D5F108E5374A108B5860B1, F04BF893D3C248C2AF7A546A, 7B19B54BCD0047F980097559, 814C7713F99942A896722EEE, BE9617BC3F2748A2B78AFF9A, 1341F3BB59E24B7DA0B3F492, DA875E6F04BA46C6BB5193DB, B95B2FF16A294D2A82E74BC9, 0AB2C536294B411EA229CFFF, 4EAEC68278E6487496AC81EE, C39AC5E9671941D89416F412, 68C7A87D2C5E49F2A7BCEF99, 85563DC5408842109B436A49, 5DE5E7C500304E16915076E3); runOnlyForDeploymentPostprocessing = 0; }; - A4BC052FDBD448CF8DCD2C26 = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (0CB7A2DEBA8041A5B75A2F74, B97B7A37B48540A8BB2653A7, 34968BB5B4FD4727A1DB7DA6, 1FB1035FC78C4034BBC93FEF); runOnlyForDeploymentPostprocessing = 0; }; - 41F620F41CDA460CA5758D12 = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (5A54FCC515114BE19610F99F); runOnlyForDeploymentPostprocessing = 0; }; - 975D24005603476B9C9AD706 = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; - AA380510B8DA455C8ADB600E = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; }; - C30FD711CB14404CA4ACF003 = {isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_ENABLE_EXPLICIT_MODULES = NO; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 2.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv; - PRODUCT_NAME = "$(TARGET_NAME)"; - API_BASE_URL = "https://www.matchlivetv.it"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - ENABLE_TESTABILITY = YES; - }; name = Debug; }; - 943E77C5BF47424D930F8289 = {isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_ENABLE_EXPLICIT_MODULES = NO; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", +/* Begin PBXBuildFile section */ + 020986FB10004F36BB7ED0BD /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 806B1034AB5148B4BC55F093 /* TokenStore.swift */; }; + 0AF514A970C5405EA7A54950 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */; }; + 100C9225A2EB448C8C3132D4 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */; }; + 216F56B707484618AE30847E /* ScoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */; }; + 279D1B02380D46DF835831B8 /* TeamColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5086F0DA44004334881343ED /* TeamColorPicker.swift */; }; + 3040471A57FA4032BF347025 /* OverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED842B98F33423988B13960 /* OverlayRenderer.swift */; }; + 3478D3DBB96D41D880BCA499 /* LivePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */; }; + 404F2B3994194914994D5998 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F05D132A65E45E8AED152F0 /* ScoreController.swift */; }; + 40D98FC5AA9B4AB28AC96902 /* MatchScoringRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */; }; + 4131A683E5784D51B19916D5 /* ScreenOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */; }; + 45CF302AAFD947ACAD8BDF81 /* OverlayMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */; }; + 45DF0F887F314A498AC82C6D /* WatermarkElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */; }; + 467525056A0648EDA1E2A0CD /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */; }; + 475FE75875AC47A58609BF5B /* WizardComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2627CF334644FCA8FD9950B /* WizardComponents.swift */; }; + 4BDAEE6159624FACA21E178D /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */; }; + 4C5F8E4B2FE308D700962D65 /* HaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = F652BB68B82C49218CCB7CD0 /* HaishinKit */; }; + 4C5F8E4C2FE308D700962D65 /* RTMPHaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */; }; + 4C8522208CD14180831F780B /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */; }; + 4F6973E4E707495FAED4E582 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */; }; + 518508A5943C425492A428C5 /* ScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */; }; + 538CA13BDD594A96B444350E /* MatchLiveTvApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */; }; + 572553E2029047128A322BE0 /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */; }; + 58119B329D05438F84507448 /* MatchLiveAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */; }; + 587FE4FFD78E472B84911501 /* BroadcastVideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */; }; + 59FAC2E6EDD54B90B41BFD34 /* ApiDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */; }; + 65DDC9416BE940A889C8647A /* MediaUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */; }; + 67BBA82C7CD04A9B85B08F23 /* MatchScoringRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */; }; + 6868D193A5874D62BD640F8E /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */; }; + 6AF878B33937472981276ED6 /* OverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A84A2EA29C441EFA31498AD /* OverlayState.swift */; }; + 6C6F8189C8554FA298D0209D /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */; }; + 798F944B9FFA401E8EDD6B28 /* WizardSessionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */; }; + 7ACD03602B744B4982DB542C /* ScoreControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */; }; + 82D2A2AC562647C9BD18055D /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */; }; + 82D2D8D7BBF642EE8E042CDF /* ApiInstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */; }; + 8414C3672E11414CB0F980C3 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D7D448D0A34D9384C5E2D3 /* Models.swift */; }; + 85162099CD364680AA080E20 /* MatchPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */; }; + 87528ECDBB644E73A6DD4C4B /* OverlayCanvasRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */; }; + 8CF81F29F432430CBD56C405 /* BroadcastModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */; }; + 906FC2F226DA4F958260FEA0 /* ActionCableClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */; }; + 9139292098C24797BE1AFB5E /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10C6CAC995404909A95482BF /* SessionRepository.swift */; }; + 9C8922FBA5DD4DCA9885E649 /* StepTransmissionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D813398882A483280580618 /* StepTransmissionScreen.swift */; }; + 9DA3E777DC3C46CD9F6C2933 /* DeviceTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */; }; + 9DA716746DA24ECCA2182B7A /* LiveBroadcastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */; }; + 9E6D271CDEDF4484BAD8161E /* ScoreState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5A60F7644B4881985D4F15 /* ScoreState.swift */; }; + A0B9F06D1A12410E9C1C361D /* OverlayLogoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */; }; + A608CDA9340247E688A315F6 /* UserFacingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC311445F0144B59AE22DA96 /* UserFacingError.swift */; }; + A7C1C8C352404A72A688CEF4 /* StepNetworkTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */; }; + A80C909B160C46CDADF1ABF8 /* WizardShellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */; }; + ACBB5846610943D9AE3FD2B8 /* MatchLiveTv/Resources/Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */; }; + B2A84ED6937E4D6780A8372B /* MatchColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE769961C58E4741B73938B8 /* MatchColors.swift */; }; + B469E3D9850E46698740395B /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E221E68194C4C92925A9CB0 /* AppConfig.swift */; }; + B54EEA69C2984D0989DDDC3E /* CompactScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */; }; + B6499B988D134CE78DCFF375 /* ModalRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */; }; + BACB4C45854B4531AC2742F3 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC01BBAC06A412296955DB3 /* Routes.swift */; }; + C580112F135E4134A55E4DC7 /* SessionCableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976DD89356184E0694B20CB8 /* SessionCableService.swift */; }; + CD2D69C03B22451E8E75BEE5 /* ApiInstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6725372B2CE473F87167457 /* ApiInstant.swift */; }; + D40F96BF9A98482D861B8714 /* MatchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */; }; + D5399E5D54444A43B1E3919B /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3DEAC5182ED489798656300 /* AppContainer.swift */; }; + D654C3E362EA4786AE9526E5 /* MatchSessionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */; }; + D687E626CEC44A11B2BE78FB /* TeamBrandingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */; }; + D792D3DE5C984BB880BE19DD /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */; }; + D7AAD6BB475B4734BA4EC4EA /* StepMatchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */; }; + D81D1E21B0E44F38A8F5C256 /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */; }; + DAFBA526BBFC4302A851BB93 /* KeepScreenOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */; }; + DC75AFA217B5445AA4FA8BCF /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */; }; + DEE3503ADE4E48F48A49001D /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B453249C904B299178A996 /* AuthRepository.swift */; }; + DFEE71197E4E41CBB835D0BA /* MatchHubFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944972DE154D444781B612ED /* MatchHubFilter.swift */; }; + E382BF9CB4274DDA96A10A98 /* LiveScoreActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E770890933A84622ABF4EA9F /* LiveScoreActions.swift */; }; + F588C2FE35B140418C13963F /* MatchLiveWordmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */; }; + FB14C028958F46EC8A8FAB18 /* SponsorElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A8764A9D2A234F91B52DDC89 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2F5D6393282640E5A53D4CFC; + remoteInfo = MatchLiveTv; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = ""; }; + 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = ""; }; + 0A84A2EA29C441EFA31498AD /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = ""; }; + 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = ""; }; + 10C6CAC995404909A95482BF /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = ""; }; + 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = ""; }; + 1ED842B98F33423988B13960 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = ""; }; + 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = ""; }; + 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = ""; }; + 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = ""; }; + 26D7D448D0A34D9384C5E2D3 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = ""; }; + 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = ""; }; + 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = ""; }; + 2E221E68194C4C92925A9CB0 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = ""; }; + 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = ""; }; + 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = ""; }; + 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = ""; }; + 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = ""; }; + 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = ""; }; + 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = ""; }; + 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = ""; }; + 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = ""; }; + 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = ""; }; + 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = ""; }; + 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = ""; }; + 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = ""; }; + 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = ""; }; + 5086F0DA44004334881343ED /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = ""; }; + 551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = ""; }; + 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = ""; }; + 5D813398882A483280580618 /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = ""; }; + 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = ""; }; + 6F05D132A65E45E8AED152F0 /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = ""; }; + 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = ""; }; + 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = ""; }; + 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = ""; }; + 806B1034AB5148B4BC55F093 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = ""; }; + 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = ""; }; + 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = ""; }; + 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = ""; }; + 944972DE154D444781B612ED /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = ""; }; + 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = ""; }; + 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = ""; }; + 976DD89356184E0694B20CB8 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = ""; }; + 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = ""; }; + 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = ""; }; + A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = ""; }; + A2627CF334644FCA8FD9950B /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = ""; }; + A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = ""; }; + ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = ""; }; + AE769961C58E4741B73938B8 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = ""; }; + B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = ""; }; + BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = ""; }; + C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = ""; }; + C3DCC59301E94417BC24492F /* MatchLiveTv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchLiveTv.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = ""; }; + CCC01BBAC06A412296955DB3 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = ""; }; + CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = ""; }; + D6725372B2CE473F87167457 /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = ""; }; + D6B453249C904B299178A996 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = ""; }; + D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = ""; }; + DE5A60F7644B4881985D4F15 /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = ""; }; + E3DEAC5182ED489798656300 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = ""; }; + E770890933A84622ABF4EA9F /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = ""; }; + EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = ""; }; + EC311445F0144B59AE22DA96 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = ""; }; + ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = ""; }; + F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = ""; }; + F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = ""; }; + FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 02B7024F8C4E4D41AE3B203A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ); - MARKETING_VERSION = 2.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv; - PRODUCT_NAME = "$(TARGET_NAME)"; - API_BASE_URL = "https://www.matchlivetv.it"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; name = Release; }; - 490BD52EC789443397269936 = {isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 2.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; - }; name = Debug; }; - EC6EAD89012E49BA855E8B4C = {isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 21; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - MARKETING_VERSION = 2.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.matchlivetv.match-live-tv.tests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; - }; name = Release; }; - C496B00D0FBC453081E8D965 = {isa = XCBuildConfiguration; buildSettings = {IPHONEOS_DEPLOYMENT_TARGET = 16.0; SWIFT_VERSION = 5.0; }; name = Debug; }; - 33B07D34DCE74B519F4207EB = {isa = XCBuildConfiguration; buildSettings = {IPHONEOS_DEPLOYMENT_TARGET = 16.0; SWIFT_VERSION = 5.0; }; name = Release; }; - 95F893DE8F6A4FFBB614AE96 = {isa = XCConfigurationList; buildConfigurations = (C30FD711CB14404CA4ACF003, 943E77C5BF47424D930F8289); defaultConfigurationName = Release; }; - 2591CEDFD4D6441784591FCE = {isa = XCConfigurationList; buildConfigurations = (490BD52EC789443397269936, EC6EAD89012E49BA855E8B4C); defaultConfigurationName = Release; }; - 5F49CA34489E4C7F89ECD863 = {isa = XCConfigurationList; buildConfigurations = (C496B00D0FBC453081E8D965, 33B07D34DCE74B519F4207EB); defaultConfigurationName = Release; }; - 98BE277FE859401F8A05312C = {isa = PBXNativeTarget; buildConfigurationList = 95F893DE8F6A4FFBB614AE96; buildPhases = (6C16E72ECFC141DABE1BF97D, 975D24005603476B9C9AD706, 41F620F41CDA460CA5758D12); buildRules = (); dependencies = (); name = MatchLiveTv; packageProductDependencies = (1794E4AFEC524A2391E1F43A, 5A2B53779BB342F0A4EB1D50); productName = MatchLiveTv; productReference = 9989F3DEB021437FB1D79792; productType = "com.apple.product-type.application"; }; - BA0168680D5443B8A9521C5B = {isa = PBXContainerItemProxy; containerPortal = FF548150A3A24533BC87A2B6 /* Project object */; proxyType = 1; remoteGlobalIDString = 98BE277FE859401F8A05312C; remoteInfo = MatchLiveTv; }; - E8409388B8964E7B848E487D = {isa = PBXTargetDependency; target = 98BE277FE859401F8A05312C /* MatchLiveTv */; targetProxy = BA0168680D5443B8A9521C5B /* PBXContainerItemProxy */; }; - AF6564A25AD445BE97A401CC = {isa = PBXNativeTarget; buildConfigurationList = 2591CEDFD4D6441784591FCE; buildPhases = (A4BC052FDBD448CF8DCD2C26, AA380510B8DA455C8ADB600E); buildRules = (); dependencies = (E8409388B8964E7B848E487D); name = MatchLiveTvTests; productName = MatchLiveTvTests; productReference = 6F0D757BA4DF4A8BAA37E5A1; productType = "com.apple.product-type.bundle.unit-test"; }; - 144AB99B1DAD4D48925A7103 = {isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/shogo4405/HaishinKit.swift"; requirement = {kind = upToNextMajorVersion; minimumVersion = 2.0.0;}; }; - FF548150A3A24533BC87A2B6 = {isa = PBXProject; attributes = {BuildIndependentTargetsInParallel = 0; LastSwiftUpdateCheck = 1600;}; buildConfigurationList = 5F49CA34489E4C7F89ECD863; compatibilityVersion = "Xcode 15.0"; developmentRegion = it; mainGroup = 9E10ADDF29394FD6B200D2A7; packageReferences = (144AB99B1DAD4D48925A7103); productRefGroup = 948F72C223D64471ABFCFE2F; targets = (98BE277FE859401F8A05312C, AF6564A25AD445BE97A401CC); }; + runOnlyForDeploymentPostprocessing = 0; + }; + 4CD553EA1678427BB1BB0478 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C5F8E4C2FE308D700962D65 /* RTMPHaishinKit in Frameworks */, + 4C5F8E4B2FE308D700962D65 /* HaishinKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 146562E9E8F4444781CCEE1C /* MatchLiveTv */ = { + isa = PBXGroup; + children = ( + EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */, + D6725372B2CE473F87167457 /* ApiInstant.swift */, + 2E221E68194C4C92925A9CB0 /* AppConfig.swift */, + 4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */, + F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */, + 8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */, + 806B1034AB5148B4BC55F093 /* TokenStore.swift */, + EC311445F0144B59AE22DA96 /* UserFacingError.swift */, + 8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */, + 0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */, + E3DEAC5182ED489798656300 /* AppContainer.swift */, + 9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */, + 976DD89356184E0694B20CB8 /* SessionCableService.swift */, + D6B453249C904B299178A996 /* AuthRepository.swift */, + 7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */, + ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */, + A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */, + 10C6CAC995404909A95482BF /* SessionRepository.swift */, + 6F05D132A65E45E8AED152F0 /* ScoreController.swift */, + 4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */, + 944972DE154D444781B612ED /* MatchHubFilter.swift */, + 7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */, + 26D7D448D0A34D9384C5E2D3 /* Models.swift */, + DE5A60F7644B4881985D4F15 /* ScoreState.swift */, + 4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */, + 0461F47FF06E487491BA53F3 /* BroadcastVideoOrientation.swift */, + 6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */, + F03F27B7CFAC4095A48C1C83 /* LiveBroadcastEngine.swift */, + B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */, + BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */, + CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */, + 4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */, + 1ED842B98F33423988B13960 /* OverlayRenderer.swift */, + 0A84A2EA29C441EFA31498AD /* OverlayState.swift */, + D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */, + C43F8C92A1094AC39A7FE978 /* SponsorElement.swift */, + 96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */, + 2D0386B94B9042918A68CD76 /* LivePreviewView.swift */, + 584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */, + 3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */, + E770890933A84622ABF4EA9F /* LiveScoreActions.swift */, + 388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */, + A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */, + 316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */, + 7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */, + 372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */, + 3F5EB6A380DC4728866BD42A /* LoginScreen.swift */, + 34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */, + 3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */, + 978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */, + CCC01BBAC06A412296955DB3 /* Routes.swift */, + 1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */, + 08A245DF805C482C89AEB2E4 /* SplashScreen.swift */, + 2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */, + C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */, + AE769961C58E4741B73938B8 /* MatchColors.swift */, + 21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */, + FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */, + 5D813398882A483280580618 /* StepTransmissionScreen.swift */, + 2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */, + 5086F0DA44004334881343ED /* TeamColorPicker.swift */, + A2627CF334644FCA8FD9950B /* WizardComponents.swift */, + 2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */, + 8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */, + 551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */, + ); + name = MatchLiveTv; + sourceTree = ""; + }; + 25CDF42A0993431FA5F457EB = { + isa = PBXGroup; + children = ( + 146562E9E8F4444781CCEE1C /* MatchLiveTv */, + 4190C9EAA59D4AA882933799 /* MatchLiveTvTests */, + B06BE907209A475AA6898BB6 /* Products */, + ); + sourceTree = ""; + }; + 4190C9EAA59D4AA882933799 /* MatchLiveTvTests */ = { + isa = PBXGroup; + children = ( + 2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */, + 227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */, + F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */, + 94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */, + ); + name = MatchLiveTvTests; + sourceTree = ""; + }; + B06BE907209A475AA6898BB6 /* Products */ = { + isa = PBXGroup; + children = ( + C3DCC59301E94417BC24492F /* MatchLiveTv.app */, + ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2F5D6393282640E5A53D4CFC /* MatchLiveTv */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */; + buildPhases = ( + A547C0346D1345579D28B86D /* Sources */, + 4CD553EA1678427BB1BB0478 /* Frameworks */, + CA0D00DDC25242438A7EBFEC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MatchLiveTv; + packageProductDependencies = ( + F652BB68B82C49218CCB7CD0 /* HaishinKit */, + 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */, + ); + productName = MatchLiveTv; + productReference = C3DCC59301E94417BC24492F /* MatchLiveTv.app */; + productType = "com.apple.product-type.application"; + }; + 938E427E770D4DFD8E992BFA /* MatchLiveTvTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */; + buildPhases = ( + F741ACE11A14421D99BBA54D /* Sources */, + 02B7024F8C4E4D41AE3B203A /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 0831318C61FF46EDA63542ED /* PBXTargetDependency */, + ); + name = MatchLiveTvTests; + productName = MatchLiveTvTests; + productReference = ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A8764A9D2A234F91B52DDC89 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 0; + LastSwiftUpdateCheck = 1600; + }; + buildConfigurationList = 1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = it; + hasScannedForEncodings = 0; + knownRegions = ( + it, + en, + Base, + ); + mainGroup = 25CDF42A0993431FA5F457EB; + packageReferences = ( + 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */, + ); + productRefGroup = B06BE907209A475AA6898BB6 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2F5D6393282640E5A53D4CFC /* MatchLiveTv */, + 938E427E770D4DFD8E992BFA /* MatchLiveTvTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CA0D00DDC25242438A7EBFEC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ACBB5846610943D9AE3FD2B8 /* MatchLiveTv/Resources/Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A547C0346D1345579D28B86D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 538CA13BDD594A96B444350E /* MatchLiveTvApp.swift in Sources */, + CD2D69C03B22451E8E75BEE5 /* ApiInstant.swift in Sources */, + B469E3D9850E46698740395B /* AppConfig.swift in Sources */, + 82D2A2AC562647C9BD18055D /* ColorHex.swift in Sources */, + 9DA3E777DC3C46CD9F6C2933 /* DeviceTelemetry.swift in Sources */, + 65DDC9416BE940A889C8647A /* MediaUrl.swift in Sources */, + 020986FB10004F36BB7ED0BD /* TokenStore.swift in Sources */, + A608CDA9340247E688A315F6 /* UserFacingError.swift in Sources */, + 59FAC2E6EDD54B90B41BFD34 /* ApiDtos.swift in Sources */, + 58119B329D05438F84507448 /* MatchLiveAPI.swift in Sources */, + D5399E5D54444A43B1E3919B /* AppContainer.swift in Sources */, + 906FC2F226DA4F958260FEA0 /* ActionCableClient.swift in Sources */, + C580112F135E4134A55E4DC7 /* SessionCableService.swift in Sources */, + DEE3503ADE4E48F48A49001D /* AuthRepository.swift in Sources */, + D40F96BF9A98482D861B8714 /* MatchRepository.swift in Sources */, + D654C3E362EA4786AE9526E5 /* MatchSessionLauncher.swift in Sources */, + 216F56B707484618AE30847E /* ScoreRepository.swift in Sources */, + 9139292098C24797BE1AFB5E /* SessionRepository.swift in Sources */, + 404F2B3994194914994D5998 /* ScoreController.swift in Sources */, + 798F944B9FFA401E8EDD6B28 /* WizardSessionHolder.swift in Sources */, + DFEE71197E4E41CBB835D0BA /* MatchHubFilter.swift in Sources */, + 40D98FC5AA9B4AB28AC96902 /* MatchScoringRules.swift in Sources */, + 8414C3672E11414CB0F980C3 /* Models.swift in Sources */, + 9E6D271CDEDF4484BAD8161E /* ScoreState.swift in Sources */, + 8CF81F29F432430CBD56C405 /* BroadcastModels.swift in Sources */, + 587FE4FFD78E472B84911501 /* BroadcastVideoOrientation.swift in Sources */, + 9DA716746DA24ECCA2182B7A /* LiveBroadcastCoordinator.swift in Sources */, + 6C6F8189C8554FA298D0209D /* LiveBroadcastEngine.swift in Sources */, + B54EEA69C2984D0989DDDC3E /* CompactScoreboardElement.swift in Sources */, + 87528ECDBB644E73A6DD4C4B /* OverlayCanvasRenderer.swift in Sources */, + A0B9F06D1A12410E9C1C361D /* OverlayLogoCache.swift in Sources */, + 45CF302AAFD947ACAD8BDF81 /* OverlayMappings.swift in Sources */, + 3040471A57FA4032BF347025 /* OverlayRenderer.swift in Sources */, + 6AF878B33937472981276ED6 /* OverlayState.swift in Sources */, + 518508A5943C425492A428C5 /* ScoreboardElement.swift in Sources */, + FB14C028958F46EC8A8FAB18 /* SponsorElement.swift in Sources */, + 45DF0F887F314A498AC82C6D /* WatermarkElement.swift in Sources */, + 3478D3DBB96D41D880BCA499 /* LivePreviewView.swift in Sources */, + 467525056A0648EDA1E2A0CD /* BroadcastControlsOverlay.swift in Sources */, + DC75AFA217B5445AA4FA8BCF /* BroadcastScreen.swift in Sources */, + E382BF9CB4274DDA96A10A98 /* LiveScoreActions.swift in Sources */, + F588C2FE35B140418C13963F /* MatchLiveWordmark.swift in Sources */, + 85162099CD364680AA080E20 /* MatchPrimaryButton.swift in Sources */, + 4F6973E4E707495FAED4E582 /* MatchScreenScaffold.swift in Sources */, + 572553E2029047128A322BE0 /* MatchSecondaryButton.swift in Sources */, + 4C8522208CD14180831F780B /* MatchStatusBadge.swift in Sources */, + 100C9225A2EB448C8C3132D4 /* LoginScreen.swift in Sources */, + D81D1E21B0E44F38A8F5C256 /* MatchesScreen.swift in Sources */, + D792D3DE5C984BB880BE19DD /* AppNavHost.swift in Sources */, + B6499B988D134CE78DCFF375 /* ModalRoutes.swift in Sources */, + BACB4C45854B4531AC2742F3 /* Routes.swift in Sources */, + 4BDAEE6159624FACA21E178D /* BroadcastPermissions.swift in Sources */, + 0AF514A970C5405EA7A54950 /* SplashScreen.swift in Sources */, + DAFBA526BBFC4302A851BB93 /* KeepScreenOn.swift in Sources */, + 4131A683E5784D51B19916D5 /* ScreenOrientation.swift in Sources */, + B2A84ED6937E4D6780A8372B /* MatchColors.swift in Sources */, + D7AAD6BB475B4734BA4EC4EA /* StepMatchScreen.swift in Sources */, + A7C1C8C352404A72A688CEF4 /* StepNetworkTestScreen.swift in Sources */, + 9C8922FBA5DD4DCA9885E649 /* StepTransmissionScreen.swift in Sources */, + D687E626CEC44A11B2BE78FB /* TeamBrandingEditor.swift in Sources */, + 279D1B02380D46DF835831B8 /* TeamColorPicker.swift in Sources */, + 475FE75875AC47A58609BF5B /* WizardComponents.swift in Sources */, + A80C909B160C46CDADF1ABF8 /* WizardShellScreen.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F741ACE11A14421D99BBA54D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 82D2D8D7BBF642EE8E042CDF /* ApiInstantTests.swift in Sources */, + 67BBA82C7CD04A9B85B08F23 /* MatchScoringRulesTests.swift in Sources */, + 6868D193A5874D62BD640F8E /* ScoreActionDecodeTests.swift in Sources */, + 7ACD03602B744B4982DB542C /* ScoreControllerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0831318C61FF46EDA63542ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2F5D6393282640E5A53D4CFC /* MatchLiveTv */; + targetProxy = C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 5C13053150F24474B8F31912 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 5EAD837968924A88BDE4D77F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + API_BASE_URL = "https://www.matchlivetv.it"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 21; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 978E94D1AF904CFE86165956 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 21; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; + }; + name = Debug; + }; + 9A8D41AAA16F4A448DFA4ED9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + API_BASE_URL = "https://www.matchlivetv.it"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 21; + DEVELOPMENT_TEAM = S8Q9TWBRG5; + ENABLE_TESTABILITY = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = MatchLiveTv/Resources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_ENABLE_EXPLICIT_MODULES = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B9E60323FF35415C9388228C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + CB913E17F096470F990A496F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 21; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.matchlivetv.match-live-tv.tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9E60323FF35415C9388228C /* Debug */, + 5C13053150F24474B8F31912 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A8D41AAA16F4A448DFA4ED9 /* Debug */, + 5EAD837968924A88BDE4D77F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 978E94D1AF904CFE86165956 /* Debug */, + CB913E17F096470F990A496F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/shogo4405/HaishinKit.swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */; + productName = RTMPHaishinKit; + }; + F652BB68B82C49218CCB7CD0 /* HaishinKit */ = { + isa = XCSwiftPackageProductDependency; + package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */; + productName = HaishinKit; + }; +/* End XCSwiftPackageProductDependency section */ }; - rootObject = FF548150A3A24533BC87A2B6 /* Project object */; + rootObject = A8764A9D2A234F91B52DDC89 /* Project object */; } diff --git a/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme b/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme index d4172ff..2974b5a 100644 --- a/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme +++ b/native/ios/MatchLiveTv.xcodeproj/xcshareddata/xcschemes/MatchLiveTv.xcscheme @@ -3,28 +3,28 @@ - + - + - + - + - + diff --git a/native/ios/MatchLiveTv/Data/API/ApiDtos.swift b/native/ios/MatchLiveTv/Data/API/ApiDtos.swift index ec963e3..61fbbcd 100644 --- a/native/ios/MatchLiveTv/Data/API/ApiDtos.swift +++ b/native/ios/MatchLiveTv/Data/API/ApiDtos.swift @@ -7,6 +7,10 @@ struct LoginRequest: Encodable { struct RefreshRequest: Encodable { let refreshToken: String + + enum CodingKeys: String, CodingKey { + case refreshToken = "refresh_token" + } } struct LoginResponse: Decodable { diff --git a/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift b/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift index 8d68578..443f196 100644 --- a/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift +++ b/native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift @@ -43,6 +43,8 @@ final class MatchLiveAPI: @unchecked Sendable { private let session: URLSession private var accessToken: String? private let baseURL: URL + /// Chiamato su 401 per rinnovare il token e ritentare la richiesta (una volta). + var tokenRefresher: (() async throws -> String?)? init(baseURL: URL = URL(string: AppConfig.apiV1)!, session: URLSession = .shared) { self.baseURL = baseURL @@ -50,7 +52,7 @@ final class MatchLiveAPI: @unchecked Sendable { } func setAccessToken(_ token: String?) { - accessToken = token + accessToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) } // MARK: - Auth @@ -232,10 +234,20 @@ final class MatchLiveAPI: @unchecked Sendable { return try await execute(request) } - private func execute(_ request: URLRequest) async throws -> T { + private func execute(_ request: URLRequest, canRefresh: Bool = true) async throws -> T { let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw APIError.invalidURL } - if http.statusCode == 401 { throw APIError.unauthorized } + if http.statusCode == 401 { + if canRefresh, !isPublicAuthRequest(request), let tokenRefresher { + if let newToken = try await tokenRefresher() { + accessToken = newToken + var retry = request + retry.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization") + return try await execute(retry, canRefresh: false) + } + } + throw APIError.unauthorized + } guard (200..<300).contains(http.statusCode) else { let body = String(data: data, encoding: .utf8) throw APIError.http(http.statusCode, body) @@ -249,6 +261,13 @@ final class MatchLiveAPI: @unchecked Sendable { throw APIError.decoding(error) } } + + private func isPublicAuthRequest(_ request: URLRequest) -> Bool { + guard let path = request.url?.path else { return false } + return path.hasSuffix("/auth/login") + || path.hasSuffix("/auth/refresh") + || path.hasSuffix("/auth/register") + } } private struct EmptyResponse: Decodable {} diff --git a/native/ios/MatchLiveTv/Data/AppContainer.swift b/native/ios/MatchLiveTv/Data/AppContainer.swift index d768389..50e8fbf 100644 --- a/native/ios/MatchLiveTv/Data/AppContainer.swift +++ b/native/ios/MatchLiveTv/Data/AppContainer.swift @@ -18,10 +18,15 @@ final class AppContainer: ObservableObject { let tokenStore = TokenStore() self.api = api self.tokenStore = tokenStore - authRepository = AuthRepository(api: api, tokenStore: tokenStore) { token in + let authRepository = AuthRepository(api: api, tokenStore: tokenStore) { token in api.setAccessToken(token) OverlayLogoCache.configure(token: token) } + self.authRepository = authRepository + api.tokenRefresher = { + guard let stored = authRepository.currentSession else { return nil } + return try await authRepository.refresh(stored.refreshToken).accessToken + } matchRepository = MatchRepository(api: api, tokenStore: tokenStore) sessionRepository = SessionRepository(api: api) scoreRepository = ScoreRepository(api: api) diff --git a/native/ios/MatchLiveTv/Data/Repository/AuthRepository.swift b/native/ios/MatchLiveTv/Data/Repository/AuthRepository.swift index 9faff80..dbee96c 100644 --- a/native/ios/MatchLiveTv/Data/Repository/AuthRepository.swift +++ b/native/ios/MatchLiveTv/Data/Repository/AuthRepository.swift @@ -28,8 +28,13 @@ final class AuthRepository { do { _ = try await api.me() return stored + } catch let error as APIError { + if case .unauthorized = error { + return try? await refresh(stored.refreshToken) + } + return stored } catch { - return try? await refresh(stored.refreshToken) + return stored } } diff --git a/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift b/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift new file mode 100644 index 0000000..256eb09 --- /dev/null +++ b/native/ios/MatchLiveTv/Streaming/BroadcastVideoOrientation.swift @@ -0,0 +1,31 @@ +import AVFoundation +import UIKit + +/// Allinea l'orientamento del sensore camera all'interfaccia (broadcast sempre landscape). +enum BroadcastVideoOrientation { + @MainActor + static func captureOrientation() -> AVCaptureVideoOrientation { + if let scene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) { + return captureOrientation(for: scene.interfaceOrientation) + } + return .landscapeRight + } + + @MainActor + static var isPortraitContent: Bool { + let orientation = captureOrientation() + return orientation == .portrait || orientation == .portraitUpsideDown + } + + private static func captureOrientation(for orientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation { + switch orientation { + case .portrait: return .portrait + case .portraitUpsideDown: return .portraitUpsideDown + case .landscapeLeft: return .landscapeLeft + case .landscapeRight: return .landscapeRight + default: return .landscapeRight + } + } +} diff --git a/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift b/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift index 5f7c841..34f2df3 100644 --- a/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift +++ b/native/ios/MatchLiveTv/Streaming/LiveBroadcastEngine.swift @@ -3,23 +3,20 @@ import Foundation import HaishinKit import RTMPHaishinKit import UIKit +import VideoToolbox -struct RTMPPublishTarget: Sendable { - let connectURL: String - let streamName: String -} +private struct BroadcastBitrateMonitor: StreamBitRateStrategy { + let mamimumVideoBitRate: Int = 0 + let mamimumAudioBitRate: Int = 0 + let onUpdate: @Sendable (Int) -> Void -enum RTMPUrlParser { - static func parse(_ urlString: String) -> RTMPPublishTarget? { - guard let url = URL(string: urlString), url.scheme?.hasPrefix("rtmp") == true else { return nil } - let pathParts = url.path.split(separator: "/").map(String.init) - guard pathParts.count >= 2 else { return nil } - let app = pathParts.dropLast().joined(separator: "/") - let streamName = pathParts.last ?? "" - var connect = "rtmp://\(url.host ?? "")" - if let port = url.port { connect += ":\(port)" } - connect += "/\(app)" - return RTMPPublishTarget(connectURL: connect, streamName: streamName) + func adjustBitrate(_ event: NetworkMonitorEvent, stream: some StreamConvertible) async { + switch event { + case .status(let report), .publishInsufficientBWOccured(let report): + onUpdate(max(0, report.currentBytesOutPerSecond / 1024)) + case .reset: + break + } } } @@ -28,20 +25,27 @@ final class LiveBroadcastEngine: ObservableObject { @Published private(set) var metrics = BroadcastMetrics() let mixer = MediaMixer() - let connection = RTMPConnection() - let stream: RTMPStream private let overlayRenderer = OverlayRenderer() private var config: BroadcastConfig? private var phase: BroadcastPhase = .idle private var metricsListener: ((BroadcastMetrics) -> Void)? - private var reconnectAttempts = 0 private var publishTask: Task? + private var readyStateTask: Task? private var pipelineConfigured = false private var isPortrait = false + private var orientationObserver: NSObjectProtocol? + private weak var previewView: MTHKView? + private var previewAttached = false + private var publishPending = false + private var publishInFlight = false + private var rtmpSession: (any Session)? + private static var rtmpFactoryRegistered = false - init() { - stream = RTMPStream(connection: connection) + private static func ensureRTMPFactoryRegistered() async { + guard !rtmpFactoryRegistered else { return } + await SessionBuilderFactory.shared.register(RTMPSessionFactory()) + rtmpFactoryRegistered = true } func setMetricsListener(_ block: ((BroadcastMetrics) -> Void)?) { @@ -53,50 +57,71 @@ final class LiveBroadcastEngine: ObservableObject { overlayRenderer.update(state: state, isPortrait: isPortrait) } + /// Anteprima locale: output del mixer (come HaishinKit PublishViewModel), non dello stream RTMP. func bindPreview(to view: MTHKView) async { - await stream.addOutput(view) + previewView = view + guard pipelineConfigured, !previewAttached else { return } + await mixer.addOutput(view) + previewAttached = true + if publishPending { + await startPendingPublish() + } } func preparePreview(config: BroadcastConfig) async throws { self.config = config + publishPending = false try await configurePipeline(config) setPhase(.preview) } - func startBroadcast(config: BroadcastConfig) async throws { + /// Configura camera/mixer e mette in coda il publish RTMP (eseguito quando l'anteprima è pronta). + func prepareForBroadcast(config: BroadcastConfig) async throws { self.config = config - publishTask?.cancel() - publishTask = nil + publishPending = true try await configurePipeline(config) setPhase(.connecting) - try await Task.sleep(nanoseconds: 350_000_000) - try await publish(config: config) + await startPendingPublish() + } + + func startBroadcast(config: BroadcastConfig) async throws { + try await prepareForBroadcast(config: config) } func pauseBroadcast() async { - try? await stream.close() + publishPending = false + publishTask?.cancel() + publishTask = nil + await teardownRTMP(keepPreview: true) setPhase(.paused) } func resumeBroadcast(config: BroadcastConfig) async throws { self.config = config + publishPending = true if !pipelineConfigured { try await configurePipeline(config) } setPhase(.connecting) - try await publish(config: config) + await startPendingPublish() } func stopBroadcast() async { publishTask?.cancel() publishTask = nil + publishPending = false + publishInFlight = false + stopOrientationMonitoring() + await teardownRTMP(keepPreview: false) + if let previewView, previewAttached { + await mixer.removeOutput(previewView) + } + previewAttached = false + previewView = nil overlayRenderer.detach() - try? await stream.close() - try? await connection.close() try? await mixer.stopRunning() pipelineConfigured = false setPhase(.idle) - reconnectAttempts = 0 } func release() async { @@ -126,19 +151,22 @@ final class LiveBroadcastEngine: ObservableObject { if let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { try await mixer.attachVideo(camera, track: 0) } - try await mixer.addOutput(stream) - try await mixer.startRunning() - overlayRenderer.attach(mixer: mixer, width: config.width, height: config.height, isPortrait: config.portrait) + await applyVideoOrientation() + startOrientationMonitoring() - var streamSettings = await stream.videoSettings - streamSettings.videoSize = .init(width: config.width, height: config.height) - streamSettings.bitRate = config.videoBitrate - streamSettings.maxKeyFrameIntervalDuration = 2 - try await stream.setVideoSettings(streamSettings) - var audioSettings = await stream.audioSettings - audioSettings.bitRate = config.audioBitrate - try await stream.setAudioSettings(audioSettings) + try await mixer.startRunning() + overlayRenderer.attach( + mixer: mixer, + width: config.width, + height: config.height, + isPortrait: isPortrait + ) pipelineConfigured = true + + if let previewView, !previewAttached { + await mixer.addOutput(previewView) + previewAttached = true + } } private func configureScreenSize(width: Int, height: Int) async { @@ -148,23 +176,175 @@ final class LiveBroadcastEngine: ObservableObject { }.value } + private func startPendingPublish() async { + guard publishPending, !publishInFlight else { return } + publishTask?.cancel() + publishTask = Task { + await waitForPreviewThenPublish() + } + } + + private func waitForPreviewThenPublish() async { + guard publishPending, let config else { return } + publishInFlight = true + defer { publishInFlight = false } + + for _ in 0..<80 { + if Task.isCancelled { return } + if previewAttached { break } + try? await Task.sleep(nanoseconds: 50_000_000) + } + guard previewAttached else { + publishPending = false + setPhase(.error, error: "Anteprima camera non pronta") + return + } + + setPhase(.connecting) + try? await Task.sleep(nanoseconds: 350_000_000) + guard publishPending else { return } + + do { + try await publish(config: config) + publishPending = false + } catch { + publishPending = false + let message = UserFacingError.message(for: error) ?? "Connessione RTMP fallita" + setPhase(.error, error: message) + } + } + private func publish(config: BroadcastConfig) async throws { - guard let target = RTMPUrlParser.parse(config.rtmpUrl) else { + guard let url = URL(string: config.rtmpUrl) else { throw APIError.http(400, "URL RTMP non valido") } - try await connection.connect(target.connectURL) - try await stream.publish(target.streamName) - reconnectAttempts = 0 + await Self.ensureRTMPFactoryRegistered() + await applyVideoOrientation() + await teardownRTMP(keepPreview: true) + + let session = try await SessionBuilderFactory.shared.make(url) + .setMode(.publish) + .build() + guard let session else { + throw APIError.http(500, "Impossibile creare sessione RTMP") + } + + let stream = await session.stream + try await applyCodecSettings(to: stream, config: config) + await stream.setBitRateStrategy(BroadcastBitrateMonitor { [weak self] kbps in + Task { @MainActor in + guard let self else { return } + self.metrics = BroadcastMetrics( + phase: self.phase, + bitrateKbps: kbps, + fps: self.config?.fps ?? self.metrics.fps, + connected: self.phase == .live, + lastError: self.metrics.lastError + ) + self.metricsListener?(self.metrics) + } + }) + await mixer.addOutput(stream) + rtmpSession = session + observeReadyState(session) + + try await session.connect { [weak self] in + Task { @MainActor in + guard let self, self.phase == .live else { return } + self.setPhase(.error, error: "Connessione RTMP interrotta") + } + } setPhase(.live) } + private func applyCodecSettings(to stream: any StreamConvertible, config: BroadcastConfig) async throws { + var videoSettings = await stream.videoSettings + videoSettings.videoSize = .init(width: config.width, height: config.height) + videoSettings.bitRate = config.videoBitrate + videoSettings.maxKeyFrameIntervalDuration = 2 + videoSettings.profileLevel = kVTProfileLevel_H264_Baseline_3_1 as String + videoSettings.expectedFrameRate = Double(config.fps) + try await stream.setVideoSettings(videoSettings) + + var audioSettings = await stream.audioSettings + audioSettings = AudioCodecSettings( + bitRate: config.audioBitrate, + downmix: audioSettings.downmix, + channelMap: audioSettings.channelMap, + sampleRate: 48_000, + format: audioSettings.format + ) + try await stream.setAudioSettings(audioSettings) + } + + private func observeReadyState(_ session: any Session) { + readyStateTask?.cancel() + readyStateTask = Task { + for await state in await session.readyState { + switch state { + case .open: + setPhase(.live) + case .closed where phase == .live: + setPhase(.error, error: "Connessione RTMP interrotta") + default: + break + } + } + } + } + + private func teardownRTMP(keepPreview: Bool) async { + readyStateTask?.cancel() + readyStateTask = nil + if let session = rtmpSession { + let stream = await session.stream + await mixer.removeOutput(stream) + try? await session.close() + } + rtmpSession = nil + if !keepPreview { + publishPending = false + } + } + + private func applyVideoOrientation() async { + let orientation = BroadcastVideoOrientation.captureOrientation() + await mixer.setVideoOrientation(orientation) + let portrait = BroadcastVideoOrientation.isPortraitContent + guard portrait != isPortrait else { return } + isPortrait = portrait + overlayRenderer.refreshOrientation(isPortrait: portrait) + } + + private func startOrientationMonitoring() { + stopOrientationMonitoring() + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + orientationObserver = NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + Task { @MainActor in + await self?.applyVideoOrientation() + } + } + } + + private func stopOrientationMonitoring() { + if let orientationObserver { + NotificationCenter.default.removeObserver(orientationObserver) + self.orientationObserver = nil + } + UIDevice.current.endGeneratingDeviceOrientationNotifications() + } + private func setPhase(_ newPhase: BroadcastPhase, error: String? = nil) { phase = newPhase metrics = BroadcastMetrics( phase: newPhase, bitrateKbps: metrics.bitrateKbps, fps: config?.fps ?? metrics.fps, - connected: newPhase == .live || newPhase == .reconnecting, + connected: newPhase == .live, lastError: error ) metricsListener?(metrics) diff --git a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift index ab29119..2ea5f72 100644 --- a/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift +++ b/native/ios/MatchLiveTv/UI/Broadcast/BroadcastScreen.swift @@ -79,7 +79,12 @@ struct BroadcastScreen: View { .onChange(of: scoreController.lastActionError) { message in if let message { snackbarMessage = message } } - .onChange(of: broadcastCoordinator.metrics.phase) { _ in updateOverlay() } + .onChange(of: broadcastCoordinator.metrics.phase) { phase in + updateOverlay() + if phase == .error, let message = broadcastCoordinator.metrics.lastError { + self.error = message + } + } .alert("Errore", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) { Button("OK") { onFinished() } } message: { @@ -287,18 +292,19 @@ struct BroadcastScreen: View { match = loadedMatch container.scoreController.bind(sessionId: sessionId, initial: loaded.score) wireCable() + await preloadLogos(for: loadedMatch) + loading = false + updateOverlay() + try? await Task.sleep(nanoseconds: 100_000_000) if let url = loaded.rtmpIngestUrl, !url.isEmpty { let config = broadcastConfig(for: loaded, rtmpUrl: url) if loaded.isPaused { try await container.broadcastCoordinator.engine.preparePreview(config: config) } else { - try await container.broadcastCoordinator.startBroadcast(config: config) + try await container.broadcastCoordinator.engine.prepareForBroadcast(config: config) } } - await preloadLogos(for: loadedMatch) startPolling() - loading = false - updateOverlay() } catch { if let message = UserFacingError.message(for: error) { self.error = message diff --git a/native/ios/MatchLiveTv/UI/Login/LoginScreen.swift b/native/ios/MatchLiveTv/UI/Login/LoginScreen.swift index 09a64d8..cb42578 100644 --- a/native/ios/MatchLiveTv/UI/Login/LoginScreen.swift +++ b/native/ios/MatchLiveTv/UI/Login/LoginScreen.swift @@ -58,13 +58,13 @@ struct LoginScreen: View { _ = try await container.authRepository.login(email: email.trimmingCharacters(in: .whitespaces), password: password) onLoggedIn() } catch { - let message = error.localizedDescription - if message.contains("401") { + if let apiError = error as? APIError, case .unauthorized = apiError { self.error = "Email o password non corretti" - } else if message.localizedCaseInsensitiveContains("timeout") { + } else if let message = UserFacingError.message(for: error), + message.localizedCaseInsensitiveContains("timeout") { self.error = "Server non raggiungibile. Verifica la connessione." } else { - self.error = message + self.error = UserFacingError.message(for: error) ?? "Accesso non riuscito" } } loading = false diff --git a/native/ios/MatchLiveTv/UI/System/ScreenOrientation.swift b/native/ios/MatchLiveTv/UI/System/ScreenOrientation.swift index 78f24bc..366266b 100644 --- a/native/ios/MatchLiveTv/UI/System/ScreenOrientation.swift +++ b/native/ios/MatchLiveTv/UI/System/ScreenOrientation.swift @@ -1,3 +1,5 @@ +import HaishinKit +import RTMPHaishinKit import SwiftUI import UIKit @@ -77,6 +79,9 @@ final class AppDelegate: NSObject, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { AppOrientation.lockPortrait() + Task { + await SessionBuilderFactory.shared.register(RTMPSessionFactory()) + } return true }