Stabilizza diretta iOS e aggiunge test simulatore pre-release.

Corregge lifecycle RTMP/preview tra partite, reset tabellone, orientamento
con debounce, condivisione su iPad e script test_ios_simulator per CI locale.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-20 12:02:14 +02:00
parent 0f69ebcccb
commit 731b43ea88
25 changed files with 1101 additions and 562 deletions

View File

@@ -277,7 +277,7 @@
offlineMsg.hidden = true;
if (!publisherOnline && showingCover && !paused) {
awaitingMsg.hidden = false;
awaitingMsg.textContent = "Copertina in onda — in attesa del segnale dal telefono.";
awaitingMsg.textContent = "In riconnessione - in attesa del video dall'evento";
} else {
awaitingMsg.hidden = !awaitingSignal;
}

View File

@@ -7,293 +7,305 @@
objects = {
/* 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 */; };
01BBD9C8228A4654BE791A09 /* WizardSessionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */; };
0712F271074B47B992C54B02 /* CompactScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */; };
0747277C94AD48939CBAE80B /* ScoreboardElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */; };
07C244E32A4D48A185963C6C /* ScoreControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */; };
1C71C5745AF541A793FBD415 /* TeamBrandingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */; };
1DE6EABFB67F4046957C9B88 /* StepMatchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */; };
266CD29FA3C048CC9413460A /* OverlayCanvasRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */; };
2757AF56199F46A49036C307 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */; };
306216CA292D4974B612AC06 /* SponsorElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A0F1558C174BD584AE0377 /* SponsorElement.swift */; };
3477E99DCB6843BB8037EB21 /* WizardComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC184551A43F44E09E536D29 /* WizardComponents.swift */; };
3628957732604F1597784B04 /* OverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */; };
38938AED35E94A99A4921FF3 /* OverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA971C263254BDC95AC2423 /* OverlayState.swift */; };
3C4DD1CE0BFA4CD9928234CD /* BroadcastVideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */; };
4236C0FB1B10435EB03C39DF /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */; };
44519A77D19C46218318593E /* OverlayMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */; };
49586D0F50AA4BE692FDCF3F /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */; };
4958D62D7A8944EAB3E54F18 /* ScoreState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880F5D2B2B13497891706E0A /* ScoreState.swift */; };
4B1080F3C1354A89A859ACC6 /* MatchRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */; };
4C26392C2FE69A090015AAF3 /* HaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3242173ACEDC4B288ACD56E4 /* HaishinKit */; };
4C26392D2FE69A090015AAF3 /* RTMPHaishinKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */; };
57B21B3B008A4345AEDFD7D2 /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818D265485A3445DAB7EAC48 /* ColorHex.swift */; };
5C80FF3E9F574F4EA2FDFEFF /* DeviceTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */; };
639783D4083F4049B5424C52 /* MediaUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA461A50E47740818FDC5890 /* MediaUrl.swift */; };
674D6A9F61D742DF95F93C5B /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A66D5B274FF34093901B981B /* AppConfig.swift */; };
7DC17CB11FC246378F6624C0 /* TeamColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573991C190AD43BBBDDD186C /* TeamColorPicker.swift */; };
7F7C79F0C4EA4B06A5A2E4BA /* ScreenOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */; };
815C82E2C5C54746ABF73BEE /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17256285CF354F0D8E4B33A3 /* SplashScreen.swift */; };
82D6D70F2E554414961F27C3 /* MatchScoringRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */; };
830325BE85674DD6A31CE672 /* WatermarkElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */; };
832E24DBB2844E9895717F6F /* LiveScoreActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */; };
83E38DFBBFFF4412BF2ABEE9 /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */; };
89F47A469F6044B9A92D647A /* LivePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F07B021AC24DCAA217238C /* LivePreviewView.swift */; };
8CDFA36E735A4A94A5A60DBF /* BroadcastControlsOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */; };
8DB866F0F33A4F3BAA074660 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */; };
8DE2FE7CF4A34222B2D3066A /* ApiDtos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB5819FE32342F1961F256E /* ApiDtos.swift */; };
9102501263E1477BAA6DEDD1 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2C8DD5093B84D5FBE360C23 /* Models.swift */; };
95848029AD4D4683B80DEA8C /* ScoreRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */; };
9770A3D9677F42AC942E98A4 /* BroadcastScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */; };
9A208AE54E434798BD5DF06F /* WizardShellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */; };
9EAD5508F44243409C697F41 /* BroadcastOrientationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */; };
9EDC5C176D2E40F29455CCEF /* ScoreActionDecodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */; };
9EF4BA3F81C341B4ACAB643F /* StepNetworkTestScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */; };
A3D8F65EBA8B479485321837 /* SessionCableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */; };
AB3DF660C79648CFAECB03CF /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951726866CB945DD917376C7 /* MatchStatusBadge.swift */; };
ABE74DCBA6804EDEB5398F8F /* MatchPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0C26984E50402281861375 /* MatchPrimaryButton.swift */; };
B578A98681A8413DBFEF58CA /* MatchColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */; };
B5937876D40F4E21A63DB92A /* ApiInstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5853956279E94634BFC1660C /* ApiInstant.swift */; };
B86EA543F6114B198D334FAF /* LiveBroadcastEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */; };
BAE5062DD4B749E6BC569CCA /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */; };
BB73AB6E535F473EA6C1D232 /* MatchSessionLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */; };
BD2628C89CAD450ABF613183 /* BroadcastPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */; };
C0CA6EBDA3E84641AFEC3A12 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136E90D700FD44D89C9D5659 /* LoginScreen.swift */; };
C1D94B81E44C4BFD8351B165 /* LiveBroadcastCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */; };
C9B2434416364C4BB263BB3F /* LiveBroadcastCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */; };
CBEE59E2E2DE4C058FF42022 /* MatchHubFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A264DB29100D404EA570CF47 /* MatchHubFilter.swift */; };
CCB2F270DD464E608046A60C /* MatchLiveTvApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24FAE643838541119B344969 /* MatchLiveTvApp.swift */; };
CEACB53FE13049B9959896F1 /* ScoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9525D894BB924BFFBC2D244C /* ScoreController.swift */; };
D1334C2CFEC145328396B7EC /* ActionCableClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */; };
D3C0878AD7354D41B540AD5B /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34DE015C16C54795A496C968 /* AppContainer.swift */; };
D67C481FC7DE46C6B7AD09C4 /* AuthRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */; };
D6F532927A6345339AD2149D /* TokenStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E7E256AA201434EBD01B7D4 /* TokenStore.swift */; };
DFC67AC6380B41BC8A8CE866 /* ApiInstantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */; };
E054E5E1703A4876B4E13F64 /* MatchScoringRules.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */; };
E5B931C144DA46819918A5EC /* MatchLiveAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */; };
E75B414604CB429A9CCE797C /* BroadcastModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E128E2B501C45419220F030 /* BroadcastModels.swift */; };
E93FDA6122664F67A5448E2D /* UserFacingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6BA18F615214E4184E199B2 /* UserFacingError.swift */; };
EA206B930BD448D3AC1E2578 /* OverlayLogoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */; };
EB3C108379594E7EBCBCE016 /* ModalRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */; };
EED290CEF4A141608F7BE515 /* KeepScreenOn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FBA01162B004E85821F206B /* KeepScreenOn.swift */; };
EF984C15882E4E328BFCDA74 /* SessionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */; };
F05733319E284083A5AA2784 /* MatchLiveTv/Resources/Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */; };
F212F8F1DA0C42619D13D30E /* StepTransmissionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */; };
F36DADA0DA9245A48E3DDB29 /* BroadcastOrientationPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */; };
F5B6F5CD2D9245BDB8E81055 /* MatchLiveWordmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */ = {
E0F627D71C6349F1857AFED5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A8764A9D2A234F91B52DDC89 /* Project object */;
containerPortal = 72A102A7C8964137A464FF32 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 2F5D6393282640E5A53D4CFC;
remoteGlobalIDString = 553940DB807C4DBFB63E9FFE;
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 = "<group>"; };
08A245DF805C482C89AEB2E4 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = "<group>"; };
0A84A2EA29C441EFA31498AD /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = "<group>"; };
0BE1F75D436643C39DFEE685 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = "<group>"; };
10C6CAC995404909A95482BF /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = "<group>"; };
1881B3AF2AC1430B93049FB9 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = "<group>"; };
1ED842B98F33423988B13960 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = "<group>"; };
21CD0798A85B4FBAB32957EE /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = "<group>"; };
2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = "<group>"; };
227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = "<group>"; };
26D7D448D0A34D9384C5E2D3 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = "<group>"; };
2962B319CBD24638BB4F3165 /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = "<group>"; };
2D0386B94B9042918A68CD76 /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = "<group>"; };
2E221E68194C4C92925A9CB0 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = "<group>"; };
2E5163A1C8C84E41A5F464F3 /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = "<group>"; };
2F55DC7DC4764DA99C378C26 /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = "<group>"; };
316ADC6162224A7E886873E7 /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = "<group>"; };
34C8D8270FA44E59A4BD0643 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = "<group>"; };
372F67B9C14F4C7181A18424 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = "<group>"; };
388619A53CF744C6A133A858 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = "<group>"; };
3CF97DD369E44828AA6EBF8D /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = "<group>"; };
3F5EB6A380DC4728866BD42A /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = "<group>"; };
3F780D9DA2C44DDDBF8083ED /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = "<group>"; };
4C0C71AB18CD400E99D2A2E6 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = "<group>"; };
4C3796D03EA5408C8ACB8C3F /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = "<group>"; };
4E38BEC61FFD4A0FB010AD13 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = "<group>"; };
4F873ACAE570437E9531FCC3 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = "<group>"; };
5086F0DA44004334881343ED /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = "<group>"; };
551CCAA3C2F64172A59E278B /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = "<group>"; };
584D8FC9F6F746E1AE83E96E /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = "<group>"; };
5D813398882A483280580618 /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = "<group>"; };
6E7DF17673B346B78CA4FBA7 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = "<group>"; };
6F05D132A65E45E8AED152F0 /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = "<group>"; };
7416D8DD1E3D43F2BD1F5863 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = "<group>"; };
7976A76F8D84463BA99F021B /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
7E472FBF3FC74060AB6C7097 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = "<group>"; };
806B1034AB5148B4BC55F093 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = "<group>"; };
8A0C2A49F46F432BBE5CDE3F /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = "<group>"; };
8E83B664666640A69A448557 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = "<group>"; };
8FA2B04BB3A143EC9555D5E5 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = "<group>"; };
944972DE154D444781B612ED /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = "<group>"; };
94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = "<group>"; };
96B4BBBD7E7D430282730B76 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = "<group>"; };
976DD89356184E0694B20CB8 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = "<group>"; };
978A9CB34DF54D4996D6FB3E /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = "<group>"; };
9E3A143FFE1C47C9A204B860 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = "<group>"; };
A13CA879C5774E6CAA53F3CD /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = "<group>"; };
A2627CF334644FCA8FD9950B /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = "<group>"; };
A86707AFE94F4002BB505F71 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = "<group>"; };
ACEF96DCDEA14A7290905C18 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = "<group>"; };
AE769961C58E4741B73938B8 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = "<group>"; };
B06452CE3CBF45DE915C5ECB /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = "<group>"; };
BA8A8C40CEC448A9B49F902A /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = "<group>"; };
C1B4955A8D804D7BB8235F0F /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = "<group>"; };
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 = "<group>"; };
CCC01BBAC06A412296955DB3 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = "<group>"; };
CE36CEAA140F4953A6872E9C /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = "<group>"; };
D6725372B2CE473F87167457 /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = "<group>"; };
D6B453249C904B299178A996 /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = "<group>"; };
D8398C3F0CC644579C5CEF35 /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = "<group>"; };
DE5A60F7644B4881985D4F15 /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = "<group>"; };
E3DEAC5182ED489798656300 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = "<group>"; };
E770890933A84622ABF4EA9F /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = "<group>"; };
EA6077D6649C4FEAA77E083B /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = "<group>"; };
EC311445F0144B59AE22DA96 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = "<group>"; };
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 = "<group>"; };
F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
F89921CBC0F64FDEA47164F5 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = "<group>"; };
FB19D7F3DBCE427E8FB29C53 /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = "<group>"; };
073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WatermarkElement.swift; path = MatchLiveTv/Streaming/Overlay/WatermarkElement.swift; sourceTree = "<group>"; };
09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepMatchScreen.swift; path = MatchLiveTv/UI/Wizard/StepMatchScreen.swift; sourceTree = "<group>"; };
0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionCableService.swift; path = MatchLiveTv/Data/Cable/SessionCableService.swift; sourceTree = "<group>"; };
136E90D700FD44D89C9D5659 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = "<group>"; };
17256285CF354F0D8E4B33A3 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplashScreen.swift; path = MatchLiveTv/UI/Splash/SplashScreen.swift; sourceTree = "<group>"; };
177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicyTests.swift; path = MatchLiveTvTests/BroadcastOrientationPolicyTests.swift; sourceTree = "<group>"; };
18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayLogoCache.swift; path = MatchLiveTv/Streaming/Overlay/OverlayLogoCache.swift; sourceTree = "<group>"; };
1E128E2B501C45419220F030 /* BroadcastModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastModels.swift; path = MatchLiveTv/Streaming/BroadcastModels.swift; sourceTree = "<group>"; };
1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = "<group>"; };
24FAE643838541119B344969 /* MatchLiveTvApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveTvApp.swift; path = MatchLiveTv/App/MatchLiveTvApp.swift; sourceTree = "<group>"; };
2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastEngine.swift; path = MatchLiveTv/Streaming/LiveBroadcastEngine.swift; sourceTree = "<group>"; };
2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ShareSheet.swift; path = MatchLiveTv/UI/System/ShareSheet.swift; sourceTree = "<group>"; };
2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamBrandingEditor.swift; path = MatchLiveTv/UI/Wizard/TeamBrandingEditor.swift; sourceTree = "<group>"; };
2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayMappings.swift; path = MatchLiveTv/Streaming/Overlay/OverlayMappings.swift; sourceTree = "<group>"; };
2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveScoreActions.swift; path = MatchLiveTv/UI/Broadcast/LiveScoreActions.swift; sourceTree = "<group>"; };
2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ModalRoutes.swift; path = MatchLiveTv/UI/Navigation/ModalRoutes.swift; sourceTree = "<group>"; };
34DE015C16C54795A496C968 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppContainer.swift; path = MatchLiveTv/Data/AppContainer.swift; sourceTree = "<group>"; };
3AA971C263254BDC95AC2423 /* OverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayState.swift; path = MatchLiveTv/Streaming/Overlay/OverlayState.swift; sourceTree = "<group>"; };
3FBA01162B004E85821F206B /* KeepScreenOn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepScreenOn.swift; path = MatchLiveTv/UI/System/KeepScreenOn.swift; sourceTree = "<group>"; };
46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastVideoOrientation.swift; path = MatchLiveTv/Streaming/BroadcastVideoOrientation.swift; sourceTree = "<group>"; };
573991C190AD43BBBDDD186C /* TeamColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TeamColorPicker.swift; path = MatchLiveTv/UI/Wizard/TeamColorPicker.swift; sourceTree = "<group>"; };
5853956279E94634BFC1660C /* ApiInstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstant.swift; path = MatchLiveTv/Core/ApiInstant.swift; sourceTree = "<group>"; };
5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveWordmark.swift; path = MatchLiveTv/UI/Components/MatchLiveWordmark.swift; sourceTree = "<group>"; };
5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatchLiveTvTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinator.swift; path = MatchLiveTv/Streaming/LiveBroadcastCoordinator.swift; sourceTree = "<group>"; };
5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastScreen.swift; path = MatchLiveTv/UI/Broadcast/BroadcastScreen.swift; sourceTree = "<group>"; };
677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayRenderer.swift; sourceTree = "<group>"; };
79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreControllerTests.swift; path = MatchLiveTvTests/ScoreControllerTests.swift; sourceTree = "<group>"; };
7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepNetworkTestScreen.swift; path = MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift; sourceTree = "<group>"; };
7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AuthRepository.swift; path = MatchLiveTv/Data/Repository/AuthRepository.swift; sourceTree = "<group>"; };
7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = "<group>"; };
810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastControlsOverlay.swift; path = MatchLiveTv/UI/Broadcast/BroadcastControlsOverlay.swift; sourceTree = "<group>"; };
818D265485A3445DAB7EAC48 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ColorHex.swift; path = MatchLiveTv/Core/ColorHex.swift; sourceTree = "<group>"; };
81F07B021AC24DCAA217238C /* LivePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LivePreviewView.swift; path = MatchLiveTv/Streaming/Preview/LivePreviewView.swift; sourceTree = "<group>"; };
880F5D2B2B13497891706E0A /* ScoreState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreState.swift; path = MatchLiveTv/Domain/ScoreState.swift; sourceTree = "<group>"; };
8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScreenOrientation.swift; path = MatchLiveTv/UI/System/ScreenOrientation.swift; sourceTree = "<group>"; };
8E7E256AA201434EBD01B7D4 /* TokenStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TokenStore.swift; path = MatchLiveTv/Core/TokenStore.swift; sourceTree = "<group>"; };
92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeviceTelemetry.swift; path = MatchLiveTv/Core/DeviceTelemetry.swift; sourceTree = "<group>"; };
951726866CB945DD917376C7 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = "<group>"; };
9525D894BB924BFFBC2D244C /* ScoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreController.swift; path = MatchLiveTv/Data/Scoring/ScoreController.swift; sourceTree = "<group>"; };
9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/ScoreboardElement.swift; sourceTree = "<group>"; };
9AB5819FE32342F1961F256E /* ApiDtos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiDtos.swift; path = MatchLiveTv/Data/API/ApiDtos.swift; sourceTree = "<group>"; };
A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastPermissions.swift; path = MatchLiveTv/UI/Permissions/BroadcastPermissions.swift; sourceTree = "<group>"; };
A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompactScoreboardElement.swift; path = MatchLiveTv/Streaming/Overlay/CompactScoreboardElement.swift; sourceTree = "<group>"; };
A264DB29100D404EA570CF47 /* MatchHubFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchHubFilter.swift; path = MatchLiveTv/Domain/MatchHubFilter.swift; sourceTree = "<group>"; };
A2C8DD5093B84D5FBE360C23 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Models.swift; path = MatchLiveTv/Domain/Models.swift; sourceTree = "<group>"; };
A66D5B274FF34093901B981B /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppConfig.swift; path = MatchLiveTv/Core/AppConfig.swift; sourceTree = "<group>"; };
A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MatchLiveTv/Resources/Assets.xcassets; sourceTree = "<group>"; };
A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreActionDecodeTests.swift; path = MatchLiveTvTests/ScoreActionDecodeTests.swift; sourceTree = "<group>"; };
AB5EAF0FD4BE4B14B5B029B8 /* MatchLiveTv/Resources/Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MatchLiveTv/Resources/Info.plist; sourceTree = "<group>"; };
B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRules.swift; path = MatchLiveTv/Domain/MatchScoringRules.swift; sourceTree = "<group>"; };
B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MatchLiveTv.app; sourceTree = BUILT_PRODUCTS_DIR; };
B5A0F1558C174BD584AE0377 /* SponsorElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SponsorElement.swift; path = MatchLiveTv/Streaming/Overlay/SponsorElement.swift; sourceTree = "<group>"; };
BA461A50E47740818FDC5890 /* MediaUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaUrl.swift; path = MatchLiveTv/Core/MediaUrl.swift; sourceTree = "<group>"; };
BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchRepository.swift; path = MatchLiveTv/Data/Repository/MatchRepository.swift; sourceTree = "<group>"; };
BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ActionCableClient.swift; path = MatchLiveTv/Data/Cable/ActionCableClient.swift; sourceTree = "<group>"; };
BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BroadcastOrientationPolicy.swift; path = MatchLiveTv/Streaming/BroadcastOrientationPolicy.swift; sourceTree = "<group>"; };
BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchLiveAPI.swift; path = MatchLiveTv/Data/API/MatchLiveAPI.swift; sourceTree = "<group>"; };
CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = "<group>"; };
CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LiveBroadcastCoordinatorTests.swift; path = MatchLiveTvTests/LiveBroadcastCoordinatorTests.swift; sourceTree = "<group>"; };
D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSessionLauncher.swift; path = MatchLiveTv/Data/Repository/MatchSessionLauncher.swift; sourceTree = "<group>"; };
D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Routes.swift; path = MatchLiveTv/UI/Navigation/Routes.swift; sourceTree = "<group>"; };
D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScoreRepository.swift; path = MatchLiveTv/Data/Repository/ScoreRepository.swift; sourceTree = "<group>"; };
DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionRepository.swift; path = MatchLiveTv/Data/Repository/SessionRepository.swift; sourceTree = "<group>"; };
DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchColors.swift; path = MatchLiveTv/UI/Theme/MatchColors.swift; sourceTree = "<group>"; };
E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StepTransmissionScreen.swift; path = MatchLiveTv/UI/Wizard/StepTransmissionScreen.swift; sourceTree = "<group>"; };
E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardSessionHolder.swift; path = MatchLiveTv/Data/WizardSessionHolder.swift; sourceTree = "<group>"; };
E6BA18F615214E4184E199B2 /* UserFacingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserFacingError.swift; path = MatchLiveTv/Core/UserFacingError.swift; sourceTree = "<group>"; };
EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScoringRulesTests.swift; path = MatchLiveTvTests/MatchScoringRulesTests.swift; sourceTree = "<group>"; };
F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardShellScreen.swift; path = MatchLiveTv/UI/Wizard/WizardShellScreen.swift; sourceTree = "<group>"; };
F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApiInstantTests.swift; path = MatchLiveTvTests/ApiInstantTests.swift; sourceTree = "<group>"; };
FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OverlayCanvasRenderer.swift; path = MatchLiveTv/Streaming/Overlay/OverlayCanvasRenderer.swift; sourceTree = "<group>"; };
FC184551A43F44E09E536D29 /* WizardComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WizardComponents.swift; path = MatchLiveTv/UI/Wizard/WizardComponents.swift; sourceTree = "<group>"; };
FE0C26984E50402281861375 /* MatchPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchPrimaryButton.swift; path = MatchLiveTv/UI/Components/MatchPrimaryButton.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
02B7024F8C4E4D41AE3B203A /* Frameworks */ = {
085E7F6CFD2B4475AC00D329 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4C26392D2FE69A090015AAF3 /* RTMPHaishinKit in Frameworks */,
4C26392C2FE69A090015AAF3 /* HaishinKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
4CD553EA1678427BB1BB0478 /* Frameworks */ = {
521337FFC4E94E5880B1F174 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4C5F8E4C2FE308D700962D65 /* RTMPHaishinKit in Frameworks */,
4C5F8E4B2FE308D700962D65 /* HaishinKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
146562E9E8F4444781CCEE1C /* MatchLiveTv */ = {
033861C6E3D248C6A4CEFAF6 /* Products */ = {
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 */,
B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */,
5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
631A87449B654E888E88BF77 = {
isa = PBXGroup;
children = (
705BD051C8004C5AA632BB29 /* MatchLiveTv */,
8658B6D49A814C9981463B85 /* MatchLiveTvTests */,
033861C6E3D248C6A4CEFAF6 /* Products */,
);
sourceTree = "<group>";
};
705BD051C8004C5AA632BB29 /* MatchLiveTv */ = {
isa = PBXGroup;
children = (
24FAE643838541119B344969 /* MatchLiveTvApp.swift */,
5853956279E94634BFC1660C /* ApiInstant.swift */,
A66D5B274FF34093901B981B /* AppConfig.swift */,
818D265485A3445DAB7EAC48 /* ColorHex.swift */,
92ABFE7C0ADC4F69B66FFAF6 /* DeviceTelemetry.swift */,
BA461A50E47740818FDC5890 /* MediaUrl.swift */,
8E7E256AA201434EBD01B7D4 /* TokenStore.swift */,
E6BA18F615214E4184E199B2 /* UserFacingError.swift */,
9AB5819FE32342F1961F256E /* ApiDtos.swift */,
BFA738A8723C440598F1B4A4 /* MatchLiveAPI.swift */,
34DE015C16C54795A496C968 /* AppContainer.swift */,
BEB49B33F26E4F9FBC5B8984 /* ActionCableClient.swift */,
0AD019C7B9EF4063AF3933A6 /* SessionCableService.swift */,
7C4E8DCB47E8409BA1BE8D1D /* AuthRepository.swift */,
BAC77F0B59AE4202BDEE66E1 /* MatchRepository.swift */,
D079A45CFC474FBAA96F57D1 /* MatchSessionLauncher.swift */,
D7176501FD1A4F06AC1F6557 /* ScoreRepository.swift */,
DC8E47E0A55F44A59F78C729 /* SessionRepository.swift */,
9525D894BB924BFFBC2D244C /* ScoreController.swift */,
E61DFC0396BF4C7CA7224E89 /* WizardSessionHolder.swift */,
A264DB29100D404EA570CF47 /* MatchHubFilter.swift */,
B10F4515DE324DC0ABFF8B24 /* MatchScoringRules.swift */,
A2C8DD5093B84D5FBE360C23 /* Models.swift */,
880F5D2B2B13497891706E0A /* ScoreState.swift */,
1E128E2B501C45419220F030 /* BroadcastModels.swift */,
BFA3A9BC8EFD47EEB2BD629D /* BroadcastOrientationPolicy.swift */,
46179BF6DAB34804A72D8DDB /* BroadcastVideoOrientation.swift */,
5E22842CB3AF4514965D3791 /* LiveBroadcastCoordinator.swift */,
2671666828B04BF7A91B1539 /* LiveBroadcastEngine.swift */,
A1B52CD9228B48FA85CC2D44 /* CompactScoreboardElement.swift */,
FBADDB48408844F39BFD1DC2 /* OverlayCanvasRenderer.swift */,
18B8BC4646CE4707B112E02E /* OverlayLogoCache.swift */,
2CC3BD7FC07E4BB29B022564 /* OverlayMappings.swift */,
677CE6F23F0445D9A30A52A9 /* OverlayRenderer.swift */,
3AA971C263254BDC95AC2423 /* OverlayState.swift */,
9A8F9E1CBDE54A15BAFCE1AD /* ScoreboardElement.swift */,
B5A0F1558C174BD584AE0377 /* SponsorElement.swift */,
073D0DFED0B3415DA15C8B28 /* WatermarkElement.swift */,
81F07B021AC24DCAA217238C /* LivePreviewView.swift */,
810AA10E867B4CC08328EC0D /* BroadcastControlsOverlay.swift */,
5FB7EC31451F4C07BB2DD674 /* BroadcastScreen.swift */,
2E98F8C22FB6416EB90F19BB /* LiveScoreActions.swift */,
5AD24618CFB64DCB846C25A2 /* MatchLiveWordmark.swift */,
FE0C26984E50402281861375 /* MatchPrimaryButton.swift */,
CD9CC837B7E847F4B294522D /* MatchScreenScaffold.swift */,
8D0D3DB5F3834E1587D1F187 /* MatchSecondaryButton.swift */,
951726866CB945DD917376C7 /* MatchStatusBadge.swift */,
136E90D700FD44D89C9D5659 /* LoginScreen.swift */,
7E28D48159B4487C9AE867D2 /* MatchesScreen.swift */,
1F2E617EAB1E4F65A7A6B5B0 /* AppNavHost.swift */,
2EDC8F2206484D8C9916E260 /* ModalRoutes.swift */,
D5F27D4CBFFA47DF9A9AB9A9 /* Routes.swift */,
A19DBD17AD504442A518E471 /* BroadcastPermissions.swift */,
17256285CF354F0D8E4B33A3 /* SplashScreen.swift */,
3FBA01162B004E85821F206B /* KeepScreenOn.swift */,
8E44D4F87459404B805ACEC8 /* ScreenOrientation.swift */,
2C8D6C06908D433AA4E4DF03 /* ShareSheet.swift */,
DFC3A6C1489646CA8DB996D0 /* MatchColors.swift */,
09528E80CD2840E0B9DEBAD2 /* StepMatchScreen.swift */,
7BF5EDE52B0A4E75BBA200AC /* StepNetworkTestScreen.swift */,
E3AA2F7662C04600BB71F47C /* StepTransmissionScreen.swift */,
2C9E84FF89324A4E929A68EA /* TeamBrandingEditor.swift */,
573991C190AD43BBBDDD186C /* TeamColorPicker.swift */,
FC184551A43F44E09E536D29 /* WizardComponents.swift */,
F6BA4AD8A2004074977D9F6A /* WizardShellScreen.swift */,
A6A99927A66246AF84CF8FC5 /* MatchLiveTv/Resources/Assets.xcassets */,
AB5EAF0FD4BE4B14B5B029B8 /* MatchLiveTv/Resources/Info.plist */,
);
name = MatchLiveTv;
sourceTree = "<group>";
};
25CDF42A0993431FA5F457EB = {
8658B6D49A814C9981463B85 /* MatchLiveTvTests */ = {
isa = PBXGroup;
children = (
146562E9E8F4444781CCEE1C /* MatchLiveTv */,
4190C9EAA59D4AA882933799 /* MatchLiveTvTests */,
B06BE907209A475AA6898BB6 /* Products */,
);
sourceTree = "<group>";
};
4190C9EAA59D4AA882933799 /* MatchLiveTvTests */ = {
isa = PBXGroup;
children = (
2222B10172FB4B41AE9E4EEE /* ApiInstantTests.swift */,
227E07193A504372A42AA447 /* MatchScoringRulesTests.swift */,
F711F4B006CD4A0CAE8788B2 /* ScoreActionDecodeTests.swift */,
94F0F380B8FD4B68AD92BD41 /* ScoreControllerTests.swift */,
F9E9DB7BBCBA4E35A0583979 /* ApiInstantTests.swift */,
177C409B72684FE7A3A977B8 /* BroadcastOrientationPolicyTests.swift */,
CF2A0A2676F8406FAACB786B /* LiveBroadcastCoordinatorTests.swift */,
EEC6D82784F44CF2885999DD /* MatchScoringRulesTests.swift */,
A70D09D7B58C4E67B59A7770 /* ScoreActionDecodeTests.swift */,
79438470374D4CB6B2969CCA /* ScoreControllerTests.swift */,
);
name = MatchLiveTvTests;
sourceTree = "<group>";
};
B06BE907209A475AA6898BB6 /* Products */ = {
isa = PBXGroup;
children = (
C3DCC59301E94417BC24492F /* MatchLiveTv.app */,
ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
2F5D6393282640E5A53D4CFC /* MatchLiveTv */ = {
553940DB807C4DBFB63E9FFE /* MatchLiveTv */ = {
isa = PBXNativeTarget;
buildConfigurationList = 56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */;
buildConfigurationList = 4332218393F64E3CA1F31139 /* Build configuration list for PBXNativeTarget "MatchLiveTv" */;
buildPhases = (
A547C0346D1345579D28B86D /* Sources */,
4CD553EA1678427BB1BB0478 /* Frameworks */,
CA0D00DDC25242438A7EBFEC /* Resources */,
12E25B1D521C492F8F41CECF /* Sources */,
085E7F6CFD2B4475AC00D329 /* Frameworks */,
3944EAB1EB4B4139BBA13CBB /* Resources */,
);
buildRules = (
);
@@ -301,40 +313,40 @@
);
name = MatchLiveTv;
packageProductDependencies = (
F652BB68B82C49218CCB7CD0 /* HaishinKit */,
8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */,
3242173ACEDC4B288ACD56E4 /* HaishinKit */,
F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */,
);
productName = MatchLiveTv;
productReference = C3DCC59301E94417BC24492F /* MatchLiveTv.app */;
productReference = B165DBC9A0DA4C62824D41C3 /* MatchLiveTv.app */;
productType = "com.apple.product-type.application";
};
938E427E770D4DFD8E992BFA /* MatchLiveTvTests */ = {
79FDBB021E5045C5A353AEE8 /* MatchLiveTvTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */;
buildConfigurationList = 6F0E68806C0E4C1FBAC0863B /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */;
buildPhases = (
F741ACE11A14421D99BBA54D /* Sources */,
02B7024F8C4E4D41AE3B203A /* Frameworks */,
83261799604545698E663330 /* Sources */,
521337FFC4E94E5880B1F174 /* Frameworks */,
);
buildRules = (
);
dependencies = (
0831318C61FF46EDA63542ED /* PBXTargetDependency */,
89AF4750097043F984332B2B /* PBXTargetDependency */,
);
name = MatchLiveTvTests;
productName = MatchLiveTvTests;
productReference = ED0935DD32EB4B28AA67CB67 /* MatchLiveTvTests.xctest */;
productReference = 5C49570E1FB3460BBEB502FC /* MatchLiveTvTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A8764A9D2A234F91B52DDC89 /* Project object */ = {
72A102A7C8964137A464FF32 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 0;
LastSwiftUpdateCheck = 1600;
};
buildConfigurationList = 1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */;
buildConfigurationList = E2AD215689B3443E85A02B4C /* Build configuration list for PBXProject "MatchLiveTv" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = it;
hasScannedForEncodings = 0;
@@ -343,158 +355,129 @@
en,
Base,
);
mainGroup = 25CDF42A0993431FA5F457EB;
mainGroup = 631A87449B654E888E88BF77;
packageReferences = (
9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */,
66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */,
);
productRefGroup = B06BE907209A475AA6898BB6 /* Products */;
productRefGroup = 033861C6E3D248C6A4CEFAF6 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
2F5D6393282640E5A53D4CFC /* MatchLiveTv */,
938E427E770D4DFD8E992BFA /* MatchLiveTvTests */,
553940DB807C4DBFB63E9FFE /* MatchLiveTv */,
79FDBB021E5045C5A353AEE8 /* MatchLiveTvTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
CA0D00DDC25242438A7EBFEC /* Resources */ = {
3944EAB1EB4B4139BBA13CBB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
ACBB5846610943D9AE3FD2B8 /* MatchLiveTv/Resources/Assets.xcassets in Resources */,
F05733319E284083A5AA2784 /* MatchLiveTv/Resources/Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A547C0346D1345579D28B86D /* Sources */ = {
12E25B1D521C492F8F41CECF /* 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 */,
CCB2F270DD464E608046A60C /* MatchLiveTvApp.swift in Sources */,
B5937876D40F4E21A63DB92A /* ApiInstant.swift in Sources */,
674D6A9F61D742DF95F93C5B /* AppConfig.swift in Sources */,
57B21B3B008A4345AEDFD7D2 /* ColorHex.swift in Sources */,
5C80FF3E9F574F4EA2FDFEFF /* DeviceTelemetry.swift in Sources */,
639783D4083F4049B5424C52 /* MediaUrl.swift in Sources */,
D6F532927A6345339AD2149D /* TokenStore.swift in Sources */,
E93FDA6122664F67A5448E2D /* UserFacingError.swift in Sources */,
8DE2FE7CF4A34222B2D3066A /* ApiDtos.swift in Sources */,
E5B931C144DA46819918A5EC /* MatchLiveAPI.swift in Sources */,
D3C0878AD7354D41B540AD5B /* AppContainer.swift in Sources */,
D1334C2CFEC145328396B7EC /* ActionCableClient.swift in Sources */,
A3D8F65EBA8B479485321837 /* SessionCableService.swift in Sources */,
D67C481FC7DE46C6B7AD09C4 /* AuthRepository.swift in Sources */,
4B1080F3C1354A89A859ACC6 /* MatchRepository.swift in Sources */,
BB73AB6E535F473EA6C1D232 /* MatchSessionLauncher.swift in Sources */,
95848029AD4D4683B80DEA8C /* ScoreRepository.swift in Sources */,
EF984C15882E4E328BFCDA74 /* SessionRepository.swift in Sources */,
CEACB53FE13049B9959896F1 /* ScoreController.swift in Sources */,
01BBD9C8228A4654BE791A09 /* WizardSessionHolder.swift in Sources */,
CBEE59E2E2DE4C058FF42022 /* MatchHubFilter.swift in Sources */,
E054E5E1703A4876B4E13F64 /* MatchScoringRules.swift in Sources */,
9102501263E1477BAA6DEDD1 /* Models.swift in Sources */,
4958D62D7A8944EAB3E54F18 /* ScoreState.swift in Sources */,
E75B414604CB429A9CCE797C /* BroadcastModels.swift in Sources */,
9EAD5508F44243409C697F41 /* BroadcastOrientationPolicy.swift in Sources */,
3C4DD1CE0BFA4CD9928234CD /* BroadcastVideoOrientation.swift in Sources */,
C9B2434416364C4BB263BB3F /* LiveBroadcastCoordinator.swift in Sources */,
B86EA543F6114B198D334FAF /* LiveBroadcastEngine.swift in Sources */,
0712F271074B47B992C54B02 /* CompactScoreboardElement.swift in Sources */,
266CD29FA3C048CC9413460A /* OverlayCanvasRenderer.swift in Sources */,
EA206B930BD448D3AC1E2578 /* OverlayLogoCache.swift in Sources */,
44519A77D19C46218318593E /* OverlayMappings.swift in Sources */,
3628957732604F1597784B04 /* OverlayRenderer.swift in Sources */,
38938AED35E94A99A4921FF3 /* OverlayState.swift in Sources */,
0747277C94AD48939CBAE80B /* ScoreboardElement.swift in Sources */,
306216CA292D4974B612AC06 /* SponsorElement.swift in Sources */,
830325BE85674DD6A31CE672 /* WatermarkElement.swift in Sources */,
89F47A469F6044B9A92D647A /* LivePreviewView.swift in Sources */,
8CDFA36E735A4A94A5A60DBF /* BroadcastControlsOverlay.swift in Sources */,
9770A3D9677F42AC942E98A4 /* BroadcastScreen.swift in Sources */,
832E24DBB2844E9895717F6F /* LiveScoreActions.swift in Sources */,
F5B6F5CD2D9245BDB8E81055 /* MatchLiveWordmark.swift in Sources */,
ABE74DCBA6804EDEB5398F8F /* MatchPrimaryButton.swift in Sources */,
2757AF56199F46A49036C307 /* MatchScreenScaffold.swift in Sources */,
83E38DFBBFFF4412BF2ABEE9 /* MatchSecondaryButton.swift in Sources */,
AB3DF660C79648CFAECB03CF /* MatchStatusBadge.swift in Sources */,
C0CA6EBDA3E84641AFEC3A12 /* LoginScreen.swift in Sources */,
4236C0FB1B10435EB03C39DF /* MatchesScreen.swift in Sources */,
49586D0F50AA4BE692FDCF3F /* AppNavHost.swift in Sources */,
EB3C108379594E7EBCBCE016 /* ModalRoutes.swift in Sources */,
8DB866F0F33A4F3BAA074660 /* Routes.swift in Sources */,
BD2628C89CAD450ABF613183 /* BroadcastPermissions.swift in Sources */,
815C82E2C5C54746ABF73BEE /* SplashScreen.swift in Sources */,
EED290CEF4A141608F7BE515 /* KeepScreenOn.swift in Sources */,
7F7C79F0C4EA4B06A5A2E4BA /* ScreenOrientation.swift in Sources */,
BAE5062DD4B749E6BC569CCA /* ShareSheet.swift in Sources */,
B578A98681A8413DBFEF58CA /* MatchColors.swift in Sources */,
1DE6EABFB67F4046957C9B88 /* StepMatchScreen.swift in Sources */,
9EF4BA3F81C341B4ACAB643F /* StepNetworkTestScreen.swift in Sources */,
F212F8F1DA0C42619D13D30E /* StepTransmissionScreen.swift in Sources */,
1C71C5745AF541A793FBD415 /* TeamBrandingEditor.swift in Sources */,
7DC17CB11FC246378F6624C0 /* TeamColorPicker.swift in Sources */,
3477E99DCB6843BB8037EB21 /* WizardComponents.swift in Sources */,
9A208AE54E434798BD5DF06F /* WizardShellScreen.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F741ACE11A14421D99BBA54D /* Sources */ = {
83261799604545698E663330 /* 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 */,
DFC67AC6380B41BC8A8CE866 /* ApiInstantTests.swift in Sources */,
F36DADA0DA9245A48E3DDB29 /* BroadcastOrientationPolicyTests.swift in Sources */,
C1D94B81E44C4BFD8351B165 /* LiveBroadcastCoordinatorTests.swift in Sources */,
82D6D70F2E554414961F27C3 /* MatchScoringRulesTests.swift in Sources */,
9EDC5C176D2E40F29455CCEF /* ScoreActionDecodeTests.swift in Sources */,
07C244E32A4D48A185963C6C /* ScoreControllerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0831318C61FF46EDA63542ED /* PBXTargetDependency */ = {
89AF4750097043F984332B2B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 2F5D6393282640E5A53D4CFC /* MatchLiveTv */;
targetProxy = C081F7F08D6748AB8C4B8DFB /* PBXContainerItemProxy */;
target = 553940DB807C4DBFB63E9FFE /* MatchLiveTv */;
targetProxy = E0F627D71C6349F1857AFED5 /* 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 */ = {
07CBA422677E4E909A377797 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
@@ -510,9 +493,25 @@
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv";
};
name = Release;
};
38453F3C33434DFD8F94862C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
SWIFT_VERSION = 5.0;
};
name = Release;
};
5C7F305207F64DC79FEA28D2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
9A8D41AAA16F4A448DFA4ED9 /* Debug */ = {
A1EF7CCA2C8D46938E2764C4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
API_BASE_URL = "https://www.matchlivetv.it";
@@ -539,15 +538,33 @@
};
name = Debug;
};
B9E60323FF35415C9388228C /* Debug */ = {
BE33A670C527464E85B1F231 /* Release */ = {
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;
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;
name = Release;
};
CB913E17F096470F990A496F /* Release */ = {
F6DDD32ED386440687F72916 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
@@ -563,34 +580,34 @@
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MatchLiveTv.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MatchLiveTv";
};
name = Release;
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1B3B522758634988987B6EE5 /* Build configuration list for PBXProject "MatchLiveTv" */ = {
4332218393F64E3CA1F31139 /* Build configuration list for PBXNativeTarget "MatchLiveTv" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B9E60323FF35415C9388228C /* Debug */,
5C13053150F24474B8F31912 /* Release */,
A1EF7CCA2C8D46938E2764C4 /* Debug */,
BE33A670C527464E85B1F231 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
56CDE22EB06747D988C0F28D /* Build configuration list for PBXNativeTarget "MatchLiveTv" */ = {
6F0E68806C0E4C1FBAC0863B /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9A8D41AAA16F4A448DFA4ED9 /* Debug */,
5EAD837968924A88BDE4D77F /* Release */,
F6DDD32ED386440687F72916 /* Debug */,
07CBA422677E4E909A377797 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7CCBB8C54461461C9BF93E4A /* Build configuration list for PBXNativeTarget "MatchLiveTvTests" */ = {
E2AD215689B3443E85A02B4C /* Build configuration list for PBXProject "MatchLiveTv" */ = {
isa = XCConfigurationList;
buildConfigurations = (
978E94D1AF904CFE86165956 /* Debug */,
CB913E17F096470F990A496F /* Release */,
5C7F305207F64DC79FEA28D2 /* Debug */,
38453F3C33434DFD8F94862C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
@@ -598,7 +615,7 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */ = {
66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/shogo4405/HaishinKit.swift";
requirement = {
@@ -609,17 +626,17 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
8DDADFA06433457CB69E9BC5 /* RTMPHaishinKit */ = {
3242173ACEDC4B288ACD56E4 /* HaishinKit */ = {
isa = XCSwiftPackageProductDependency;
package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */;
productName = RTMPHaishinKit;
};
F652BB68B82C49218CCB7CD0 /* HaishinKit */ = {
isa = XCSwiftPackageProductDependency;
package = 9BBC13CDC0274B3EA6A38447 /* XCRemoteSwiftPackageReference "HaishinKit" */;
package = 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */;
productName = HaishinKit;
};
F3539A3AB0894C54992DB7D5 /* RTMPHaishinKit */ = {
isa = XCSwiftPackageProductDependency;
package = 66E17694875F4BB2ADF2E1D6 /* XCRemoteSwiftPackageReference "HaishinKit" */;
productName = RTMPHaishinKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A8764A9D2A234F91B52DDC89 /* Project object */;
rootObject = 72A102A7C8964137A464FF32 /* Project object */;
}

View File

@@ -3,28 +3,28 @@
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES">
<BuildActionEntries>
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
</BuildActionEntry>
<BuildActionEntry buildForTesting="YES" buildForRunning="NO" buildForProfiling="NO" buildForArchiving="NO" buildForAnalyzing="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="938E427E770D4DFD8E992BFA" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="79FDBB021E5045C5A353AEE8" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES">
<Testables>
<TestableReference skipped="NO">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="938E427E770D4DFD8E992BFA" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="79FDBB021E5045C5A353AEE8" BuildableName="MatchLiveTvTests.xctest" BlueprintName="MatchLiveTvTests" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES">
<BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES">
<BuildableProductRunnable runnableDebuggingMode="0">
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="2F5D6393282640E5A53D4CFC" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="553940DB807C4DBFB63E9FFE" BuildableName="MatchLiveTv.app" BlueprintName="MatchLiveTv" ReferencedContainer="container:MatchLiveTv.xcodeproj"/>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction buildConfiguration="Debug"/>

View File

@@ -201,6 +201,29 @@ struct SetPartialDto: Codable {
let set: Int?
let home: Int?
let away: Int?
init(set: Int?, home: Int?, away: Int?) {
self.set = set
self.home = home
self.away = away
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
set = Self.decodeInt(container, key: .set)
home = Self.decodeInt(container, key: .home)
away = Self.decodeInt(container, key: .away)
}
enum CodingKeys: String, CodingKey {
case set, home, away
}
private static func decodeInt(_ container: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Int? {
if let value = try? container.decode(Int.self, forKey: key) { return value }
if let text = try? container.decode(String.self, forKey: key), let value = Int(text) { return value }
return nil
}
}
struct ScoreStateDto: Decodable {

View File

@@ -18,6 +18,7 @@ final class SessionCableService: ObservableObject {
private var deviceRole = "camera"
func connect(sessionId: String, accessToken: String, deviceRole: String = "camera") {
disconnect()
self.sessionId = sessionId
self.accessToken = accessToken
self.deviceRole = deviceRole
@@ -29,9 +30,12 @@ final class SessionCableService: ObservableObject {
func disconnect() {
intentionalDisconnect = true
reconnectTask?.cancel()
reconnectTask = nil
client?.disconnect()
client = nil
connected = false
sessionId = nil
accessToken = nil
}
func sendScoreUpdate(_ score: ScoreState) {
@@ -84,8 +88,8 @@ extension SessionCableService: ActionCableClient.Listener {
case "score_update":
onScoreUpdate?(ScoreState.fromCablePayload(payload))
case "command":
let command = payload["command"] as? String ?? ""
switch command {
let action = payload["action"] as? String ?? ""
switch action {
case "pause_stream": onPauseStream?()
case "resume_stream": onResumeStream?()
case "stop_stream": onStopStream?()

View File

@@ -18,13 +18,24 @@ final class ScoreController: ObservableObject {
}
func bind(sessionId: String, initial: ScoreState?) {
let sessionChanged = self.sessionId != sessionId
self.sessionId = sessionId
if let initial { score = initial }
if sessionChanged {
syncTask?.cancel()
syncGeneration += 1
lastActionError = nil
suppressRemoteUntil = .distantPast
score = initial ?? ScoreState()
} else if let initial {
score = initial
}
}
func applyRemote(_ remote: ScoreState) {
if remote == score { return }
if Date() < suppressRemoteUntil && remote.progressKey() <= score.progressKey() { return }
// Non accettare mai uno stato "più vecchio" (es. polling che sovrascrive close_set).
if remote.progressKey() < score.progressKey() { return }
if Date() < suppressRemoteUntil, remote.progressKey() == score.progressKey() { return }
score = remote
}
@@ -85,23 +96,55 @@ final class ScoreController: ObservableObject {
/// Chiude il set corrente tramite `close_set` sul motore backend.
func closeSetAsync() async -> Bool {
lastActionError = nil
syncTask?.cancel()
syncGeneration += 1
guard let sessionId else { return false }
let optimistic = closedSetState(from: score)
score = optimistic
sessionCable.sendScoreUpdate(optimistic)
do {
if let updated = try await scoreRepository.applyScoreAction(sessionId: sessionId, action: "close_set") {
suppressRemoteUntil = Date().addingTimeInterval(1.2)
suppressRemoteUntil = Date().addingTimeInterval(3)
score = updated
sessionCable.sendScoreUpdate(updated)
return true
}
return false
suppressRemoteUntil = Date().addingTimeInterval(3)
return true
} catch {
lastActionError = UserFacingError.message(for: error)
return false
}
}
private func closedSetState(from current: ScoreState) -> ScoreState {
let homeWon = current.homePoints > current.awayPoints
let awayWon = current.awayPoints > current.homePoints
var partials = current.setPartials
if current.homePoints > 0 || current.awayPoints > 0 {
partials.append(SetPartial(set: current.currentSet, home: current.homePoints, away: current.awayPoints))
}
return ScoreState(
homeSets: homeWon ? current.homeSets + 1 : current.homeSets,
awaySets: awayWon ? current.awaySets + 1 : current.awaySets,
homePoints: 0,
awayPoints: 0,
currentSet: current.currentSet + 1,
setPartials: partials,
timeoutHome: false,
timeoutAway: false,
boardType: current.boardType,
period: current.period,
periodLabel: current.periodLabel,
clockSecs: current.clockSecs,
clockRunning: current.clockRunning,
overtime: current.overtime
)
}
private func updateScore(_ transform: (inout ScoreState) -> Void) {
var next = score
transform(&next)
@@ -143,28 +186,7 @@ final class ScoreController: ObservableObject {
}
func closeSet() {
let current = score
let homeWon = current.homePoints > current.awayPoints
var partials = current.setPartials
if current.homePoints > 0 || current.awayPoints > 0 {
partials.append(SetPartial(set: current.currentSet, home: current.homePoints, away: current.awayPoints))
}
score = ScoreState(
homeSets: homeWon ? current.homeSets + 1 : current.homeSets,
awaySets: homeWon ? current.awaySets : current.awaySets + 1,
homePoints: 0,
awayPoints: 0,
currentSet: current.currentSet + 1,
setPartials: partials,
timeoutHome: false,
timeoutAway: false,
boardType: current.boardType,
period: current.period,
periodLabel: current.periodLabel,
clockSecs: current.clockSecs,
clockRunning: current.clockRunning,
overtime: current.overtime
)
score = closedSetState(from: score)
schedulePush()
}

View File

@@ -51,12 +51,19 @@ struct ScoreState: Equatable, Sendable {
}
func progressKey() -> Int64 {
Int64(currentSet) * 10_000_000_000
+ Int64(homeSets) * 1_000_000
+ Int64(awaySets) * 100_000
+ Int64(homePoints) * 1_000
+ Int64(awayPoints)
+ Int64(clockSecs)
var key = Int64(currentSet) * 10_000_000
key &+= Int64(homeSets) * 100_000
key &+= Int64(awaySets) * 1_000
key &+= Int64(homePoints) * 10
key &+= Int64(awayPoints)
key &+= Int64(clockSecs)
key &+= Int64(setPartials.count) * 1_000_000
for partial in setPartials {
key &+= Int64(partial.set) * 10_000
key &+= Int64(partial.home) * 100
key &+= Int64(partial.away)
}
return key
}
static let periodBoards: Set<String> = ["basket", "timed"]

View File

@@ -0,0 +1,28 @@
import AVFoundation
/// Regole orientamento in diretta (allineate al debounce Android).
enum BroadcastOrientationPolicy {
/// Attesa prima di applicare rotazione al mixer (evita burst da giro iPad).
static let debounceNanoseconds: UInt64 = 350_000_000
/// Finestra in cui ignorare `closed` RTMP dopo rotazione encoder.
static let suppressDisconnectSeconds: TimeInterval = 1.0
static func shouldApplyMixerOrientation(
new: AVCaptureVideoOrientation,
previous: AVCaptureVideoOrientation?,
landscapeLocked: Bool
) -> Bool {
guard isApplicableForBroadcast(new, landscapeLocked: landscapeLocked) else { return false }
guard let previous else { return true }
return new != previous
}
/// In diretta landscape ignoriamo portrait/unknown: evita glitch quando il sensore passa da verticale.
static func isApplicableForBroadcast(
_ orientation: AVCaptureVideoOrientation,
landscapeLocked: Bool
) -> Bool {
guard landscapeLocked else { return true }
return orientation == .landscapeLeft || orientation == .landscapeRight
}
}

View File

@@ -1,15 +1,16 @@
import AVFoundation
import HaishinKit
import UIKit
/// Allinea l'orientamento del sensore camera all'interfaccia (broadcast sempre landscape).
/// Orientamento camera allineato a HaishinKit DeviceUtil (broadcast landscape su iPad).
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)
if let scene = activeWindowScene(),
let orientation = DeviceUtil.videoOrientation(by: scene.interfaceOrientation) {
return orientation
}
// BroadcastScreen forza landscapeRight.
return .landscapeRight
}
@@ -19,13 +20,10 @@ enum BroadcastVideoOrientation {
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
}
@MainActor
private static func activeWindowScene() -> UIWindowScene? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first(where: { $0.activationState == .foregroundActive })
}
}

View File

@@ -6,6 +6,8 @@ final class LiveBroadcastCoordinator: ObservableObject {
let engine = LiveBroadcastEngine()
@Published private(set) var metrics = BroadcastMetrics()
private(set) var activeBroadcastSessionId: String?
init() {
engine.setMetricsListener { [weak self] value in
Task { @MainActor in self?.metrics = value }
@@ -22,24 +24,56 @@ final class LiveBroadcastCoordinator: ObservableObject {
try? session.setActive(true)
}
func startBroadcast(config: BroadcastConfig) async throws {
func waitForPreviewSurface() async -> Bool {
await engine.waitForPreviewSurface()
}
func prepareBroadcast(sessionId: String, config: BroadcastConfig, paused: Bool) async throws {
configureBackgroundAudio()
try await engine.startBroadcast(config: config)
if Self.shouldStopPreviousSession(active: activeBroadcastSessionId, incoming: sessionId) {
await engine.stopBroadcast(suppressDisconnectError: true)
}
activeBroadcastSessionId = sessionId
if paused {
try await engine.preparePreview(config: config)
_ = await engine.waitForPreviewReady()
} else {
try await engine.prepareForBroadcast(config: config)
}
}
func startBroadcast(sessionId: String, config: BroadcastConfig) async throws {
try await prepareBroadcast(sessionId: sessionId, config: config, paused: false)
}
func pauseBroadcast() async {
await engine.pauseBroadcast()
await engine.pauseBroadcast(suppressDisconnectError: true)
}
func resumeBroadcast(config: BroadcastConfig) async throws {
try await engine.resumeBroadcast(config: config)
}
func stopBroadcast() async {
await engine.stopBroadcast()
/// Ferma RTMP solo se la sessione indicata è ancora quella attiva (evita race tra dirette).
func stopBroadcast(sessionId: String) async {
guard activeBroadcastSessionId == sessionId else { return }
activeBroadcastSessionId = nil
await engine.stopBroadcast(suppressDisconnectError: true)
}
/// Chiusura cover / uscita forzata: ferma sempre lo stream corrente.
func forceStopBroadcast() async {
activeBroadcastSessionId = nil
await engine.stopBroadcast(suppressDisconnectError: true)
}
func release() async {
await engine.release()
await forceStopBroadcast()
}
/// `true` solo quando si passa da una diretta già attiva a un'altra sessione (non al primo avvio).
nonisolated static func shouldStopPreviousSession(active: String?, incoming: String) -> Bool {
guard let active else { return false }
return active != incoming
}
}

View File

@@ -39,6 +39,11 @@ final class LiveBroadcastEngine: ObservableObject {
private var previewAttached = false
private var publishPending = false
private var publishInFlight = false
private var broadcastGeneration = 0
private var suppressDisconnectError = false
private var orientationTransitionUntil: Date = .distantPast
private var lastAppliedCaptureOrientation: AVCaptureVideoOrientation?
private var orientationDebounceTask: Task<Void, Never>?
private var rtmpSession: (any Session)?
private static var rtmpFactoryRegistered = false
@@ -60,14 +65,38 @@ final class LiveBroadcastEngine: ObservableObject {
/// Anteprima locale: output del mixer (come HaishinKit PublishViewModel), non dello stream RTMP.
func bindPreview(to view: MTHKView) async {
previewView = view
guard pipelineConfigured, !previewAttached else { return }
await mixer.addOutput(view)
await attachPreviewIfNeeded()
}
private func attachPreviewIfNeeded() async {
guard let previewView, pipelineConfigured, !previewAttached else { return }
await mixer.addOutput(previewView)
previewAttached = true
if publishPending {
await startPendingPublish()
}
}
/// Attende che SwiftUI monti la surface di anteprima (MTHKView).
func waitForPreviewSurface(maxAttempts: Int = 100) async -> Bool {
for _ in 0..<maxAttempts {
if previewView != nil { return true }
try? await Task.sleep(nanoseconds: 50_000_000)
}
return previewView != nil
}
/// Attende che l'anteprima sia collegata al mixer (dopo `configurePipeline`).
func waitForPreviewReady(maxAttempts: Int = 100) async -> Bool {
for _ in 0..<maxAttempts {
if Task.isCancelled { return previewAttached }
await attachPreviewIfNeeded()
if previewAttached { return true }
try? await Task.sleep(nanoseconds: 50_000_000)
}
return previewAttached
}
func preparePreview(config: BroadcastConfig) async throws {
self.config = config
publishPending = false
@@ -88,12 +117,15 @@ final class LiveBroadcastEngine: ObservableObject {
try await prepareForBroadcast(config: config)
}
func pauseBroadcast() async {
func pauseBroadcast(suppressDisconnectError: Bool = false) async {
self.suppressDisconnectError = suppressDisconnectError
defer { self.suppressDisconnectError = false }
broadcastGeneration += 1
publishPending = false
publishTask?.cancel()
publishTask = nil
await teardownRTMP(keepPreview: true)
setPhase(.paused)
await teardownRTMP(keepPreview: true)
}
func resumeBroadcast(config: BroadcastConfig) async throws {
@@ -101,27 +133,35 @@ final class LiveBroadcastEngine: ObservableObject {
publishPending = true
if !pipelineConfigured {
try await configurePipeline(config)
} else {
await attachPreviewIfNeeded()
}
setPhase(.connecting)
await startPendingPublish()
}
func stopBroadcast() async {
func stopBroadcast(suppressDisconnectError: Bool = false) async {
self.suppressDisconnectError = suppressDisconnectError
defer { self.suppressDisconnectError = false }
broadcastGeneration += 1
publishTask?.cancel()
publishTask = nil
publishPending = false
publishInFlight = false
stopOrientationMonitoring()
orientationDebounceTask?.cancel()
orientationDebounceTask = nil
lastAppliedCaptureOrientation = nil
orientationTransitionUntil = .distantPast
setPhase(.idle)
await teardownRTMP(keepPreview: false)
if let previewView, previewAttached {
await mixer.removeOutput(previewView)
}
previewAttached = false
previewView = nil
overlayRenderer.detach()
try? await mixer.stopRunning()
pipelineConfigured = false
setPhase(.idle)
}
func release() async {
@@ -133,7 +173,10 @@ final class LiveBroadcastEngine: ObservableObject {
AVCaptureDevice.authorizationStatus(for: .audio) == .authorized else {
throw APIError.http(403, "Permessi camera e microfono richiesti")
}
if pipelineConfigured { return }
if pipelineConfigured {
await attachPreviewIfNeeded()
return
}
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .allowBluetooth])
@@ -149,9 +192,11 @@ final class LiveBroadcastEngine: ObservableObject {
try await mixer.attachAudio(microphone, track: 0)
}
if let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
try await mixer.attachVideo(camera, track: 0)
try await mixer.attachVideo(camera, track: 0) { videoUnit in
videoUnit.isVideoMirrored = false
}
await applyVideoOrientation()
}
await applyVideoOrientationIfNeeded(force: true)
startOrientationMonitoring()
try await mixer.startRunning()
@@ -162,11 +207,7 @@ final class LiveBroadcastEngine: ObservableObject {
isPortrait: isPortrait
)
pipelineConfigured = true
if let previewView, !previewAttached {
await mixer.addOutput(previewView)
previewAttached = true
}
await attachPreviewIfNeeded()
}
private func configureScreenSize(width: Int, height: Int) async {
@@ -186,40 +227,48 @@ final class LiveBroadcastEngine: ObservableObject {
private func waitForPreviewThenPublish() async {
guard publishPending, let config else { return }
let generation = broadcastGeneration
publishInFlight = true
defer { publishInFlight = false }
for _ in 0..<80 {
for _ in 0..<120 {
if Task.isCancelled { return }
if generation != broadcastGeneration { return }
await attachPreviewIfNeeded()
if previewAttached { break }
try? await Task.sleep(nanoseconds: 50_000_000)
}
guard previewAttached else {
guard previewAttached, generation == broadcastGeneration else {
if generation == broadcastGeneration {
publishPending = false
setPhase(.error, error: "Anteprima camera non pronta")
}
return
}
setPhase(.connecting)
try? await Task.sleep(nanoseconds: 350_000_000)
guard publishPending else { return }
guard publishPending, generation == broadcastGeneration else { return }
do {
try await publish(config: config)
try await publish(config: config, generation: generation)
if generation == broadcastGeneration {
publishPending = false
}
} catch {
guard generation == broadcastGeneration else { return }
publishPending = false
let message = UserFacingError.message(for: error) ?? "Connessione RTMP fallita"
setPhase(.error, error: message)
}
}
private func publish(config: BroadcastConfig) async throws {
private func publish(config: BroadcastConfig, generation: Int) async throws {
guard let url = URL(string: config.rtmpUrl) else {
throw APIError.http(400, "URL RTMP non valido")
}
await Self.ensureRTMPFactoryRegistered()
await applyVideoOrientation()
await applyVideoOrientationIfNeeded(force: true)
await teardownRTMP(keepPreview: true)
let session = try await SessionBuilderFactory.shared.make(url)
@@ -246,14 +295,21 @@ final class LiveBroadcastEngine: ObservableObject {
})
await mixer.addOutput(stream)
rtmpSession = session
observeReadyState(session)
observeReadyState(session, generation: generation)
try await session.connect { [weak self] in
Task { @MainActor in
guard let self, self.phase == .live else { return }
guard let self,
!self.shouldSuppressDisconnectError,
self.broadcastGeneration == generation,
self.phase == .live else { return }
self.setPhase(.error, error: "Connessione RTMP interrotta")
}
}
guard generation == broadcastGeneration else {
await teardownRTMP(keepPreview: true)
return
}
setPhase(.live)
}
@@ -277,14 +333,16 @@ final class LiveBroadcastEngine: ObservableObject {
try await stream.setAudioSettings(audioSettings)
}
private func observeReadyState(_ session: any Session) {
private func observeReadyState(_ session: any Session, generation: Int) {
readyStateTask?.cancel()
readyStateTask = Task {
for await state in await session.readyState {
guard generation == broadcastGeneration else { return }
switch state {
case .open:
setPhase(.live)
case .closed where phase == .live:
guard !shouldSuppressDisconnectError else { return }
setPhase(.error, error: "Connessione RTMP interrotta")
default:
break
@@ -307,15 +365,49 @@ final class LiveBroadcastEngine: ObservableObject {
}
}
private func applyVideoOrientation() async {
private var shouldSuppressDisconnectError: Bool {
suppressDisconnectError || Date() < orientationTransitionUntil
}
private func applyVideoOrientationIfNeeded(force: Bool = false) async {
let orientation = BroadcastVideoOrientation.captureOrientation()
let landscapeLocked = AppOrientation.mode == .landscape
if !force,
!BroadcastOrientationPolicy.shouldApplyMixerOrientation(
new: orientation,
previous: lastAppliedCaptureOrientation,
landscapeLocked: landscapeLocked
) {
return
}
guard BroadcastOrientationPolicy.isApplicableForBroadcast(orientation, landscapeLocked: landscapeLocked) else {
return
}
if phase == .live || phase == .connecting {
orientationTransitionUntil = Date().addingTimeInterval(BroadcastOrientationPolicy.suppressDisconnectSeconds)
}
lastAppliedCaptureOrientation = orientation
await mixer.setVideoOrientation(orientation)
try? await mixer.configuration(video: 0) { videoUnit in
videoUnit.isVideoMirrored = false
}
let portrait = BroadcastVideoOrientation.isPortraitContent
guard portrait != isPortrait else { return }
isPortrait = portrait
overlayRenderer.refreshOrientation(isPortrait: portrait)
}
private func scheduleOrientationUpdate() {
orientationDebounceTask?.cancel()
orientationDebounceTask = Task {
try? await Task.sleep(nanoseconds: BroadcastOrientationPolicy.debounceNanoseconds)
guard !Task.isCancelled else { return }
await applyVideoOrientationIfNeeded()
}
}
private func startOrientationMonitoring() {
stopOrientationMonitoring()
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
@@ -325,12 +417,14 @@ final class LiveBroadcastEngine: ObservableObject {
queue: .main
) { [weak self] _ in
Task { @MainActor in
await self?.applyVideoOrientation()
self?.scheduleOrientationUpdate()
}
}
}
private func stopOrientationMonitoring() {
orientationDebounceTask?.cancel()
orientationDebounceTask = nil
if let orientationObserver {
NotificationCenter.default.removeObserver(orientationObserver)
self.orientationObserver = nil

View File

@@ -4,12 +4,12 @@ struct WatermarkElement: OverlayElement {
private let logo = UIImage(named: "logo-white-m-256")
func draw(in context: CGContext, state: OverlayState, layout: OverlayLayout) {
guard state.overlayKind != .none, state.watermarkVisible, let logo, let cg = logo.cgImage else { return }
guard state.overlayKind != .none, state.watermarkVisible, let logo else { return }
let targetWidth = min(72, max(28, Int((CGFloat(layout.canvasWidth) * 0.055).rounded())))
let scale = CGFloat(targetWidth) / logo.size.width
let targetHeight = Int(logo.size.height * scale)
let left = layout.canvasWidth - layout.marginPx - targetWidth
let top = layout.canvasHeight - layout.marginPx - targetHeight
context.draw(cg, in: CGRect(x: left, y: top, width: targetWidth, height: targetHeight))
logo.draw(in: CGRect(x: left, y: top, width: targetWidth, height: targetHeight))
}
}

View File

@@ -7,11 +7,14 @@ struct LivePreviewView: UIViewRepresentable {
func makeUIView(context: Context) -> MTHKView {
let view = MTHKView(frame: .zero)
view.videoGravity = .resizeAspectFill
// resizeAspect: mostra tutto il frame 16:9 trasmesso (resizeAspectFill taglia i lati su iPad).
view.videoGravity = .resizeAspect
view.isUserInteractionEnabled = false
Task { await engine.bindPreview(to: view) }
return view
}
func updateUIView(_ uiView: MTHKView, context: Context) {}
func updateUIView(_ uiView: MTHKView, context: Context) {
Task { await engine.bindPreview(to: uiView) }
}
}

View File

@@ -130,6 +130,8 @@ struct BroadcastControlsOverlay: View {
)
}
}
.frame(width: sideToolbarWidth)
.allowsHitTesting(true)
}
private var rightToolbar: some View {
@@ -449,6 +451,7 @@ private struct SideIconButton: View {
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(enabled ? .white : MatchColors.textSecondary)
.frame(width: iconButtonSize, height: iconButtonSize)
.contentShape(Rectangle())
.background(backgroundColor, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)

View File

@@ -25,12 +25,21 @@ struct BroadcastScreen: View {
@State private var controlsVisible = true
@State private var logoReady = 0
@State private var pauseInFlight = false
@State private var resumeInFlight = false
@State private var deviceHealth = DeviceTelemetry.snapshot()
@State private var snackbarMessage: String?
@State private var shareItem: ShareItem?
@State private var bootstrapGeneration = 0
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
if permissions.allGranted {
LivePreviewView(engine: broadcastCoordinator.engine)
.ignoresSafeArea()
.allowsHitTesting(false)
.opacity(loading || session == nil || match == nil ? 0 : 1)
}
if loading {
ProgressView().tint(MatchColors.primaryRed)
} else if !permissions.allGranted {
@@ -46,9 +55,6 @@ struct BroadcastScreen: View {
}
.padding(24)
} else if let session, let match {
LivePreviewView(engine: broadcastCoordinator.engine)
.ignoresSafeArea()
.allowsHitTesting(false)
broadcastOverlay(session: session, match: match)
.id(scoreController.score.progressKey())
.zIndex(1)
@@ -59,22 +65,47 @@ struct BroadcastScreen: View {
.onAppear {
UIDevice.current.isBatteryMonitoringEnabled = true
}
.onDisappear {
Task { await teardown() }
}
.task {
await permissions.refresh()
if !permissions.allGranted {
await permissions.requestAll()
}
}
.task(id: permissions.allGranted) {
.task(id: "\(sessionId)-\(permissions.allGranted)") {
guard permissions.allGranted else {
loading = false
return
}
await bootstrap()
}
.task(id: sessionId) {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 4_000_000_000)
if let remote = try? await container.sessionRepository.fetchSession(id: sessionId).score {
container.scoreController.applyRemote(remote)
}
}
}
.task(id: sessionId) {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 2_000_000_000)
deviceHealth = DeviceTelemetry.snapshot()
}
}
.task(id: sessionId) {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 10_000_000_000)
let health = DeviceTelemetry.snapshot()
let metrics = container.broadcastCoordinator.metrics
try? await container.sessionRepository.postTelemetry(
sessionId: sessionId,
health: health,
currentBitrate: metrics.bitrateKbps * 1000,
targetBitrate: session?.targetBitrate,
fps: metrics.fps
)
}
}
.onChange(of: scoreController.score) { _ in updateOverlay() }
.onChange(of: scoreController.lastActionError) { message in
if let message { snackbarMessage = message }
@@ -90,6 +121,11 @@ struct BroadcastScreen: View {
} message: {
Text(error ?? "")
}
.sheet(item: $shareItem) { item in
ShareSheet(items: item.items, subject: item.subject) {
shareItem = nil
}
}
.background {
if let match {
ScoreDialogRouter(
@@ -215,7 +251,7 @@ struct BroadcastScreen: View {
onTerminate: { Task { await stopStream() } },
onShareLive: { shareLiveLink(session: session, subject: shareSubject) },
onShareRegia: { shareRegiaLink(subject: shareSubject) },
shareLiveEnabled: session.watchShareUrl() != nil,
shareLiveEnabled: true,
fps: metrics.fps,
targetFps: session.targetFps,
bitrateKbps: metrics.bitrateKbps,
@@ -229,7 +265,8 @@ struct BroadcastScreen: View {
rules: MatchScoringContext(match: match),
scoreController: scoreController,
dialogHost: scoreDialogHost,
onStopStream: { await stopStream() }
onStopStream: { await stopStream() },
onScoreboardChanged: { updateOverlay() }
)
}
@@ -254,58 +291,62 @@ struct BroadcastScreen: View {
}
private func shareLiveLink(session: StreamSession, subject: String) {
guard let urlString = session.watchShareUrl(), let url = URL(string: urlString) else {
let urlString = session.watchShareUrl() ?? "\(AppConfig.apiBaseUrl)/live/\(session.id)"
guard let url = URL(string: urlString) else {
snackbarMessage = "Link diretta non ancora disponibile"
return
}
presentShare(items: [url], subject: subject)
shareItem = ShareItem(items: [url], subject: subject)
}
private func shareRegiaLink(subject: String) {
Task {
Task { @MainActor in
do {
let urlString = try await container.sessionRepository.createRegiaLink(sessionId: sessionId)
guard let url = URL(string: urlString) else { return }
presentShare(items: [url], subject: "Link regia — \(subject)")
guard let url = URL(string: urlString) else {
snackbarMessage = "Link regia non valido"
return
}
shareItem = ShareItem(items: [url], subject: "Link regia — \(subject)")
} catch {
snackbarMessage = UserFacingError.message(for: error) ?? "Errore link regia"
}
}
}
private func presentShare(items: [Any], subject: String) {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else { return }
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
controller.setValue(subject, forKey: "subject")
root.present(controller, animated: true)
}
private func bootstrap() async {
bootstrapGeneration += 1
let generation = bootstrapGeneration
loading = true
error = nil
await container.broadcastCoordinator.stopBroadcast()
container.sessionCable.disconnect()
do {
let loaded = try await container.sessionRepository.fetchSession(id: sessionId)
guard generation == bootstrapGeneration else { return }
let loadedMatch = try await container.matchRepository.fetchMatch(matchId: loaded.matchId)
guard generation == bootstrapGeneration else { return }
session = loaded
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)
loading = false
guard generation == bootstrapGeneration else { return }
guard await container.broadcastCoordinator.waitForPreviewSurface() else {
throw APIError.http(500, "Anteprima camera non pronta")
}
guard generation == bootstrapGeneration else { return }
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.engine.prepareForBroadcast(config: config)
try await container.broadcastCoordinator.prepareBroadcast(
sessionId: sessionId,
config: config,
paused: loaded.isPaused
)
}
}
startPolling()
} catch {
guard generation == bootstrapGeneration else { return }
if let message = UserFacingError.message(for: error) {
self.error = message
}
@@ -319,11 +360,15 @@ struct BroadcastScreen: View {
container?.scoreController.applyRemote(remote)
}
container.sessionCable.onPauseStream = { [weak container] in
Task { @MainActor in await container?.broadcastCoordinator.pauseBroadcast() }
Task { @MainActor in
guard let container, !pauseInFlight else { return }
await applyRemotePause(container: container)
}
}
container.sessionCable.onResumeStream = { [weak container] in
Task { @MainActor in
await resumeStream()
guard let container, !resumeInFlight else { return }
await applyRemoteResume(container: container)
}
}
container.sessionCable.onStopStream = { [weak container] in
@@ -381,48 +426,26 @@ struct BroadcastScreen: View {
container.broadcastCoordinator.updateOverlay(state)
}
private func startPolling() {
Task {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 4_000_000_000)
if let remote = try? await container.sessionRepository.fetchSession(id: sessionId).score {
container.scoreController.applyRemote(remote)
}
}
}
Task {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 2_000_000_000)
deviceHealth = DeviceTelemetry.snapshot()
}
}
Task {
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 10_000_000_000)
let health = DeviceTelemetry.snapshot()
let metrics = container.broadcastCoordinator.metrics
try? await container.sessionRepository.postTelemetry(
sessionId: sessionId,
health: health,
currentBitrate: metrics.bitrateKbps * 1000,
targetBitrate: session?.targetBitrate,
fps: metrics.fps
)
}
}
}
private func pauseStream() {
guard !pauseInFlight else { return }
pauseInFlight = true
Task {
_ = try? await container.sessionRepository.pauseSession(id: sessionId)
defer { pauseInFlight = false }
do {
let updated = try await container.sessionRepository.pauseSession(id: sessionId)
session = updated
await container.broadcastCoordinator.pauseBroadcast()
pauseInFlight = false
snackbarMessage = "Diretta in pausa"
} catch {
snackbarMessage = UserFacingError.message(for: error) ?? "Pausa non riuscita"
}
}
}
private func resumeStream() async {
guard !resumeInFlight else { return }
resumeInFlight = true
defer { resumeInFlight = false }
do {
var updated = try await container.sessionRepository.resumeSession(id: sessionId)
session = updated
@@ -431,11 +454,39 @@ struct BroadcastScreen: View {
try await container.broadcastCoordinator.resumeBroadcast(config: config)
updated = try await container.sessionRepository.fetchSession(id: sessionId)
session = updated
snackbarMessage = "Diretta ripresa"
} catch {
if let message = UserFacingError.message(for: error) {
self.error = message
snackbarMessage = UserFacingError.message(for: error) ?? "Ripresa non riuscita"
}
}
/// Pausa dalla regia / echo cable: solo RTMP locale, senza PATCH pause.
private func applyRemotePause(container: AppContainer) async {
guard !pauseInFlight else { return }
pauseInFlight = true
defer { pauseInFlight = false }
await container.broadcastCoordinator.pauseBroadcast()
if let fetched = try? await container.sessionRepository.fetchSession(id: sessionId) {
session = fetched
}
snackbarMessage = "Pausa dalla regia"
}
/// Ripresa dalla regia / echo cable: solo RTMP locale, senza PATCH resume.
private func applyRemoteResume(container: AppContainer) async {
guard !resumeInFlight else { return }
resumeInFlight = true
defer { resumeInFlight = false }
do {
let current = try await container.sessionRepository.fetchSession(id: sessionId)
session = current
guard let url = current.rtmpIngestUrl, !url.isEmpty else { return }
let config = broadcastConfig(for: current, rtmpUrl: url)
try await container.broadcastCoordinator.resumeBroadcast(config: config)
snackbarMessage = "Diretta ripresa"
} catch {
snackbarMessage = UserFacingError.message(for: error) ?? "Ripresa RTMP non riuscita"
}
}
private func broadcastConfig(for loaded: StreamSession, rtmpUrl: String) -> BroadcastConfig {
@@ -456,7 +507,11 @@ struct BroadcastScreen: View {
}
private func teardown() async {
container.sessionCable.onScoreUpdate = nil
container.sessionCable.onPauseStream = nil
container.sessionCable.onResumeStream = nil
container.sessionCable.onStopStream = nil
container.sessionCable.disconnect()
await container.broadcastCoordinator.stopBroadcast()
await container.broadcastCoordinator.stopBroadcast(sessionId: sessionId)
}
}

View File

@@ -43,17 +43,20 @@ final class LiveScoreActions {
private let scoreController: ScoreController
private let dialogHost: LiveScoreDialogHost
private let onStopStream: () async -> Void
private let onScoreboardChanged: () -> Void
init(
rules: MatchScoringContext,
scoreController: ScoreController,
dialogHost: LiveScoreDialogHost,
onStopStream: @escaping () async -> Void
onStopStream: @escaping () async -> Void,
onScoreboardChanged: @escaping () -> Void = {}
) {
self.rules = rules
self.scoreController = scoreController
self.dialogHost = dialogHost
self.onStopStream = onStopStream
self.onScoreboardChanged = onScoreboardChanged
}
func afterPointChange(score: ScoreState) async {
@@ -74,6 +77,7 @@ final class LiveScoreActions {
)
if close {
guard await scoreController.closeSetAsync() else { return }
onScoreboardChanged()
await afterCloseSet()
}
}
@@ -92,6 +96,7 @@ final class LiveScoreActions {
if !ok { return }
}
guard await scoreController.closeSetAsync() else { return }
onScoreboardChanged()
await afterCloseSet()
}

View File

@@ -58,10 +58,17 @@ struct AppNavHost: View {
.lockPortraitOrientation()
}
.fullScreenCover(item: $broadcastRoute, onDismiss: {
Task { @MainActor in
container.sessionCable.disconnect()
await container.broadcastCoordinator.forceStopBroadcast()
}
AppOrientation.lockPortrait()
matchesRefreshToken += 1
}) { route in
BroadcastScreen(container: container, sessionId: route.sessionId) {
Task { @MainActor in
await container.broadcastCoordinator.stopBroadcast(sessionId: route.sessionId)
}
AppOrientation.lockPortrait()
container.wizardSession.reset()
broadcastRoute = nil

View File

@@ -0,0 +1,28 @@
import SwiftUI
import UIKit
struct ShareItem: Identifiable {
let id = UUID()
let items: [Any]
let subject: String?
}
/// Foglio condivisione compatibile iPhone/iPad (evita present() diretto da root VC).
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
var subject: String?
var onComplete: (() -> Void)?
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let subject {
controller.setValue(subject, forKey: "subject")
}
controller.completionWithItemsHandler = { _, _, _, _ in
onComplete?()
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

View File

@@ -19,6 +19,7 @@ struct StepNetworkTestScreen: View {
@State private var selectedQualityLabel: String?
@State private var starting = false
@State private var currentSession: StreamSession
@State private var shareItem: ShareItem?
init(
container: AppContainer,
@@ -154,6 +155,11 @@ struct StepNetworkTestScreen: View {
.task(id: session.id) {
await pollYoutubeIfNeeded()
}
.sheet(item: $shareItem) { item in
ShareSheet(items: item.items, subject: item.subject) {
shareItem = nil
}
}
}
private func runTest() {
@@ -223,8 +229,14 @@ struct StepNetworkTestScreen: View {
Task { @MainActor in
do {
let url = try await container.sessionRepository.createRegiaLink(sessionId: currentSession.id)
guard let link = URL(string: url) else { return }
presentShare(items: [link])
guard let link = URL(string: url) else {
onError("Link regia non valido")
return
}
shareItem = ShareItem(
items: [link],
subject: "Link regia — \(match.teamName) vs \(match.opponentName)"
)
} catch {
if let message = UserFacingError.message(for: error) {
onError(message)
@@ -233,13 +245,6 @@ struct StepNetworkTestScreen: View {
}
}
private func presentShare(items: [Any]) {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController else { return }
let controller = UIActivityViewController(activityItems: items, applicationActivities: nil)
root.present(controller, animated: true)
}
private func formatQualityLabel(_ result: NetworkTestResponse) -> String {
let bitrateMbps = Double(result.targetBitrate ?? 2_500_000) / 1_000_000.0
let fps = result.targetFps ?? 30

View File

@@ -0,0 +1,59 @@
import AVFoundation
import XCTest
@testable import MatchLiveTv
final class BroadcastOrientationPolicyTests: XCTestCase {
func testAppliesFirstLandscapeOrientation() {
XCTAssertTrue(
BroadcastOrientationPolicy.shouldApplyMixerOrientation(
new: .landscapeRight,
previous: nil,
landscapeLocked: true
)
)
}
func testSkipsDuplicateLandscapeOrientation() {
XCTAssertFalse(
BroadcastOrientationPolicy.shouldApplyMixerOrientation(
new: .landscapeRight,
previous: .landscapeRight,
landscapeLocked: true
)
)
}
func testAppliesLandscapeFlip() {
XCTAssertTrue(
BroadcastOrientationPolicy.shouldApplyMixerOrientation(
new: .landscapeLeft,
previous: .landscapeRight,
landscapeLocked: true
)
)
}
func testIgnoresPortraitWhenLandscapeLocked() {
XCTAssertFalse(
BroadcastOrientationPolicy.isApplicableForBroadcast(.portrait, landscapeLocked: true)
)
XCTAssertFalse(
BroadcastOrientationPolicy.shouldApplyMixerOrientation(
new: .portrait,
previous: .landscapeRight,
landscapeLocked: true
)
)
}
func testAllowsPortraitWhenNotLandscapeLocked() {
XCTAssertTrue(
BroadcastOrientationPolicy.isApplicableForBroadcast(.portrait, landscapeLocked: false)
)
}
func testDebounceAndSuppressWindowsArePositive() {
XCTAssertGreaterThan(BroadcastOrientationPolicy.debounceNanoseconds, 0)
XCTAssertGreaterThan(BroadcastOrientationPolicy.suppressDisconnectSeconds, 0)
}
}

View File

@@ -0,0 +1,16 @@
import XCTest
@testable import MatchLiveTv
final class LiveBroadcastCoordinatorTests: XCTestCase {
func testShouldStopPreviousSessionFalseOnFirstLaunch() {
XCTAssertFalse(LiveBroadcastCoordinator.shouldStopPreviousSession(active: nil, incoming: "session-a"))
}
func testShouldStopPreviousSessionFalseForSameSession() {
XCTAssertFalse(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-a"))
}
func testShouldStopPreviousSessionTrueWhenSwitching() {
XCTAssertTrue(LiveBroadcastCoordinator.shouldStopPreviousSession(active: "session-a", incoming: "session-b"))
}
}

View File

@@ -33,4 +33,36 @@ final class ScoreActionDecodeTests: XCTestCase {
XCTAssertEqual(score.homePoints, 1)
XCTAssertEqual(score.boardType, "volley")
}
func testDecodesCloseSetResponseWithPartials() throws {
let json = """
{
"id": "821ddb81-9dc2-466a-9ad5-2555e78f4653",
"match_id": "e887ce1d-10ee-4680-8323-79bb8f11e0aa",
"status": "live",
"platform": "matchlivetv",
"score": {
"type": "score_update",
"board_type": "volley",
"home_sets": 1,
"away_sets": 0,
"home_points": 0,
"away_points": 0,
"current_set": 2,
"set_partials": [{ "set": 1, "home": 25, "away": 18 }],
"timeout_home": false,
"timeout_away": false
},
"set_won": true,
"match_won": false,
"winner": "home"
}
""".data(using: .utf8)!
let dto = try ApiInstant.decoder.decode(StreamSessionDto.self, from: json)
let score = try XCTUnwrap(dto.score?.toDomain())
XCTAssertEqual(score.currentSet, 2)
XCTAssertEqual(score.setPartials.count, 1)
XCTAssertEqual(score.setPartials[0].home, 25)
XCTAssertEqual(score.setPartials[0].away, 18)
}
}

View File

@@ -3,6 +3,36 @@ import XCTest
@MainActor
final class ScoreControllerTests: XCTestCase {
func testBindNewSessionResetsScoreToZero() {
let controller = ScoreController(
scoreRepository: ScoreRepository(api: MatchLiveAPI()),
sessionCable: SessionCableService()
)
var previous = ScoreState()
previous.homePoints = 3
previous.awayPoints = 3
controller.bind(sessionId: "session-1", initial: previous)
XCTAssertEqual(controller.score.homePoints, 3)
controller.bind(sessionId: "session-2", initial: nil)
XCTAssertEqual(controller.score.homePoints, 0)
XCTAssertEqual(controller.score.awayPoints, 0)
}
func testBindNewSessionUsesServerInitialState() {
let controller = ScoreController(
scoreRepository: ScoreRepository(api: MatchLiveAPI()),
sessionCable: SessionCableService()
)
controller.bind(sessionId: "session-1", initial: ScoreState(homePoints: 2, awayPoints: 1))
var fresh = ScoreState()
fresh.homePoints = 1
fresh.awayPoints = 0
controller.bind(sessionId: "session-2", initial: fresh)
XCTAssertEqual(controller.score.homePoints, 1)
XCTAssertEqual(controller.score.awayPoints, 0)
}
func testIncrementHomeUpdatesPublishedScore() {
let controller = ScoreController(
scoreRepository: ScoreRepository(api: MatchLiveAPI()),

View File

@@ -8,6 +8,9 @@ SCHEME="MatchLiveTv"
CONFIG="${CONFIG:-Release}"
API_BASE_URL="${API_BASE_URL:-https://www.matchlivetv.it}"
echo "== Pre-release: test simulatore =="
"$ROOT/scripts/test_ios_simulator.sh"
cd "$IOS_DIR"
if [[ ! -d "$PROJECT" ]]; then

66
scripts/test_ios_simulator.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Test iOS su simulatore (arm64) — eseguire prima di ogni test su dispositivo / rilascio.
# Uso: ./scripts/test_ios_simulator.sh
# API=http://localhost:3000 ./scripts/test_ios_simulator.sh --e2e
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
IOS_DIR="$ROOT/native/ios"
SIM_ID="${SIM_ID:-48A837E2-845E-4043-B168-B8D4FDCAAE21}"
DERIVED="$IOS_DIR/build/DerivedData"
RUN_E2E=false
for arg in "$@"; do
case "$arg" in
--e2e) RUN_E2E=true ;;
-h|--help)
echo "Uso: $0 [--e2e]"
echo " --e2e include anche scripts/e2e_ios_simulator.sh (richiede API Rails su localhost:3000)"
exit 0
;;
esac
done
cd "$IOS_DIR"
python3 generate_xcodeproj.py
DEST="platform=iOS Simulator,id=$SIM_ID"
ARCH_FLAGS=(ONLY_ACTIVE_ARCH=YES ARCHS=arm64 EXCLUDED_ARCHS=x86_64)
echo "== Unit test iOS (simulatore arm64) =="
xcodebuild \
-project MatchLiveTv.xcodeproj \
-scheme MatchLiveTv \
-destination "$DEST" \
-derivedDataPath "$DERIVED" \
-configuration Debug \
"${ARCH_FLAGS[@]}" \
test 2>&1 | tee /tmp/matchlivetv-ios-test.log | tail -30
if grep -q "TEST SUCCEEDED" /tmp/matchlivetv-ios-test.log; then
echo "== Unit test: OK =="
else
echo "== Unit test: FALLITI (vedi /tmp/matchlivetv-ios-test.log) ==" >&2
exit 1
fi
echo "== Build simulatore (smoke) =="
xcodebuild \
-project MatchLiveTv.xcodeproj \
-scheme MatchLiveTv \
-destination "$DEST" \
-derivedDataPath "$DERIVED" \
-configuration Debug \
"${ARCH_FLAGS[@]}" \
build 2>&1 | tail -5
APP="$DERIVED/Build/Products/Debug-iphonesimulator/MatchLiveTv.app"
test -d "$APP"
echo "== Build simulatore: OK ($APP) =="
if $RUN_E2E; then
echo "== E2E API + simulatore =="
API="${API:-http://localhost:3000}" "$ROOT/scripts/e2e_ios_simulator.sh"
fi
echo "== test_ios_simulator.sh completato =="