@pickleball/expo-sdk 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +163 -40
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +47 -0
  6. package/dist/advanced.d.cts +3 -0
  7. package/dist/advanced.d.ts +3 -0
  8. package/dist/advanced.js +47 -0
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-2JHULJZ7.js +265 -0
  12. package/dist/chunk-62YV3PCD.cjs +139 -0
  13. package/dist/chunk-6F3HN6US.js +50 -0
  14. package/dist/chunk-6YE5BL3A.cjs +50 -0
  15. package/dist/chunk-7OX2DSKI.cjs +33 -0
  16. package/dist/chunk-7Z7TN5N3.js +318 -0
  17. package/dist/chunk-DHDX3ZOO.js +800 -0
  18. package/dist/chunk-GXQYMPHE.cjs +9 -0
  19. package/dist/chunk-IUIRVKJI.cjs +2290 -0
  20. package/dist/chunk-KFQGP6VL.js +33 -0
  21. package/dist/chunk-LCWGNGEN.cjs +265 -0
  22. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  23. package/dist/chunk-XGP2MN5R.js +2290 -0
  24. package/dist/chunk-XL4CISVQ.js +9 -0
  25. package/dist/chunk-YGSQ25UA.cjs +800 -0
  26. package/dist/chunk-ZSKSXOCZ.js +139 -0
  27. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  28. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  29. package/dist/device-agent/index.cjs +1427 -0
  30. package/dist/device-agent/index.d.cts +493 -0
  31. package/dist/device-agent/index.d.ts +493 -0
  32. package/dist/device-agent/index.js +1427 -0
  33. package/dist/engine-BSvSdCSE.d.cts +741 -0
  34. package/dist/engine-BSvSdCSE.d.ts +741 -0
  35. package/dist/index.cjs +426 -6174
  36. package/dist/index.d.cts +299 -1400
  37. package/dist/index.d.ts +299 -1400
  38. package/dist/index.js +413 -6063
  39. package/dist/match/index.cjs +2456 -0
  40. package/dist/match/index.d.cts +253 -0
  41. package/dist/match/index.d.ts +253 -0
  42. package/dist/match/index.js +2456 -0
  43. package/dist/match/remote/index.cjs +21 -0
  44. package/dist/match/remote/index.d.cts +67 -0
  45. package/dist/match/remote/index.d.ts +67 -0
  46. package/dist/match/remote/index.js +21 -0
  47. package/dist/match/voice/index.cjs +23 -0
  48. package/dist/match/voice/index.d.cts +114 -0
  49. package/dist/match/voice/index.d.ts +114 -0
  50. package/dist/match/voice/index.js +23 -0
  51. package/dist/plugin.cjs +52 -71
  52. package/dist/plugin.d.cts +28 -1
  53. package/dist/plugin.d.ts +28 -1
  54. package/dist/plugin.js +26 -23
  55. package/dist/realtime/index.cjs +31 -0
  56. package/dist/realtime/index.d.cts +15 -0
  57. package/dist/realtime/index.d.ts +15 -0
  58. package/dist/realtime/index.js +31 -0
  59. package/dist/types-CrmcX408.d.cts +176 -0
  60. package/dist/types-CrmcX408.d.ts +176 -0
  61. package/dist/types-DiKWjwvN.d.cts +96 -0
  62. package/dist/types-DiKWjwvN.d.ts +96 -0
  63. package/dist/ui/index.cjs +1297 -0
  64. package/dist/ui/index.d.cts +493 -0
  65. package/dist/ui/index.d.ts +493 -0
  66. package/dist/ui/index.js +1297 -0
  67. package/dist/version.cjs +4 -32
  68. package/dist/version.js +2 -1
  69. package/package.json +95 -15
  70. package/src/advanced.ts +49 -0
  71. package/src/auto-resume.tsx +259 -0
  72. package/src/camera-controls.ts +99 -0
  73. package/src/camera-grant-provider.ts +392 -0
  74. package/src/camera-settings.ts +133 -0
  75. package/src/contracts.ts +73 -0
  76. package/src/device-agent/agent.ts +696 -0
  77. package/src/device-agent/beat-policy.ts +46 -0
  78. package/src/device-agent/component.tsx +60 -0
  79. package/src/device-agent/convex-beat-transport.ts +140 -0
  80. package/src/device-agent/convex-command-subscription.ts +101 -0
  81. package/src/device-agent/convex-control-channel.ts +88 -0
  82. package/src/device-agent/http-transport.ts +164 -0
  83. package/src/device-agent/index.ts +26 -0
  84. package/src/device-agent/mqtt-channel.ts +379 -0
  85. package/src/device-agent/mqtt-codec.ts +347 -0
  86. package/src/device-agent/tournament.ts +352 -0
  87. package/src/engine.ts +601 -20
  88. package/src/errors.ts +16 -0
  89. package/src/http-session-provider.ts +21 -183
  90. package/src/index.ts +117 -14
  91. package/src/local-video-quality.ts +43 -0
  92. package/src/match/index.ts +29 -0
  93. package/src/match/provider.tsx +4 -2
  94. package/src/match/remote/hid-gesture.ts +10 -2
  95. package/src/match/remote/types.ts +6 -4
  96. package/src/match/voice/scheduler.ts +6 -1
  97. package/src/match/voice/service.ts +16 -4
  98. package/src/native-runtime.ts +283 -77
  99. package/src/plugin.ts +54 -20
  100. package/src/provider.tsx +129 -22
  101. package/src/realtime/index.ts +56 -0
  102. package/src/resume-policy.ts +85 -0
  103. package/src/resume.ts +132 -0
  104. package/src/rtmp-room-adapter.ts +348 -0
  105. package/src/session-grant.ts +256 -0
  106. package/src/ui/camera-screen.tsx +484 -0
  107. package/src/ui/controls-sheet.tsx +445 -0
  108. package/src/ui/copy.ts +326 -0
  109. package/src/ui/helpers.ts +105 -0
  110. package/src/ui/hooks.ts +59 -0
  111. package/src/ui/index.ts +42 -0
  112. package/src/ui/preview-view.tsx +39 -0
  113. package/src/ui/primitives.tsx +304 -0
  114. package/src/uplink-check.ts +173 -0
  115. package/dist/chunk-VHLUJQG5.js +0 -17
  116. package/src/livekit-room-adapter.ts +0 -353
  117. package/src/livestream-screen.tsx +0 -980
