@pickleball/server-sdk 0.1.1 → 0.3.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.
- package/CHANGELOG.md +30 -0
- package/README.md +22 -0
- package/dist/{chunk-HBTNLURN.js → chunk-UPS4VFER.js} +188 -21
- package/dist/index.cjs +189 -21
- package/dist/index.d.cts +168 -3
- package/dist/index.d.ts +168 -3
- package/dist/index.js +3 -1
- package/dist/proxy.cjs +321 -23
- package/dist/proxy.d.cts +11 -1
- package/dist/proxy.d.ts +11 -1
- package/dist/proxy.js +133 -3
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
PickleballInvalidResponseError: () => PickleballInvalidResponseError,
|
|
27
27
|
PickleballLiveError: () => PickleballLiveError,
|
|
28
28
|
PickleballNetworkError: () => PickleballNetworkError,
|
|
29
|
+
PickleballPartnerApiError: () => PickleballPartnerApiError,
|
|
29
30
|
PickleballTimeoutError: () => PickleballTimeoutError,
|
|
30
31
|
PickleballWebhookVerificationError: () => PickleballWebhookVerificationError,
|
|
31
32
|
SDK_TELEMETRY_EVENT_NAMES: () => SDK_TELEMETRY_EVENT_NAMES,
|
|
@@ -55,6 +56,15 @@ var PickleballApiError = class extends PickleballLiveError {
|
|
|
55
56
|
this.name = "PickleballApiError";
|
|
56
57
|
}
|
|
57
58
|
};
|
|
59
|
+
var PickleballPartnerApiError = class extends PickleballLiveError {
|
|
60
|
+
constructor(message, partnerCode, status, details = {}) {
|
|
61
|
+
super(message, "PARTNER_API_ERROR");
|
|
62
|
+
this.partnerCode = partnerCode;
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.details = details;
|
|
65
|
+
this.name = "PickleballPartnerApiError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
58
68
|
var PickleballHttpError = class extends PickleballLiveError {
|
|
59
69
|
constructor(status) {
|
|
60
70
|
super(`Request failed with HTTP ${status}`, "HTTP_ERROR");
|
|
@@ -283,6 +293,49 @@ function validateGrant(value) {
|
|
|
283
293
|
config: validateRemoteConfig(data.config)
|
|
284
294
|
};
|
|
285
295
|
}
|
|
296
|
+
function validateCameraGrant(value) {
|
|
297
|
+
const data = record(value);
|
|
298
|
+
const base = validateGrant(data);
|
|
299
|
+
if (!/^rtmps?:\/\//i.test(base.serverUrl)) invalid();
|
|
300
|
+
if (data.status !== "scheduled" && data.status !== "live") invalid();
|
|
301
|
+
return {
|
|
302
|
+
...base,
|
|
303
|
+
status: data.status,
|
|
304
|
+
matchRef: nullableString(data.matchRef),
|
|
305
|
+
courtRef: nullableString(data.courtRef),
|
|
306
|
+
watchUrl: nullableString(data.watchUrl),
|
|
307
|
+
convexUrl: nullableString(data.convexUrl),
|
|
308
|
+
issuedAt: finite(data.issuedAt),
|
|
309
|
+
expiresAt: finite(data.expiresAt)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function validateCameraSessionCreated(value) {
|
|
313
|
+
const data = record(value);
|
|
314
|
+
if (data.status !== "scheduled" && data.status !== "live" && data.status !== "ended") {
|
|
315
|
+
invalid();
|
|
316
|
+
}
|
|
317
|
+
if (data.visibility !== "public" && data.visibility !== "private") invalid();
|
|
318
|
+
if (typeof data.reused !== "boolean") invalid();
|
|
319
|
+
return {
|
|
320
|
+
id: nonblank(data.id),
|
|
321
|
+
status: data.status,
|
|
322
|
+
visibility: data.visibility,
|
|
323
|
+
quality: finite(data.quality),
|
|
324
|
+
deliveryMode: nonblank(data.deliveryMode),
|
|
325
|
+
matchRef: nullableString(data.matchRef),
|
|
326
|
+
courtRef: nullableString(data.courtRef),
|
|
327
|
+
playbackUrl: nullableString(data.playbackUrl),
|
|
328
|
+
whepUrl: nullableString(data.whepUrl),
|
|
329
|
+
watchUrl: nullableString(data.watchUrl),
|
|
330
|
+
reused: data.reused,
|
|
331
|
+
cameraGrant: validateCameraGrant(data.cameraGrant)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function validateLegacyEnd(value) {
|
|
335
|
+
const data = record(value);
|
|
336
|
+
if (data.ok !== true || data.status !== "ended") invalid();
|
|
337
|
+
return { status: "ended" };
|
|
338
|
+
}
|
|
286
339
|
function validateRefresh(value) {
|
|
287
340
|
const data = record(value);
|
|
288
341
|
return {
|
|
@@ -320,6 +373,7 @@ function validateSession(value) {
|
|
|
320
373
|
matchRef: nullableString(data.matchRef),
|
|
321
374
|
playbackUrl: nullableString(data.playbackUrl),
|
|
322
375
|
watchUrl: nullableString(data.watchUrl),
|
|
376
|
+
whepUrl: nullableString(data.whepUrl),
|
|
323
377
|
scheduledAt: nullableNumber(data.scheduledAt),
|
|
324
378
|
startedAt: nullableNumber(data.startedAt),
|
|
325
379
|
endedAt: nullableNumber(data.endedAt),
|
|
@@ -350,6 +404,29 @@ function validateRecordings(value, sessionId) {
|
|
|
350
404
|
if (!Array.isArray(value)) invalid();
|
|
351
405
|
return value.map((item) => validateRecording(item, sessionId));
|
|
352
406
|
}
|
|
407
|
+
function validateDeviceRegister(value) {
|
|
408
|
+
const data = record(value);
|
|
409
|
+
const heartbeatIntervalMs = finite(data.heartbeatIntervalMs);
|
|
410
|
+
if (heartbeatIntervalMs <= 0) invalid();
|
|
411
|
+
return {
|
|
412
|
+
deviceId: nonblank(data.deviceId),
|
|
413
|
+
heartbeatIntervalMs
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function validateDeviceHeartbeat(value) {
|
|
417
|
+
const data = record(value);
|
|
418
|
+
if (data.controlSession === null || data.controlSession === void 0) {
|
|
419
|
+
return { controlSession: null };
|
|
420
|
+
}
|
|
421
|
+
const grant = record(data.controlSession);
|
|
422
|
+
return {
|
|
423
|
+
controlSession: {
|
|
424
|
+
id: nonblank(grant.id),
|
|
425
|
+
token: nonblank(grant.token),
|
|
426
|
+
convexUrl: nonblank(grant.convexUrl)
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
353
430
|
|
|
354
431
|
// src/contracts.ts
|
|
355
432
|
var SDK_TELEMETRY_EVENT_NAMES = [
|
|
@@ -495,7 +572,23 @@ async function readBoundedResponseText(response) {
|
|
|
495
572
|
}
|
|
496
573
|
return new TextDecoder().decode(bytes);
|
|
497
574
|
}
|
|
498
|
-
|
|
575
|
+
function parsePartnerError(payload, status, apiKey) {
|
|
576
|
+
if (!isRecord(payload) || !("error" in payload)) {
|
|
577
|
+
return new PickleballInvalidResponseError();
|
|
578
|
+
}
|
|
579
|
+
if (isRecord(payload.error)) {
|
|
580
|
+
return parseSdkError(payload, status, apiKey);
|
|
581
|
+
}
|
|
582
|
+
if (typeof payload.error !== "string") return new PickleballInvalidResponseError();
|
|
583
|
+
const { error, code, ...details } = payload;
|
|
584
|
+
const safeMessage = apiKey === "" ? error : error.split(apiKey).join("[REDACTED]");
|
|
585
|
+
const partnerCode = typeof code === "string" && code.trim() !== "" ? code : `HTTP_${status}`;
|
|
586
|
+
return new PickleballPartnerApiError(safeMessage, partnerCode, status, details);
|
|
587
|
+
}
|
|
588
|
+
async function decodeResponse(response, apiKey, validate, options = {
|
|
589
|
+
errorStyle: "sdk",
|
|
590
|
+
envelope: "data"
|
|
591
|
+
}) {
|
|
499
592
|
if (response.status >= 300 && response.status < 400) {
|
|
500
593
|
await response.body?.cancel().catch(() => void 0);
|
|
501
594
|
throw new PickleballHttpError(response.status);
|
|
@@ -507,7 +600,10 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
507
600
|
} catch {
|
|
508
601
|
throw new PickleballInvalidResponseError();
|
|
509
602
|
}
|
|
510
|
-
if (!response.ok)
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
throw options.errorStyle === "v1" ? parsePartnerError(payload, response.status, apiKey) : parseSdkError(payload, response.status, apiKey);
|
|
605
|
+
}
|
|
606
|
+
if (options.envelope === "raw") return validate(payload);
|
|
511
607
|
if (!isRecord(payload) || !("data" in payload)) {
|
|
512
608
|
throw new PickleballInvalidResponseError();
|
|
513
609
|
}
|
|
@@ -515,7 +611,15 @@ async function decodeResponse(response, apiKey, validate) {
|
|
|
515
611
|
}
|
|
516
612
|
function createPickleballLiveClient(options) {
|
|
517
613
|
const baseUrl = validatedBaseUrl(options.baseUrl);
|
|
518
|
-
const
|
|
614
|
+
const configuredAppId = options.appId === void 0 ? null : requiredNonblank(options.appId, "appId");
|
|
615
|
+
const requireAppId = () => {
|
|
616
|
+
if (configuredAppId === null) {
|
|
617
|
+
throw new PickleballConfigurationError(
|
|
618
|
+
"appId is required for /api/v2/sdk operations (apps/sessions/devices)"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return configuredAppId;
|
|
622
|
+
};
|
|
519
623
|
const apiKey = requiredNonblank(options.apiKey, "apiKey");
|
|
520
624
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
521
625
|
if (typeof fetchImplementation !== "function") {
|
|
@@ -528,7 +632,9 @@ function createPickleballLiveClient(options) {
|
|
|
528
632
|
path,
|
|
529
633
|
body,
|
|
530
634
|
retryPolicy,
|
|
531
|
-
validate
|
|
635
|
+
validate,
|
|
636
|
+
errorStyle = "sdk",
|
|
637
|
+
envelope = "data"
|
|
532
638
|
}) {
|
|
533
639
|
const retryLimit = retryPolicy === "transient" ? maxRetries : 0;
|
|
534
640
|
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
@@ -549,14 +655,14 @@ function createPickleballLiveClient(options) {
|
|
|
549
655
|
} : {
|
|
550
656
|
accept: "application/json",
|
|
551
657
|
"x-api-key": apiKey,
|
|
552
|
-
"x-pickleball-app-id":
|
|
658
|
+
...configuredAppId === null ? {} : { "x-pickleball-app-id": configuredAppId }
|
|
553
659
|
},
|
|
554
660
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
555
661
|
signal: controller.signal,
|
|
556
662
|
redirect: "error"
|
|
557
663
|
});
|
|
558
664
|
try {
|
|
559
|
-
return await decodeResponse(response, apiKey, validate);
|
|
665
|
+
return await decodeResponse(response, apiKey, validate, { errorStyle, envelope });
|
|
560
666
|
} catch (error) {
|
|
561
667
|
if (isTransientStatus(response.status) && attempt < retryLimit && isRetryableResponseError(error)) {
|
|
562
668
|
clearTimeout(timer);
|
|
@@ -568,7 +674,7 @@ function createPickleballLiveClient(options) {
|
|
|
568
674
|
throw error;
|
|
569
675
|
}
|
|
570
676
|
} catch (error) {
|
|
571
|
-
if (error instanceof PickleballApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
677
|
+
if (error instanceof PickleballApiError || error instanceof PickleballPartnerApiError || error instanceof PickleballHttpError || error instanceof PickleballInvalidResponseError) {
|
|
572
678
|
throw error;
|
|
573
679
|
}
|
|
574
680
|
if (attempt < retryLimit) {
|
|
@@ -587,54 +693,115 @@ function createPickleballLiveClient(options) {
|
|
|
587
693
|
const post = (path, body, retryPolicy, validate) => request({ method: "POST", path, body, retryPolicy, validate });
|
|
588
694
|
const get = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate });
|
|
589
695
|
const sessionPath = (sessionId) => `/api/v2/sdk/sessions/${encodeURIComponent(sessionId)}`;
|
|
696
|
+
const v1SessionPath = (sessionId) => `/api/v1/live-sessions/${encodeURIComponent(sessionId)}`;
|
|
697
|
+
const postV1 = (path, body, retryPolicy, validate, envelope = "data") => request({ method: "POST", path, body, retryPolicy, validate, errorStyle: "v1", envelope });
|
|
698
|
+
const getV1 = (path, validate) => request({ method: "GET", path, retryPolicy: "transient", validate, errorStyle: "v1" });
|
|
590
699
|
return {
|
|
591
700
|
apps: {
|
|
592
|
-
bootstrap: (input) => post(
|
|
701
|
+
bootstrap: async (input) => post(
|
|
593
702
|
"/api/v2/sdk/bootstrap",
|
|
594
|
-
{ ...input, appId },
|
|
703
|
+
{ ...input, appId: requireAppId() },
|
|
595
704
|
"transient",
|
|
596
705
|
validateBootstrap
|
|
597
706
|
)
|
|
598
707
|
},
|
|
599
708
|
sessions: {
|
|
600
|
-
start: (input) => post(
|
|
709
|
+
start: async (input) => post(
|
|
601
710
|
"/api/v2/sdk/sessions",
|
|
602
|
-
{ ...input, appId },
|
|
711
|
+
{ ...input, appId: requireAppId() },
|
|
603
712
|
"transient",
|
|
604
713
|
validateGrant
|
|
605
714
|
),
|
|
606
|
-
refresh: (sessionId, input) => post(
|
|
715
|
+
refresh: async (sessionId, input) => post(
|
|
607
716
|
`${sessionPath(sessionId)}/refresh`,
|
|
608
|
-
{ ...input, appId },
|
|
717
|
+
{ ...input, appId: requireAppId() },
|
|
609
718
|
"never",
|
|
610
719
|
validateRefresh
|
|
611
720
|
),
|
|
612
721
|
end: async (input) => {
|
|
613
722
|
await post(
|
|
614
723
|
`${sessionPath(input.sessionId)}/end`,
|
|
615
|
-
{ ...input, appId },
|
|
724
|
+
{ ...input, appId: requireAppId() },
|
|
616
725
|
"transient",
|
|
617
726
|
(value) => validateEnd(value, input.sessionId)
|
|
618
727
|
);
|
|
619
728
|
},
|
|
620
|
-
publish: (sessionId) => post(
|
|
729
|
+
publish: async (sessionId) => post(
|
|
621
730
|
`${sessionPath(sessionId)}/publish`,
|
|
622
|
-
{ sessionId, appId },
|
|
731
|
+
{ sessionId, appId: requireAppId() },
|
|
623
732
|
"transient",
|
|
624
733
|
(value) => validatePublishState(value, sessionId)
|
|
625
734
|
),
|
|
626
|
-
unpublish: (sessionId) => post(
|
|
735
|
+
unpublish: async (sessionId) => post(
|
|
627
736
|
`${sessionPath(sessionId)}/unpublish`,
|
|
628
|
-
{ sessionId, appId },
|
|
737
|
+
{ sessionId, appId: requireAppId() },
|
|
629
738
|
"transient",
|
|
630
739
|
(value) => validatePublishState(value, sessionId)
|
|
631
740
|
),
|
|
632
|
-
get: (sessionId) =>
|
|
633
|
-
|
|
634
|
-
|
|
741
|
+
get: async (sessionId) => {
|
|
742
|
+
requireAppId();
|
|
743
|
+
return get(sessionPath(sessionId), validateSession);
|
|
744
|
+
},
|
|
745
|
+
getRecordings: async (sessionId) => {
|
|
746
|
+
requireAppId();
|
|
747
|
+
return get(
|
|
748
|
+
`${sessionPath(sessionId)}/recordings`,
|
|
749
|
+
(value) => validateRecordings(value, sessionId)
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
liveSessions: {
|
|
754
|
+
createCameraSession: (input) => postV1(
|
|
755
|
+
"/api/v1/live-sessions",
|
|
756
|
+
{
|
|
757
|
+
title: input.title,
|
|
758
|
+
matchRef: input.matchRef,
|
|
759
|
+
cameraGrant: true,
|
|
760
|
+
deliveryMode: "device-rtmp",
|
|
761
|
+
...input.visibility === void 0 ? {} : { visibility: input.visibility },
|
|
762
|
+
...input.description === void 0 ? {} : { description: input.description },
|
|
763
|
+
...input.quality === void 0 ? {} : { quality: input.quality },
|
|
764
|
+
...input.courtRef === void 0 ? {} : { courtRef: input.courtRef }
|
|
765
|
+
},
|
|
766
|
+
// Idempotent theo (host, matchRef): gọi lại chỉ cấp grant mới, an toàn để retry.
|
|
767
|
+
"transient",
|
|
768
|
+
validateCameraSessionCreated
|
|
769
|
+
),
|
|
770
|
+
cameraGrant: (sessionId) => postV1(
|
|
771
|
+
`${v1SessionPath(sessionId)}/camera-grant`,
|
|
772
|
+
{},
|
|
773
|
+
"transient",
|
|
774
|
+
validateCameraGrant
|
|
775
|
+
),
|
|
776
|
+
end: (sessionId) => postV1(
|
|
777
|
+
`${v1SessionPath(sessionId)}/end`,
|
|
778
|
+
{},
|
|
779
|
+
"transient",
|
|
780
|
+
validateLegacyEnd,
|
|
781
|
+
"raw"
|
|
782
|
+
),
|
|
783
|
+
get: (sessionId) => getV1(v1SessionPath(sessionId), validateSession),
|
|
784
|
+
getRecordings: (sessionId) => getV1(
|
|
785
|
+
`${v1SessionPath(sessionId)}/recordings`,
|
|
635
786
|
(value) => validateRecordings(value, sessionId)
|
|
636
787
|
)
|
|
637
788
|
},
|
|
789
|
+
devices: {
|
|
790
|
+
register: async (input) => post(
|
|
791
|
+
"/api/v2/sdk/devices/register",
|
|
792
|
+
{ ...input, appId: requireAppId() },
|
|
793
|
+
"transient",
|
|
794
|
+
validateDeviceRegister
|
|
795
|
+
),
|
|
796
|
+
heartbeat: async (input) => post(
|
|
797
|
+
"/api/v2/sdk/devices/heartbeat",
|
|
798
|
+
{ ...input, appId: requireAppId() },
|
|
799
|
+
// Heartbeat có side effect trao token 1 lần — không retry để tránh
|
|
800
|
+
// nhận token đã bị đánh dấu delivered ở lần trước
|
|
801
|
+
"never",
|
|
802
|
+
validateDeviceHeartbeat
|
|
803
|
+
)
|
|
804
|
+
},
|
|
638
805
|
webhooks: { verify: verifyWebhook }
|
|
639
806
|
};
|
|
640
807
|
}
|
|
@@ -646,6 +813,7 @@ function createPickleballLiveClient(options) {
|
|
|
646
813
|
PickleballInvalidResponseError,
|
|
647
814
|
PickleballLiveError,
|
|
648
815
|
PickleballNetworkError,
|
|
816
|
+
PickleballPartnerApiError,
|
|
649
817
|
PickleballTimeoutError,
|
|
650
818
|
PickleballWebhookVerificationError,
|
|
651
819
|
SDK_TELEMETRY_EVENT_NAMES,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type SdkErrorCode = "PERMISSION_DENIED" | "NETWORK_UNAVAILABLE" | "TOKEN_EXPIRED" | "SDK_UPGRADE_REQUIRED" | "SESSION_CONFLICT" | "RECORDING_FAILED" | "CONSENT_REQUIRED" | "SDK_DISABLED" | "UNKNOWN";
|
|
2
|
-
type ServerSdkErrorCode = SdkErrorCode | "CONFIGURATION_ERROR" | "TIMEOUT" | "NETWORK_ERROR" | "HTTP_ERROR" | "INVALID_RESPONSE" | "WEBHOOK_VERIFICATION_FAILED";
|
|
2
|
+
type ServerSdkErrorCode = SdkErrorCode | "CONFIGURATION_ERROR" | "TIMEOUT" | "NETWORK_ERROR" | "HTTP_ERROR" | "INVALID_RESPONSE" | "PARTNER_API_ERROR" | "WEBHOOK_VERIFICATION_FAILED";
|
|
3
3
|
declare class PickleballLiveError extends Error {
|
|
4
4
|
readonly code: ServerSdkErrorCode;
|
|
5
5
|
constructor(message: string, code: ServerSdkErrorCode, options?: ErrorOptions);
|
|
@@ -13,6 +13,19 @@ declare class PickleballApiError extends PickleballLiveError {
|
|
|
13
13
|
/** Stable allowlisted code. Unknown server codes are normalized to UNKNOWN. */
|
|
14
14
|
code: SdkErrorCode, status: number);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Lỗi của REST v1 (đường đối tác: /api/v1/live-sessions, camera grant). Thân
|
|
18
|
+
* lỗi là `{error, code, ...extra}` — `partnerCode` là mã máy đọc (VD
|
|
19
|
+
* COURT_HAS_DEVICE, CAPACITY, SESSION_EXPIRED), `details` là các field kèm
|
|
20
|
+
* (activeSessionId, deviceId, retryAfterMs, retryable…). Đối tác switch theo
|
|
21
|
+
* `partnerCode`, KHÔNG parse message.
|
|
22
|
+
*/
|
|
23
|
+
declare class PickleballPartnerApiError extends PickleballLiveError {
|
|
24
|
+
readonly partnerCode: string;
|
|
25
|
+
readonly status: number;
|
|
26
|
+
readonly details: Record<string, unknown>;
|
|
27
|
+
constructor(message: string, partnerCode: string, status: number, details?: Record<string, unknown>);
|
|
28
|
+
}
|
|
16
29
|
declare class PickleballHttpError extends PickleballLiveError {
|
|
17
30
|
readonly status: number;
|
|
18
31
|
constructor(status: number);
|
|
@@ -66,6 +79,13 @@ interface StartLivestreamInput {
|
|
|
66
79
|
* "scheduled" và KHÔNG khởi động recording egress cho tới khi publish.
|
|
67
80
|
*/
|
|
68
81
|
standby?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Độ phân giải app THỰC SỰ quay, để server ghi đúng vào phiên. Chỉ app biết
|
|
84
|
+
* con số này: nó là lựa chọn local và app còn override `config.videoQuality`
|
|
85
|
+
* mà server trả về. Không khai thì server dùng mặc định của nó (1080) và bản
|
|
86
|
+
* ghi phiên nói dối.
|
|
87
|
+
*/
|
|
88
|
+
quality?: 720 | 1080;
|
|
69
89
|
}
|
|
70
90
|
interface SdkTelemetryCredentials {
|
|
71
91
|
endpoint: string;
|
|
@@ -96,6 +116,64 @@ interface SdkPublishState {
|
|
|
96
116
|
sessionId: string;
|
|
97
117
|
status: "scheduled" | "live";
|
|
98
118
|
}
|
|
119
|
+
type CameraGrantStatus = "scheduled" | "live";
|
|
120
|
+
/**
|
|
121
|
+
* Camera grant — điện thoại của user ĐỐI TÁC lên sóng (ADR-0002). Backend đối
|
|
122
|
+
* tác nhận từ REST v1 (POST /api/v1/live-sessions {cameraGrant:true} hoặc
|
|
123
|
+
* POST /api/v1/live-sessions/{id}/camera-grant) và chuyển NGUYÊN TRẠNG cho app;
|
|
124
|
+
* app dùng `telemetry.token` làm Bearer với namespace /api/v2/camera/sessions.
|
|
125
|
+
*
|
|
126
|
+
* Superset nghiêm ngặt của SdkSessionGrant: `serverUrl` là RTMP ingest
|
|
127
|
+
* (`rtmp://host:1935/`), `participantToken` là stream key — SDK 0.2 đã nhận
|
|
128
|
+
* hình dạng này qua isSessionGrant nên không cần validator mới ở lớp engine.
|
|
129
|
+
*/
|
|
130
|
+
interface CameraGrant extends SdkSessionGrant {
|
|
131
|
+
/** Không bao giờ "ended" — endpoint trả 409 thay vì grant. */
|
|
132
|
+
status: CameraGrantStatus;
|
|
133
|
+
matchRef: string | null;
|
|
134
|
+
courtRef: string | null;
|
|
135
|
+
watchUrl: string | null;
|
|
136
|
+
/**
|
|
137
|
+
* Origin websocket Convex (https://<dep>.convex.cloud) để SDK subscribe lệnh
|
|
138
|
+
* thay vì poll HTTP 2s; null khi deployment không lộ origin chuẩn → SDK poll.
|
|
139
|
+
*/
|
|
140
|
+
convexUrl: string | null;
|
|
141
|
+
issuedAt: number;
|
|
142
|
+
/**
|
|
143
|
+
* Hạn dùng của grant. Phiên chờ mà app CHƯA từng kết nối bị dọn sau 30 phút
|
|
144
|
+
* (RTMP_STANDBY_SILENT_MS) — xin grant đúng lúc user mở camera, đừng cấp sớm
|
|
145
|
+
* hàng giờ trước trận.
|
|
146
|
+
*/
|
|
147
|
+
expiresAt: number;
|
|
148
|
+
}
|
|
149
|
+
/** Thân POST /api/v1/live-sessions với cameraGrant:true (phiên điện thoại đối tác). */
|
|
150
|
+
interface CreateCameraSessionInput {
|
|
151
|
+
/** Định danh trận phía đối tác — phiên khoá theo trận, gọi lại cùng matchRef là idempotent. */
|
|
152
|
+
matchRef: string;
|
|
153
|
+
title: string;
|
|
154
|
+
visibility?: SessionVisibility;
|
|
155
|
+
description?: string;
|
|
156
|
+
/** Độ phân giải app sẽ quay; mặc định 720 cho điện thoại. */
|
|
157
|
+
quality?: 720 | 1080;
|
|
158
|
+
/** Đậu phiên vào sân giải để dùng match-start/match-end như camera cố định. */
|
|
159
|
+
courtRef?: string;
|
|
160
|
+
}
|
|
161
|
+
/** Phản hồi tạo phiên điện thoại: phiên + grant chuyển cho app. */
|
|
162
|
+
interface CameraSessionCreated {
|
|
163
|
+
id: string;
|
|
164
|
+
status: SessionStatus;
|
|
165
|
+
visibility: SessionVisibility;
|
|
166
|
+
quality: number;
|
|
167
|
+
deliveryMode: string;
|
|
168
|
+
matchRef: string | null;
|
|
169
|
+
courtRef: string | null;
|
|
170
|
+
playbackUrl: string | null;
|
|
171
|
+
whepUrl: string | null;
|
|
172
|
+
watchUrl: string | null;
|
|
173
|
+
/** true = phiên cũ cùng matchRef còn sống được dùng lại (grant vẫn MỚI). */
|
|
174
|
+
reused: boolean;
|
|
175
|
+
cameraGrant: CameraGrant;
|
|
176
|
+
}
|
|
99
177
|
type SessionStatus = "scheduled" | "live" | "ended";
|
|
100
178
|
type SessionVisibility = "public" | "private";
|
|
101
179
|
type RecordingType = "clean" | "derived" | "device";
|
|
@@ -108,6 +186,11 @@ interface ApiLiveSession {
|
|
|
108
186
|
playbackUrl: string | null;
|
|
109
187
|
/** Link trang xem web (`/watch/{id}`); null nếu chưa cấu hình WEB_PUBLIC_BASE_URL. */
|
|
110
188
|
watchUrl: string | null;
|
|
189
|
+
/**
|
|
190
|
+
* Endpoint WebRTC/WHEP độ trễ <1s — chỉ khác null khi phiên ĐANG live ở mode
|
|
191
|
+
* device-rtmp. Chết ngay khi phiên kết thúc, đừng lưu; null = dùng playbackUrl.
|
|
192
|
+
*/
|
|
193
|
+
whepUrl: string | null;
|
|
111
194
|
scheduledAt: number | null;
|
|
112
195
|
startedAt: number | null;
|
|
113
196
|
endedAt: number | null;
|
|
@@ -126,6 +209,46 @@ interface ApiRecording {
|
|
|
126
209
|
type StartSdkSessionInput = BootstrapInput & StartLivestreamInput & {
|
|
127
210
|
idempotencyKey: string;
|
|
128
211
|
};
|
|
212
|
+
/** Thông tin thiết bị kèm khi đăng ký (từ expo-device). */
|
|
213
|
+
interface DeviceInfoInput {
|
|
214
|
+
appVersion?: string;
|
|
215
|
+
deviceName?: string;
|
|
216
|
+
deviceModel?: string;
|
|
217
|
+
osVersion?: string;
|
|
218
|
+
}
|
|
219
|
+
interface DeviceMetricsInput {
|
|
220
|
+
ramFreeMb?: number;
|
|
221
|
+
ramTotalMb?: number;
|
|
222
|
+
storageFreeGb?: number;
|
|
223
|
+
batteryPct?: number;
|
|
224
|
+
batteryTempC?: number;
|
|
225
|
+
thermal?: number;
|
|
226
|
+
netType?: string;
|
|
227
|
+
linkMbps?: number;
|
|
228
|
+
rssi?: number;
|
|
229
|
+
connQuality?: string;
|
|
230
|
+
}
|
|
231
|
+
interface DeviceRegisterResult {
|
|
232
|
+
deviceId: string;
|
|
233
|
+
heartbeatIntervalMs: number;
|
|
234
|
+
}
|
|
235
|
+
interface DeviceHeartbeatInput {
|
|
236
|
+
/** Trạng thái engine hiện tại (idle|standby|live|...). */
|
|
237
|
+
agentState?: string;
|
|
238
|
+
/** Id phiên live đang giữ — null = không có phiên (server clear link). */
|
|
239
|
+
currentSessionId?: string | null;
|
|
240
|
+
appVersion?: string;
|
|
241
|
+
metrics?: DeviceMetricsInput;
|
|
242
|
+
}
|
|
243
|
+
/** Kênh điều khiển được dashboard yêu cầu — app dùng token mở Convex websocket. */
|
|
244
|
+
interface DeviceControlSessionGrant {
|
|
245
|
+
id: string;
|
|
246
|
+
token: string;
|
|
247
|
+
convexUrl: string;
|
|
248
|
+
}
|
|
249
|
+
interface DeviceHeartbeatResult {
|
|
250
|
+
controlSession: DeviceControlSessionGrant | null;
|
|
251
|
+
}
|
|
129
252
|
declare const SDK_TELEMETRY_EVENT_NAMES: readonly ["state_changed", "connection_quality", "reconnect", "error", "heartbeat", "session_ended"];
|
|
130
253
|
type SdkTelemetryEventName = (typeof SDK_TELEMETRY_EVENT_NAMES)[number];
|
|
131
254
|
interface SdkTelemetryEvent {
|
|
@@ -149,9 +272,20 @@ interface LivestreamSessionProvider {
|
|
|
149
272
|
unpublish?(sessionId: string): Promise<void>;
|
|
150
273
|
}
|
|
151
274
|
type LivestreamState = "idle" | "preparing" | "preview" | "connecting" | "standby" | "live" | "reconnecting" | "ending" | "ended" | "error";
|
|
275
|
+
/**
|
|
276
|
+
* Vì sao 409 SESSION_CONFLICT bị nhận trên namespace /api/v2/camera — mirror
|
|
277
|
+
* của packages/shared (type-test Equal ghim): superseded = grant bị grant mới
|
|
278
|
+
* thay (app dừng tại chỗ), ended = phiên đã kết thúc, expired = quá trần
|
|
279
|
+
* thời lượng credential (xin grant/phiên mới).
|
|
280
|
+
*/
|
|
281
|
+
type SdkConflictReason = "superseded" | "ended" | "expired";
|
|
152
282
|
interface SdkError {
|
|
153
283
|
code: SdkErrorCode;
|
|
154
284
|
message: string;
|
|
285
|
+
/** Chỉ có ở 409 SESSION_CONFLICT của namespace /api/v2/camera. */
|
|
286
|
+
reason?: SdkConflictReason;
|
|
287
|
+
/** Chỉ có ở 429 — cùng số với header `Retry-After` (giây, làm tròn lên). */
|
|
288
|
+
retryAfterMs?: number;
|
|
155
289
|
}
|
|
156
290
|
|
|
157
291
|
type WebhookRawBody = string | Uint8Array;
|
|
@@ -178,7 +312,11 @@ declare function verifyWebhook<T extends WebhookPayload = WebhookPayload>(rawBod
|
|
|
178
312
|
|
|
179
313
|
interface PickleballLiveClientOptions {
|
|
180
314
|
baseUrl: string;
|
|
181
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Chỉ cần cho các thao tác `/api/v2/sdk/*` (mô hình proxy cũ). Đường
|
|
317
|
+
* `liveSessions.*` (camera grant, REST v1) không dùng appId — bỏ trống được.
|
|
318
|
+
*/
|
|
319
|
+
appId?: string;
|
|
182
320
|
apiKey: string;
|
|
183
321
|
fetch?: typeof globalThis.fetch;
|
|
184
322
|
timeoutMs?: number;
|
|
@@ -186,21 +324,48 @@ interface PickleballLiveClientOptions {
|
|
|
186
324
|
}
|
|
187
325
|
interface PickleballLiveClient {
|
|
188
326
|
apps: {
|
|
327
|
+
/** @deprecated Mô hình proxy SDK v2 — dùng `liveSessions.createCameraSession` (ADR-0002). */
|
|
189
328
|
bootstrap(input: BootstrapInput): Promise<SdkBootstrap>;
|
|
190
329
|
};
|
|
191
330
|
sessions: {
|
|
331
|
+
/** @deprecated Mô hình proxy SDK v2 — dùng `liveSessions.createCameraSession` (ADR-0002). */
|
|
192
332
|
start(input: StartSdkSessionInput): Promise<SdkSessionGrant>;
|
|
333
|
+
/** @deprecated Mô hình proxy SDK v2 — app tự refresh bằng Bearer grant ở /api/v2/camera. */
|
|
193
334
|
refresh(sessionId: string, input: BootstrapInput): Promise<SdkSessionRefresh>;
|
|
335
|
+
/** @deprecated Mô hình proxy SDK v2 — dùng `liveSessions.end`. */
|
|
194
336
|
end(input: EndLivestreamInput): Promise<void>;
|
|
337
|
+
/** @deprecated Mô hình proxy SDK v2 — app tự publish bằng Bearer grant ở /api/v2/camera. */
|
|
195
338
|
publish(sessionId: string): Promise<SdkPublishState>;
|
|
339
|
+
/** @deprecated Mô hình proxy SDK v2 — app tự unpublish bằng Bearer grant ở /api/v2/camera. */
|
|
196
340
|
unpublish(sessionId: string): Promise<SdkPublishState>;
|
|
197
341
|
get(sessionId: string): Promise<ApiLiveSession>;
|
|
198
342
|
getRecordings(sessionId: string): Promise<ApiRecording[]>;
|
|
199
343
|
};
|
|
344
|
+
/**
|
|
345
|
+
* REST v1 — đường của backend đối tác (x-api-key), không cần appId.
|
|
346
|
+
* Camera grant (ADR-0002): tạo phiên theo matchRef rồi chuyển `cameraGrant`
|
|
347
|
+
* nguyên trạng cho app; app dùng @pickleball/expo-sdk nói thẳng với Picklive.
|
|
348
|
+
*/
|
|
349
|
+
liveSessions: {
|
|
350
|
+
/** POST /api/v1/live-sessions {cameraGrant:true} — idempotent theo (host, matchRef). */
|
|
351
|
+
createCameraSession(input: CreateCameraSessionInput): Promise<CameraSessionCreated>;
|
|
352
|
+
/** POST /api/v1/live-sessions/{id}/camera-grant — cấp lại grant, thu hồi grant cũ. */
|
|
353
|
+
cameraGrant(sessionId: string): Promise<CameraGrant>;
|
|
354
|
+
/** POST /api/v1/live-sessions/{id}/end — kết thúc phiên từ backend đối tác. */
|
|
355
|
+
end(sessionId: string): Promise<{
|
|
356
|
+
status: "ended";
|
|
357
|
+
}>;
|
|
358
|
+
get(sessionId: string): Promise<ApiLiveSession>;
|
|
359
|
+
getRecordings(sessionId: string): Promise<ApiRecording[]>;
|
|
360
|
+
};
|
|
361
|
+
devices: {
|
|
362
|
+
register(input: BootstrapInput & DeviceInfoInput): Promise<DeviceRegisterResult>;
|
|
363
|
+
heartbeat(input: BootstrapInput & DeviceHeartbeatInput): Promise<DeviceHeartbeatResult>;
|
|
364
|
+
};
|
|
200
365
|
webhooks: {
|
|
201
366
|
verify<T extends WebhookPayload = WebhookPayload>(rawBody: WebhookRawBody, headers: WebhookHeaders, secret: string, options?: VerifyWebhookOptions): Promise<VerifiedWebhook<T>>;
|
|
202
367
|
};
|
|
203
368
|
}
|
|
204
369
|
declare function createPickleballLiveClient(options: PickleballLiveClientOptions): PickleballLiveClient;
|
|
205
370
|
|
|
206
|
-
export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
|
|
371
|
+
export { type ApiLiveSession, type ApiRecording, type BootstrapInput, type CameraGrant, type CameraGrantStatus, type CameraSessionCreated, type CreateCameraSessionInput, type DeviceControlSessionGrant, type DeviceHeartbeatInput, type DeviceHeartbeatResult, type DeviceInfoInput, type DeviceMetricsInput, type DeviceRegisterResult, type EndLivestreamInput, type LivestreamSessionProvider, type LivestreamState, PickleballApiError, PickleballConfigurationError, PickleballHttpError, PickleballInvalidResponseError, type PickleballLiveClient, type PickleballLiveClientOptions, PickleballLiveError, PickleballNetworkError, PickleballPartnerApiError, PickleballTimeoutError, PickleballWebhookVerificationError, type RecordingType, SDK_TELEMETRY_EVENT_NAMES, type SdkBootstrap, type SdkConflictReason, type SdkError, type SdkErrorCode, type SdkPublishState, type SdkRemoteConfig, type SdkSessionGrant, type SdkSessionRefresh, type SdkTelemetryCredentials, type SdkTelemetryEvent, type SdkTelemetryEventName, type ServerSdkErrorCode, type SessionStatus, type SessionVisibility, type StartLivestreamInput, type StartSdkSessionInput, type VerifiedWebhook, type VerifyWebhookOptions, type WebhookHeaders, type WebhookPayload, type WebhookRawBody, createPickleballLiveClient, verifyWebhook };
|