package/dist/plugin.d.cts CHANGED
@@ -1,10 +1,37 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
2
 
3
+ /**
4
+ * Version của SCHEMA plugin — tách khỏi version package có chủ ý. Giá trị này
5
+ * được inline vào dist/plugin.cjs, mà @expo/fingerprint băm file đó làm
6
+ * runtimeVersion của app fleet: gắn theo version package là MỖI lần bump SDK
7
+ * (kể cả patch JS-only) đổi fingerprint và toàn bộ máy ngoài sân mất OTA. Chỉ
8
+ * nâng khi plugin đổi hành vi native (permission, mod).
9
+ */
10
+ declare const PLUGIN_SCHEMA_VERSION = "1";
3
11
  interface PickleballLivePluginProps {
4
12
  cameraPermission?: string;
5
13
  microphonePermission?: string;
6
14
  bluetoothPermission?: string;
7
15
  consumeHidVolumeKeys?: boolean;
16
+ /**
17
+ * Tắt phần cấu hình native của module không dùng để app gọn hơn:
18
+ * - remote: false → bỏ permission Bluetooth (Android + iOS usage string) và
19
+ * không patch MainActivity nhận phím HID. Dùng khi app không import
20
+ * "@pickleball/expo-sdk/match/remote".
21
+ * - voice: false → hiện chưa gate gì trong plugin (mic thuộc livestream);
22
+ * nhận trước để tương thích về sau. Muốn loại hẳn native khỏi binary, thêm
23
+ * expo.autolinking.exclude trong package.json của app (xem README).
24
+ * - livekit: KHÔNG còn tác dụng từ 1.0 (SDK RTMP-only, không compose plugin
25
+ * LiveKit nữa). Vẫn nhận để app.config cũ của fleet không phải sửa — sửa
26
+ * là đổi fingerprint OTA.
27
+ */
28
+ modules?: {
29
+ remote?: boolean;
30
+ voice?: boolean;
31
+ /** @deprecated bị bỏ qua từ 1.0 */
32
+ livekit?: boolean;
33
+ };
34
+ /** @deprecated bị bỏ qua từ 1.0 (SDK không còn LiveKit). */
8
35
  liveKit?: {
9
36
  android?: {
10
37
  audioType?: string;
@@ -18,4 +45,4 @@ interface PickleballLivePluginProps {
18
45
  declare function injectHidVolumeMainActivity(source: string): string;
19
46
  declare const _default: ConfigPlugin<PickleballLivePluginProps | undefined>;
20
47
 
21
- export { type PickleballLivePluginProps, _default as default, injectHidVolumeMainActivity };
48
+ export { PLUGIN_SCHEMA_VERSION, type PickleballLivePluginProps, _default as default, injectHidVolumeMainActivity };
package/dist/plugin.d.ts CHANGED
@@ -1,10 +1,37 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
2
 
3
+ /**
4
+ * Version của SCHEMA plugin — tách khỏi version package có chủ ý. Giá trị này
5
+ * được inline vào dist/plugin.cjs, mà @expo/fingerprint băm file đó làm
6
+ * runtimeVersion của app fleet: gắn theo version package là MỖI lần bump SDK
7
+ * (kể cả patch JS-only) đổi fingerprint và toàn bộ máy ngoài sân mất OTA. Chỉ
8
+ * nâng khi plugin đổi hành vi native (permission, mod).
9
+ */
10
+ declare const PLUGIN_SCHEMA_VERSION = "1";
3
11
  interface PickleballLivePluginProps {
4
12
  cameraPermission?: string;
5
13
  microphonePermission?: string;
6
14
  bluetoothPermission?: string;
7
15
  consumeHidVolumeKeys?: boolean;
16
+ /**
17
+ * Tắt phần cấu hình native của module không dùng để app gọn hơn:
18
+ * - remote: false → bỏ permission Bluetooth (Android + iOS usage string) và
19
+ * không patch MainActivity nhận phím HID. Dùng khi app không import
20
+ * "@pickleball/expo-sdk/match/remote".
21
+ * - voice: false → hiện chưa gate gì trong plugin (mic thuộc livestream);
22
+ * nhận trước để tương thích về sau. Muốn loại hẳn native khỏi binary, thêm
23
+ * expo.autolinking.exclude trong package.json của app (xem README).
24
+ * - livekit: KHÔNG còn tác dụng từ 1.0 (SDK RTMP-only, không compose plugin
25
+ * LiveKit nữa). Vẫn nhận để app.config cũ của fleet không phải sửa — sửa
26
+ * là đổi fingerprint OTA.
27
+ */
28
+ modules?: {
29
+ remote?: boolean;
30
+ voice?: boolean;
31
+ /** @deprecated bị bỏ qua từ 1.0 */
32
+ livekit?: boolean;
33
+ };
34
+ /** @deprecated bị bỏ qua từ 1.0 (SDK không còn LiveKit). */
8
35
  liveKit?: {
9
36
  android?: {
10
37
  audioType?: string;
@@ -18,4 +45,4 @@ interface PickleballLivePluginProps {
18
45
  declare function injectHidVolumeMainActivity(source: string): string;
19
46
  declare const _default: ConfigPlugin<PickleballLivePluginProps | undefined>;
20
47
 
21
- export { type PickleballLivePluginProps, _default as default, injectHidVolumeMainActivity };
48
+ export { PLUGIN_SCHEMA_VERSION, type PickleballLivePluginProps, _default as default, injectHidVolumeMainActivity };
package/dist/plugin.js CHANGED
@@ -1,6 +1,4 @@
1
- import {
2
- PICKLEBALL_EXPO_SDK_VERSION
3
- } from "./chunk-VHLUJQG5.js";
1
+ import "./chunk-KFQGP6VL.js";
4
2
 
5
3
  // src/plugin.ts
6
4
  import {
@@ -8,24 +6,29 @@ import {
8
6
  createRunOncePlugin,
9
7
  withAndroidManifest,
10
8
  withDangerousMod,
11
- withInfoPlist,
12
- withPlugins
9
+ withInfoPlist
13
10
  } from "expo/config-plugins";
14
11
  import { readdir, readFile, writeFile } from "fs/promises";
15
12
  import { join } from "path";
16
- var ANDROID_PERMISSIONS = [
13
+ var PLUGIN_SCHEMA_VERSION = "1";
14
+ var CORE_ANDROID_PERMISSIONS = [
17
15
  "android.permission.ACCESS_NETWORK_STATE",
18
- "android.permission.BLUETOOTH",
19
- "android.permission.BLUETOOTH_ADMIN",
20
- "android.permission.BLUETOOTH_SCAN",
21
- "android.permission.BLUETOOTH_CONNECT",
22
- "android.permission.ACCESS_FINE_LOCATION",
23
16
  "android.permission.CAMERA",
17
+ "android.permission.FOREGROUND_SERVICE",
18
+ "android.permission.FOREGROUND_SERVICE_CAMERA",
19
+ "android.permission.FOREGROUND_SERVICE_MICROPHONE",
24
20
  "android.permission.INTERNET",
25
21
  "android.permission.MODIFY_AUDIO_SETTINGS",
26
22
  "android.permission.RECORD_AUDIO",
27
23
  "android.permission.WAKE_LOCK"
28
24
  ];
25
+ var REMOTE_ANDROID_PERMISSIONS = [
26
+ "android.permission.BLUETOOTH",
27
+ "android.permission.BLUETOOTH_ADMIN",
28
+ "android.permission.BLUETOOTH_SCAN",
29
+ "android.permission.BLUETOOTH_CONNECT",
30
+ "android.permission.ACCESS_FINE_LOCATION"
31
+ ];
29
32
  function injectHidVolumeMainActivity(source) {
30
33
  if (source.includes("PickleballRemoteHidDispatcher.dispatch(event)")) return source;
31
34
  if (!/class\s+MainActivity\s*:\s*ReactActivity\(\)\s*\{/.test(source)) {
@@ -60,19 +63,18 @@ var withPickleballLive = (config, props = {}) => {
60
63
  const cameraPermission = props?.cameraPermission ?? "Allow $(PRODUCT_NAME) to use the camera for livestreaming.";
61
64
  const microphonePermission = props?.microphonePermission ?? "Allow $(PRODUCT_NAME) to use the microphone for livestreaming.";
62
65
  const bluetoothPermission = props?.bluetoothPermission ?? "Cho ph\xE9p $(PRODUCT_NAME) d\xF9ng Bluetooth \u0111\u1EC3 nh\u1EADn thao t\xE1c t\u1EEB n\xFAt \u0111i\u1EC1u khi\u1EC3n tr\xEAn s\xE2n.";
63
- config = withPlugins(config, [
64
- ["@livekit/react-native-expo-plugin", props?.liveKit]
65
- ]);
66
+ const remoteEnabled = props?.modules?.remote !== false;
67
+ const androidPermissions = remoteEnabled ? [...CORE_ANDROID_PERMISSIONS, ...REMOTE_ANDROID_PERMISSIONS] : [...CORE_ANDROID_PERMISSIONS];
66
68
  config.ios = { ...config.ios, bitcode: false };
67
69
  config = withInfoPlist(config, (mod) => {
68
70
  mod.modResults.NSCameraUsageDescription = cameraPermission;
69
71
  mod.modResults.NSMicrophoneUsageDescription = microphonePermission;
70
- mod.modResults.NSBluetoothAlwaysUsageDescription = bluetoothPermission;
72
+ if (remoteEnabled) {
73
+ mod.modResults.NSBluetoothAlwaysUsageDescription = bluetoothPermission;
74
+ }
71
75
  return mod;
72
76
  });
73
- config = AndroidConfig.Permissions.withPermissions(config, [
74
- ...ANDROID_PERMISSIONS
75
- ]);
77
+ config = AndroidConfig.Permissions.withPermissions(config, androidPermissions);
76
78
  config = withAndroidManifest(config, (mod) => {
77
79
  const manifest = mod.modResults.manifest;
78
80
  const existing = manifest["uses-permission"] ?? [];
@@ -89,7 +91,7 @@ var withPickleballLive = (config, props = {}) => {
89
91
  const name = entry.$?.["android:name"];
90
92
  if (name && !byName.has(name)) byName.set(name, entry);
91
93
  }
92
- for (const permission of ANDROID_PERMISSIONS) {
94
+ for (const permission of androidPermissions) {
93
95
  const entry = byName.get(permission) ?? { $: { "android:name": permission } };
94
96
  entry.$ = {
95
97
  "android:name": permission,
@@ -97,17 +99,17 @@ var withPickleballLive = (config, props = {}) => {
97
99
  };
98
100
  byName.set(permission, entry);
99
101
  }
100
- const requiredNames = new Set(ANDROID_PERMISSIONS);
102
+ const requiredNames = new Set(androidPermissions);
101
103
  manifest["uses-permission"] = [
102
104
  ...existing.filter((entry) => {
103
105
  const name = entry.$?.["android:name"];
104
106
  return !name || !requiredNames.has(name);
105
107
  }),
106
- ...ANDROID_PERMISSIONS.map((name) => byName.get(name))
108
+ ...androidPermissions.map((name) => byName.get(name))
107
109
  ];
108
110
  return mod;
109
111
  });
110
- if (props?.consumeHidVolumeKeys !== false) {
112
+ if (remoteEnabled && props?.consumeHidVolumeKeys !== false) {
111
113
  config = withDangerousMod(config, ["android", async (mod) => {
112
114
  const root = join(mod.modRequest.platformProjectRoot, "app", "src", "main", "java");
113
115
  const path = await findMainActivity(root);
@@ -123,9 +125,10 @@ var withPickleballLive = (config, props = {}) => {
123
125
  var plugin_default = createRunOncePlugin(
124
126
  withPickleballLive,
125
127
  "@pickleball/expo-sdk",
126
- PICKLEBALL_EXPO_SDK_VERSION
128
+ PLUGIN_SCHEMA_VERSION
127
129
  );
128
130
  export {
131
+ PLUGIN_SCHEMA_VERSION,
129
132
  plugin_default as default,
130
133
  injectHidVolumeMainActivity
131
134
  };
@@ -0,0 +1,31 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+ var _chunk6YE5BL3Acjs = require('../chunk-6YE5BL3A.cjs');
5
+ require('../chunk-7OX2DSKI.cjs');
6
+
7
+ // src/realtime/index.ts
8
+ function createGrantCommandSubscription(getConvexUrl) {
9
+ const clients = /* @__PURE__ */ new Map();
10
+ return {
11
+ subscribe(credentials, sessionId, onCommands, onError) {
12
+ const url = getConvexUrl();
13
+ if (!url) return () => void 0;
14
+ let realtime = clients.get(url);
15
+ if (!realtime) {
16
+ realtime = _chunk6YE5BL3Acjs.createConvexDeviceRealtime.call(void 0, url);
17
+ clients.set(url, realtime);
18
+ }
19
+ return realtime.commandSubscription.subscribe(credentials, sessionId, onCommands, onError);
20
+ },
21
+ dispose() {
22
+ for (const realtime of clients.values()) realtime.dispose();
23
+ clients.clear();
24
+ }
25
+ };
26
+ }
27
+
28
+
29
+
30
+
31
+ exports.createConvexCommandSubscription = _chunk6YE5BL3Acjs.createConvexCommandSubscription; exports.createConvexDeviceRealtime = _chunk6YE5BL3Acjs.createConvexDeviceRealtime; exports.createGrantCommandSubscription = createGrantCommandSubscription;
@@ -0,0 +1,15 @@
1
+ export { C as ConvexDeviceRealtime, c as createConvexCommandSubscription, a as createConvexDeviceRealtime } from '../convex-command-subscription-DUgpu75l.cjs';
2
+ import { S as SessionCommandSubscription } from '../engine-BSvSdCSE.cjs';
3
+
4
+ /**
5
+ * Subscription cho app dùng camera grant: URL Convex nằm TRONG grant
6
+ * (`grant.convexUrl`) nên chỉ biết sau khi start — đọc URL lúc engine subscribe
7
+ * (ngay sau khi vào phiên), dựng MỘT client mỗi origin và tái dùng qua các
8
+ * phiên. Không có URL (deployment không lộ origin chuẩn) → không subscribe,
9
+ * engine giữ poll HTTP như cũ.
10
+ *
11
+ * Dùng với createCameraGrantProvider: `() => provider.getActiveGrant()?.convexUrl`.
12
+ */
13
+ declare function createGrantCommandSubscription(getConvexUrl: () => string | null | undefined): SessionCommandSubscription;
14
+
15
+ export { createGrantCommandSubscription };
@@ -0,0 +1,15 @@
1
+ export { C as ConvexDeviceRealtime, c as createConvexCommandSubscription, a as createConvexDeviceRealtime } from '../convex-command-subscription-Cabh9tND.js';
2
+ import { S as SessionCommandSubscription } from '../engine-BSvSdCSE.js';
3
+
4
+ /**
5
+ * Subscription cho app dùng camera grant: URL Convex nằm TRONG grant
6
+ * (`grant.convexUrl`) nên chỉ biết sau khi start — đọc URL lúc engine subscribe
7
+ * (ngay sau khi vào phiên), dựng MỘT client mỗi origin và tái dùng qua các
8
+ * phiên. Không có URL (deployment không lộ origin chuẩn) → không subscribe,
9
+ * engine giữ poll HTTP như cũ.
10
+ *
11
+ * Dùng với createCameraGrantProvider: `() => provider.getActiveGrant()?.convexUrl`.
12
+ */
13
+ declare function createGrantCommandSubscription(getConvexUrl: () => string | null | undefined): SessionCommandSubscription;
14
+
15
+ export { createGrantCommandSubscription };
@@ -0,0 +1,31 @@
1
+ import {
2
+ createConvexCommandSubscription,
3
+ createConvexDeviceRealtime
4
+ } from "../chunk-6F3HN6US.js";
5
+ import "../chunk-KFQGP6VL.js";
6
+
7
+ // src/realtime/index.ts
8
+ function createGrantCommandSubscription(getConvexUrl) {
9
+ const clients = /* @__PURE__ */ new Map();
10
+ return {
11
+ subscribe(credentials, sessionId, onCommands, onError) {
12
+ const url = getConvexUrl();
13
+ if (!url) return () => void 0;
14
+ let realtime = clients.get(url);
15
+ if (!realtime) {
16
+ realtime = createConvexDeviceRealtime(url);
17
+ clients.set(url, realtime);
18
+ }
19
+ return realtime.commandSubscription.subscribe(credentials, sessionId, onCommands, onError);
20
+ },
21
+ dispose() {
22
+ for (const realtime of clients.values()) realtime.dispose();
23
+ clients.clear();
24
+ }
25
+ };
26
+ }
27
+ export {
28
+ createConvexCommandSubscription,
29
+ createConvexDeviceRealtime,
30
+ createGrantCommandSubscription
31
+ };
@@ -0,0 +1,176 @@
1
+ type MatchFormat = "singles" | "doubles";
2
+ type MatchScoringMode = "rally" | "sideOut";
3
+ type MatchTeamId = "A" | "B";
4
+ type CourtSide = "left" | "right";
5
+ type MatchInputSource = "local" | "remote" | "voice" | "ble" | "system";
6
+ interface MatchPlayer {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ }
10
+ interface MatchTeam {
11
+ readonly id: MatchTeamId;
12
+ readonly name?: string;
13
+ /** Defaults to the first player in the roster. */
14
+ readonly initialServerId?: string;
15
+ readonly players: readonly MatchPlayer[];
16
+ }
17
+ interface MatchCreateInput {
18
+ readonly id: string;
19
+ readonly format: MatchFormat;
20
+ readonly scoringMode: MatchScoringMode;
21
+ readonly targetScore?: number;
22
+ readonly initialServingTeam?: MatchTeamId;
23
+ readonly teams: Readonly<Record<MatchTeamId, MatchTeam>>;
24
+ }
25
+ interface TeamScore {
26
+ readonly A: number;
27
+ readonly B: number;
28
+ }
29
+ interface ServeState {
30
+ readonly teamId: MatchTeamId;
31
+ readonly playerId: string;
32
+ readonly serverNumber: 1 | 2;
33
+ readonly court: CourtSide;
34
+ }
35
+ interface GameSnapshot {
36
+ readonly number: 1 | 2 | 3;
37
+ readonly score: TeamScore;
38
+ readonly startingServingTeam: MatchTeamId;
39
+ readonly winner: MatchTeamId | null;
40
+ readonly midGameSwitchCompleted: boolean;
41
+ }
42
+ type PendingCourtSwitch = {
43
+ readonly reason: "gameEnd";
44
+ readonly game: 1 | 2;
45
+ } | {
46
+ readonly reason: "gameThreeMidpoint";
47
+ readonly game: 3;
48
+ };
49
+ interface MatchSnapshot {
50
+ readonly id: string;
51
+ readonly format: MatchFormat;
52
+ readonly scoringMode: MatchScoringMode;
53
+ readonly targetScore: number;
54
+ readonly bestOf: 3;
55
+ readonly teams: Readonly<Record<MatchTeamId, MatchTeam>>;
56
+ readonly designatedServerByTeam: Readonly<Record<MatchTeamId, string>>;
57
+ readonly initialServingTeam: MatchTeamId;
58
+ readonly courtSides: Readonly<Record<CourtSide, MatchTeamId>>;
59
+ readonly playerCourts: Readonly<Record<string, CourtSide>>;
60
+ readonly games: readonly GameSnapshot[];
61
+ readonly currentGame: 1 | 2 | 3;
62
+ readonly gamesWon: TeamScore;
63
+ readonly serve: ServeState;
64
+ readonly pendingCourtSwitch: PendingCourtSwitch | null;
65
+ readonly status: "inProgress" | "completed" | "ended";
66
+ readonly winner: MatchTeamId | null;
67
+ readonly endReason: string | null;
68
+ readonly sequence: number;
69
+ }
70
+ interface MatchCorrection {
71
+ readonly score?: Partial<TeamScore>;
72
+ readonly serve?: ServeState;
73
+ readonly playerCourts?: Readonly<Record<string, CourtSide>>;
74
+ }
75
+ interface MatchEventBase {
76
+ readonly schemaVersion: 1;
77
+ readonly id: string;
78
+ readonly sequence: number;
79
+ readonly source: MatchInputSource;
80
+ }
81
+ interface MatchCreatedEvent extends MatchEventBase {
82
+ readonly type: "matchCreated";
83
+ readonly input: MatchCreateInput;
84
+ readonly snapshot: MatchSnapshot;
85
+ }
86
+ interface RallyAwardedEvent extends MatchEventBase {
87
+ readonly type: "rallyAwarded";
88
+ readonly displaySide: CourtSide;
89
+ readonly winnerTeamId: MatchTeamId;
90
+ }
91
+ interface CourtSwitchConfirmedEvent extends MatchEventBase {
92
+ readonly type: "courtSwitchConfirmed";
93
+ readonly pending: PendingCourtSwitch;
94
+ }
95
+ interface MatchCorrectedEvent extends MatchEventBase {
96
+ readonly type: "matchCorrected";
97
+ readonly correction: MatchCorrection;
98
+ }
99
+ interface MatchEndedEvent extends MatchEventBase {
100
+ readonly type: "matchEnded";
101
+ readonly reason: string;
102
+ }
103
+ interface MatchEventUndoneEvent extends MatchEventBase {
104
+ readonly type: "matchEventUndone";
105
+ readonly targetEventId: string;
106
+ readonly restoredSnapshot: MatchSnapshot;
107
+ }
108
+ type MatchEvent = MatchCreatedEvent | RallyAwardedEvent | CourtSwitchConfirmedEvent | MatchCorrectedEvent | MatchEndedEvent | MatchEventUndoneEvent;
109
+ interface MatchCreation {
110
+ readonly snapshot: MatchSnapshot;
111
+ readonly events: readonly [MatchCreatedEvent];
112
+ }
113
+
114
+ type AnnouncementKind = "score" | "serviceChange" | "gamePoint" | "gameWon" | "courtSwitch" | "matchWon";
115
+ type FallbackVoiceToken = `number-${number}` | "number-mot" | "number-lam" | `score-deu-${number}` | "score-deu-mot" | "score-deu-lam" | "server-1" | "server-2" | "court-left" | "court-right" | "service-change" | "game-point" | "game-won" | "court-switch" | "match-won" | "test-speaker";
116
+ interface AnnouncementPlan {
117
+ readonly kind: AnnouncementKind;
118
+ readonly text: string;
119
+ readonly score?: readonly [number, number];
120
+ readonly fallbackTokens: readonly FallbackVoiceToken[];
121
+ }
122
+ type VoiceAudioRoute = "system" | "bluetoothA2DP" | "bluetoothHFP" | "speaker" | "headphones" | "unknown";
123
+ type VoiceDisconnectReason = "oldDeviceUnavailable" | "becomingNoisy" | "unknown";
124
+ interface VoiceRouteState {
125
+ readonly route: VoiceAudioRoute;
126
+ readonly interrupted: boolean;
127
+ readonly disconnectedReason: VoiceDisconnectReason | null;
128
+ }
129
+ type VoiceAudioMode = "offline" | "live";
130
+ interface AudioPlayback {
131
+ configure(mode: VoiceAudioMode): Promise<void>;
132
+ play(source: string): Promise<void>;
133
+ stop(): Promise<void>;
134
+ dispose(): Promise<void>;
135
+ getRouteState(): VoiceRouteState;
136
+ subscribeToRouteChanges?(listener: (state: VoiceRouteState) => void): () => void;
137
+ }
138
+ type NeuralVoiceAvailability = "available" | "unavailable";
139
+ interface NeuralVoiceEngine {
140
+ availability(): Promise<NeuralVoiceAvailability>;
141
+ prewarm(commonPhrases: readonly string[]): Promise<void>;
142
+ synthesize(text: string, signal: AbortSignal): Promise<string>;
143
+ cancel(): Promise<void>;
144
+ release?(filePath: string): Promise<void>;
145
+ dispose(): Promise<void>;
146
+ }
147
+ /** Cloud TTS endpoint the SDK posts announcement text to (see apps/proxy /voice/tts). */
148
+ interface VoiceTtsOptions {
149
+ readonly url: string;
150
+ /** Extra request headers, e.g. partner bearer tokens. Re-evaluated per request. */
151
+ readonly headers?: () => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
152
+ readonly voiceName?: string;
153
+ readonly languageCode?: string;
154
+ readonly requestTimeoutMs?: number;
155
+ }
156
+ type VoiceErrorCode = "engine_unavailable" | "network_unavailable" | "synthesis_failed" | "storage_failure" | "playback_failed";
157
+ declare class VoiceError extends Error {
158
+ readonly code: VoiceErrorCode;
159
+ readonly name = "VoiceError";
160
+ constructor(code: VoiceErrorCode, message: string, options?: {
161
+ cause?: unknown;
162
+ });
163
+ }
164
+ interface MatchVoiceService {
165
+ readonly audioState: VoiceRouteState;
166
+ readonly lastError?: Error | null;
167
+ initialize(): Promise<void>;
168
+ subscribe(listener: () => void): () => void;
169
+ announce(previous: MatchSnapshot, current: MatchSnapshot, event: MatchEvent): void;
170
+ replayAnnouncement(): Promise<void>;
171
+ testSpeaker(): Promise<void>;
172
+ configureAudioMode?(mode: VoiceAudioMode): Promise<void>;
173
+ dispose(): Promise<void>;
174
+ }
175
+
176
+ export { type AnnouncementPlan as A, type CourtSide as C, type FallbackVoiceToken as F, type GameSnapshot as G, type MatchSnapshot as M, type NeuralVoiceEngine as N, type PendingCourtSwitch as P, type RallyAwardedEvent as R, type ServeState as S, type TeamScore as T, type VoiceAudioMode as V, type MatchEvent as a, type AudioPlayback as b, type VoiceRouteState as c, type VoiceTtsOptions as d, type MatchVoiceService as e, type AnnouncementKind as f, type NeuralVoiceAvailability as g, type VoiceAudioRoute as h, type VoiceDisconnectReason as i, VoiceError as j, type VoiceErrorCode as k, type MatchInputSource as l, type CourtSwitchConfirmedEvent as m, type MatchCorrection as n, type MatchCorrectedEvent as o, type MatchCreateInput as p, type MatchCreation as q, type MatchEndedEvent as r, type MatchEventUndoneEvent as s, type MatchCreatedEvent as t, type MatchFormat as u, type MatchPlayer as v, type MatchScoringMode as w, type MatchTeam as x, type MatchTeamId as y };
@@ -0,0 +1,176 @@
1
+ type MatchFormat = "singles" | "doubles";
2
+ type MatchScoringMode = "rally" | "sideOut";
3
+ type MatchTeamId = "A" | "B";
4
+ type CourtSide = "left" | "right";
5
+ type MatchInputSource = "local" | "remote" | "voice" | "ble" | "system";
6
+ interface MatchPlayer {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ }
10
+ interface MatchTeam {
11
+ readonly id: MatchTeamId;
12
+ readonly name?: string;
13
+ /** Defaults to the first player in the roster. */
14
+ readonly initialServerId?: string;
15
+ readonly players: readonly MatchPlayer[];
16
+ }
17
+ interface MatchCreateInput {
18
+ readonly id: string;
19
+ readonly format: MatchFormat;
20
+ readonly scoringMode: MatchScoringMode;
21
+ readonly targetScore?: number;
22
+ readonly initialServingTeam?: MatchTeamId;
23
+ readonly teams: Readonly<Record<MatchTeamId, MatchTeam>>;
24
+ }
25
+ interface TeamScore {
26
+ readonly A: number;
27
+ readonly B: number;
28
+ }
29
+ interface ServeState {
30
+ readonly teamId: MatchTeamId;
31
+ readonly playerId: string;
32
+ readonly serverNumber: 1 | 2;
33
+ readonly court: CourtSide;
34
+ }
35
+ interface GameSnapshot {
36
+ readonly number: 1 | 2 | 3;
37
+ readonly score: TeamScore;
38
+ readonly startingServingTeam: MatchTeamId;
39
+ readonly winner: MatchTeamId | null;
40
+ readonly midGameSwitchCompleted: boolean;
41
+ }
42
+ type PendingCourtSwitch = {
43
+ readonly reason: "gameEnd";
44
+ readonly game: 1 | 2;
45
+ } | {
46
+ readonly reason: "gameThreeMidpoint";
47
+ readonly game: 3;
48
+ };
49
+ interface MatchSnapshot {
50
+ readonly id: string;
51
+ readonly format: MatchFormat;
52
+ readonly scoringMode: MatchScoringMode;
53
+ readonly targetScore: number;
54
+ readonly bestOf: 3;
55
+ readonly teams: Readonly<Record<MatchTeamId, MatchTeam>>;
56
+ readonly designatedServerByTeam: Readonly<Record<MatchTeamId, string>>;
57
+ readonly initialServingTeam: MatchTeamId;
58
+ readonly courtSides: Readonly<Record<CourtSide, MatchTeamId>>;
59
+ readonly playerCourts: Readonly<Record<string, CourtSide>>;
60
+ readonly games: readonly GameSnapshot[];
61
+ readonly currentGame: 1 | 2 | 3;
62
+ readonly gamesWon: TeamScore;
63
+ readonly serve: ServeState;
64
+ readonly pendingCourtSwitch: PendingCourtSwitch | null;
65
+ readonly status: "inProgress" | "completed" | "ended";
66
+ readonly winner: MatchTeamId | null;
67
+ readonly endReason: string | null;
68
+ readonly sequence: number;
69
+ }
70
+ interface MatchCorrection {
71
+ readonly score?: Partial<TeamScore>;
72
+ readonly serve?: ServeState;
73
+ readonly playerCourts?: Readonly<Record<string, CourtSide>>;
74
+ }
75
+ interface MatchEventBase {
76
+ readonly schemaVersion: 1;
77
+ readonly id: string;
78
+ readonly sequence: number;
79
+ readonly source: MatchInputSource;
80
+ }
81
+ interface MatchCreatedEvent extends MatchEventBase {
82
+ readonly type: "matchCreated";
83
+ readonly input: MatchCreateInput;
84
+ readonly snapshot: MatchSnapshot;
85
+ }
86
+ interface RallyAwardedEvent extends MatchEventBase {
87
+ readonly type: "rallyAwarded";
88
+ readonly displaySide: CourtSide;
89
+ readonly winnerTeamId: MatchTeamId;
90
+ }
91
+ interface CourtSwitchConfirmedEvent extends MatchEventBase {
92
+ readonly type: "courtSwitchConfirmed";
93
+ readonly pending: PendingCourtSwitch;
94
+ }
95
+ interface MatchCorrectedEvent extends MatchEventBase {
96
+ readonly type: "matchCorrected";
97
+ readonly correction: MatchCorrection;
98
+ }
99
+ interface MatchEndedEvent extends MatchEventBase {
100
+ readonly type: "matchEnded";
101
+ readonly reason: string;
102
+ }
103
+ interface MatchEventUndoneEvent extends MatchEventBase {
104
+ readonly type: "matchEventUndone";
105
+ readonly targetEventId: string;
106
+ readonly restoredSnapshot: MatchSnapshot;
107
+ }
108
+ type MatchEvent = MatchCreatedEvent | RallyAwardedEvent | CourtSwitchConfirmedEvent | MatchCorrectedEvent | MatchEndedEvent | MatchEventUndoneEvent;
109
+ interface MatchCreation {
110
+ readonly snapshot: MatchSnapshot;
111
+ readonly events: readonly [MatchCreatedEvent];
112
+ }
113
+
114
+ type AnnouncementKind = "score" | "serviceChange" | "gamePoint" | "gameWon" | "courtSwitch" | "matchWon";
115
+ type FallbackVoiceToken = `number-${number}` | "number-mot" | "number-lam" | `score-deu-${number}` | "score-deu-mot" | "score-deu-lam" | "server-1" | "server-2" | "court-left" | "court-right" | "service-change" | "game-point" | "game-won" | "court-switch" | "match-won" | "test-speaker";
116
+ interface AnnouncementPlan {
117
+ readonly kind: AnnouncementKind;
118
+ readonly text: string;
119
+ readonly score?: readonly [number, number];
120
+ readonly fallbackTokens: readonly FallbackVoiceToken[];
121
+ }
122
+ type VoiceAudioRoute = "system" | "bluetoothA2DP" | "bluetoothHFP" | "speaker" | "headphones" | "unknown";
123
+ type VoiceDisconnectReason = "oldDeviceUnavailable" | "becomingNoisy" | "unknown";
124
+ interface VoiceRouteState {
125
+ readonly route: VoiceAudioRoute;
126
+ readonly interrupted: boolean;
127
+ readonly disconnectedReason: VoiceDisconnectReason | null;
128
+ }
129
+ type VoiceAudioMode = "offline" | "live";
130
+ interface AudioPlayback {
131
+ configure(mode: VoiceAudioMode): Promise<void>;
132
+ play(source: string): Promise<void>;
133
+ stop(): Promise<void>;
134
+ dispose(): Promise<void>;
135
+ getRouteState(): VoiceRouteState;
136
+ subscribeToRouteChanges?(listener: (state: VoiceRouteState) => void): () => void;
137
+ }
138
+ type NeuralVoiceAvailability = "available" | "unavailable";
139
+ interface NeuralVoiceEngine {
140
+ availability(): Promise<NeuralVoiceAvailability>;
141
+ prewarm(commonPhrases: readonly string[]): Promise<void>;
142
+ synthesize(text: string, signal: AbortSignal): Promise<string>;
143
+ cancel(): Promise<void>;
144
+ release?(filePath: string): Promise<void>;
145
+ dispose(): Promise<void>;
146
+ }
147
+ /** Cloud TTS endpoint the SDK posts announcement text to (see apps/proxy /voice/tts). */
148
+ interface VoiceTtsOptions {
149
+ readonly url: string;
150
+ /** Extra request headers, e.g. partner bearer tokens. Re-evaluated per request. */
151
+ readonly headers?: () => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
152
+ readonly voiceName?: string;
153
+ readonly languageCode?: string;
154
+ readonly requestTimeoutMs?: number;
155
+ }
156
+ type VoiceErrorCode = "engine_unavailable" | "network_unavailable" | "synthesis_failed" | "storage_failure" | "playback_failed";
157
+ declare class VoiceError extends Error {
158
+ readonly code: VoiceErrorCode;
159
+ readonly name = "VoiceError";
160
+ constructor(code: VoiceErrorCode, message: string, options?: {
161
+ cause?: unknown;
162
+ });
163
+ }
164
+ interface MatchVoiceService {
165
+ readonly audioState: VoiceRouteState;
166
+ readonly lastError?: Error | null;
167
+ initialize(): Promise<void>;
168
+ subscribe(listener: () => void): () => void;
169
+ announce(previous: MatchSnapshot, current: MatchSnapshot, event: MatchEvent): void;
170
+ replayAnnouncement(): Promise<void>;
171
+ testSpeaker(): Promise<void>;
172
+ configureAudioMode?(mode: VoiceAudioMode): Promise<void>;
173
+ dispose(): Promise<void>;
174
+ }
175
+
176
+ export { type AnnouncementPlan as A, type CourtSide as C, type FallbackVoiceToken as F, type GameSnapshot as G, type MatchSnapshot as M, type NeuralVoiceEngine as N, type PendingCourtSwitch as P, type RallyAwardedEvent as R, type ServeState as S, type TeamScore as T, type VoiceAudioMode as V, type MatchEvent as a, type AudioPlayback as b, type VoiceRouteState as c, type VoiceTtsOptions as d, type MatchVoiceService as e, type AnnouncementKind as f, type NeuralVoiceAvailability as g, type VoiceAudioRoute as h, type VoiceDisconnectReason as i, VoiceError as j, type VoiceErrorCode as k, type MatchInputSource as l, type CourtSwitchConfirmedEvent as m, type MatchCorrection as n, type MatchCorrectedEvent as o, type MatchCreateInput as p, type MatchCreation as q, type MatchEndedEvent as r, type MatchEventUndoneEvent as s, type MatchCreatedEvent as t, type MatchFormat as u, type MatchPlayer as v, type MatchScoringMode as w, type MatchTeam as x, type MatchTeamId as y };