@rixl/media-core 0.0.0-stage → 1.0.1
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/LICENSE.md +1 -0
- package/README.md +118 -2
- package/dist/api/analytics.d.ts +16 -0
- package/dist/api/analytics.js +227 -0
- package/dist/api/feed.js +52 -0
- package/dist/api/heatmap.d.ts +6 -0
- package/dist/api/heatmap.js +28 -0
- package/dist/api/hot-segments.d.ts +7 -0
- package/dist/api/hot-segments.js +20 -0
- package/dist/api/image.js +11 -0
- package/dist/api/rixl-client.d.ts +3 -0
- package/dist/api/rixl-client.js +14 -0
- package/dist/api/upload.d.ts +76 -0
- package/dist/api/upload.js +150 -0
- package/dist/api/video.js +11 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +73 -0
- package/dist/lib/analytics-builders.d.ts +59 -0
- package/dist/lib/analytics-builders.js +88 -0
- package/dist/lib/analytics-initialize.d.ts +1 -0
- package/dist/lib/analytics-initialize.js +59 -0
- package/dist/lib/analytics-utils.d.ts +43 -0
- package/dist/lib/analytics-utils.js +85 -0
- package/dist/lib/analytics-wire.js +153 -0
- package/dist/lib/buffer-progress.d.ts +20 -0
- package/dist/lib/buffer-progress.js +37 -0
- package/dist/lib/caption-styles.d.ts +20 -0
- package/dist/lib/caption-styles.js +56 -0
- package/dist/lib/caption-utils.d.ts +80 -0
- package/dist/lib/caption-utils.js +111 -0
- package/dist/lib/chapter-utils.d.ts +102 -0
- package/dist/lib/chapter-utils.js +147 -0
- package/dist/lib/click-disambiguation.d.ts +44 -0
- package/dist/lib/click-disambiguation.js +80 -0
- package/dist/lib/click-outside.d.ts +68 -0
- package/dist/lib/click-outside.js +64 -0
- package/dist/lib/colors.d.ts +6 -0
- package/dist/lib/colors.js +16 -0
- package/dist/lib/content-analytics.d.ts +27 -0
- package/dist/lib/content-analytics.js +38 -0
- package/dist/lib/environment-effects.d.ts +89 -0
- package/dist/lib/environment-effects.js +139 -0
- package/dist/lib/feed-autoscroll.d.ts +10 -0
- package/dist/lib/feed-autoscroll.js +13 -0
- package/dist/lib/feed-container-height.d.ts +7 -0
- package/dist/lib/feed-container-height.js +11 -0
- package/dist/lib/feed-description-overflow.d.ts +1 -0
- package/dist/lib/feed-description-overflow.js +9 -0
- package/dist/lib/feed-dwell.d.ts +2 -0
- package/dist/lib/feed-dwell.js +8 -0
- package/dist/lib/feed-loop.d.ts +10 -0
- package/dist/lib/feed-loop.js +21 -0
- package/dist/lib/feed-scroll.d.ts +11 -0
- package/dist/lib/feed-scroll.js +19 -0
- package/dist/lib/feed-transforms.d.ts +5 -0
- package/dist/lib/feed-transforms.js +30 -0
- package/dist/lib/filter-undefined.js +11 -0
- package/dist/lib/fullscreen-utils.d.ts +1 -0
- package/dist/lib/fullscreen-utils.js +7 -0
- package/dist/lib/hls-buffer-flush.js +46 -0
- package/dist/lib/hls-error-recovery.d.ts +35 -0
- package/dist/lib/hls-error-recovery.js +75 -0
- package/dist/lib/hls-event-handlers.js +61 -0
- package/dist/lib/hls-loader.js +23 -0
- package/dist/lib/hls-setup.d.ts +72 -0
- package/dist/lib/hls-setup.js +144 -0
- package/dist/lib/hls-utils.d.ts +31 -0
- package/dist/lib/hls-utils.js +34 -0
- package/dist/lib/image-render.d.ts +29 -0
- package/dist/lib/image-render.js +63 -0
- package/dist/lib/keyboard-config.d.ts +66 -0
- package/dist/lib/keyboard-config.js +127 -0
- package/dist/lib/keyboard-utils.d.ts +4 -0
- package/dist/lib/keyboard-utils.js +11 -0
- package/dist/lib/lifecycle-effects.d.ts +67 -0
- package/dist/lib/lifecycle-effects.js +78 -0
- package/dist/lib/loading-debounce.d.ts +26 -0
- package/dist/lib/loading-debounce.js +38 -0
- package/dist/lib/numbers.d.ts +6 -0
- package/dist/lib/numbers.js +36 -0
- package/dist/lib/pip-utils.d.ts +16 -0
- package/dist/lib/pip-utils.js +32 -0
- package/dist/lib/playback-handlers.d.ts +33 -0
- package/dist/lib/playback-handlers.js +51 -0
- package/dist/lib/playback-slider.d.ts +23 -0
- package/dist/lib/playback-slider.js +24 -0
- package/dist/lib/player-labels.d.ts +23 -0
- package/dist/lib/player-labels.js +132 -0
- package/dist/lib/player-menu-labels.d.ts +80 -0
- package/dist/lib/player-menu-labels.js +829 -0
- package/dist/lib/player-ui-actions.d.ts +10 -0
- package/dist/lib/player-ui-actions.js +58 -0
- package/dist/lib/progress-fraction.d.ts +11 -0
- package/dist/lib/progress-fraction.js +20 -0
- package/dist/lib/progress-persist.d.ts +28 -0
- package/dist/lib/progress-persist.js +54 -0
- package/dist/lib/segment-analytics.d.ts +20 -0
- package/dist/lib/segment-analytics.js +29 -0
- package/dist/lib/subtitle-logic.d.ts +31 -0
- package/dist/lib/subtitle-logic.js +75 -0
- package/dist/lib/subtitle-utils.d.ts +16 -0
- package/dist/lib/subtitle-utils.js +37 -0
- package/dist/lib/time.d.ts +1 -0
- package/dist/lib/time.js +17 -0
- package/dist/lib/track-persistence.d.ts +4 -0
- package/dist/lib/track-persistence.js +35 -0
- package/dist/lib/video-preload-config.d.ts +34 -0
- package/dist/lib/video-preload-config.js +41 -0
- package/dist/lib/video-spinner.d.ts +25 -0
- package/dist/lib/video-spinner.js +35 -0
- package/dist/lib/video-stats.d.ts +28 -0
- package/dist/lib/video-stats.js +44 -0
- package/dist/lib/volume-utils.d.ts +59 -0
- package/dist/lib/volume-utils.js +55 -0
- package/dist/progressbar.d.ts +10 -0
- package/dist/progressbar.js +123 -0
- package/dist/store/analytics-context-store.d.ts +4 -0
- package/dist/store/analytics-context-store.js +13 -0
- package/dist/store/analytics-device-store.d.ts +8 -0
- package/dist/store/analytics-device-store.js +44 -0
- package/dist/store/analytics-session-store.d.ts +8 -0
- package/dist/store/analytics-session-store.js +40 -0
- package/dist/store/analytics-store.d.ts +30 -0
- package/dist/store/analytics-store.js +88 -0
- package/dist/store/controls-store.d.ts +16 -0
- package/dist/store/controls-store.js +53 -0
- package/dist/store/feed-cache-store.d.ts +24 -0
- package/dist/store/feed-cache-store.js +165 -0
- package/dist/store/feed-interaction-store.d.ts +17 -0
- package/dist/store/feed-interaction-store.js +32 -0
- package/dist/store/feed-store.d.ts +16 -0
- package/dist/store/feed-store.js +38 -0
- package/dist/store/generic-store.js +24 -0
- package/dist/store/image-store.d.ts +2 -0
- package/dist/store/image-store.js +8 -0
- package/dist/store/player-container-element-store.d.ts +10 -0
- package/dist/store/player-container-element-store.js +38 -0
- package/dist/store/player-global-actions.js +53 -0
- package/dist/store/player-media-element-store.d.ts +10 -0
- package/dist/store/player-media-element-store.js +38 -0
- package/dist/store/player-store-actions.js +197 -0
- package/dist/store/player-store.d.ts +94 -0
- package/dist/store/player-store.js +127 -0
- package/dist/store/player-track-actions.js +44 -0
- package/dist/store/postStore.d.ts +20 -0
- package/dist/store/postStore.js +112 -0
- package/dist/store/settings/constants.d.ts +15 -0
- package/dist/store/settings/constants.js +47 -0
- package/dist/store/settings/defaults.d.ts +2 -0
- package/dist/store/settings/defaults.js +24 -0
- package/dist/store/settings/storage.d.ts +3 -0
- package/dist/store/settings/storage.js +28 -0
- package/dist/store/settings/store.d.ts +32 -0
- package/dist/store/settings/store.js +59 -0
- package/dist/store/settings/types.d.ts +27 -0
- package/dist/store/settings/validation.d.ts +7 -0
- package/dist/store/settings/validation.js +55 -0
- package/dist/store/settings-menu-store.d.ts +19 -0
- package/dist/store/settings-menu-store.js +30 -0
- package/dist/store/video-store.d.ts +2 -0
- package/dist/store/video-store.js +8 -0
- package/dist/types/analytics.d.ts +113 -0
- package/dist/types/feed.d.ts +21 -0
- package/dist/types/file.d.ts +12 -0
- package/dist/types/heatmap.d.ts +5 -0
- package/dist/types/hot-segments.d.ts +9 -0
- package/dist/types/image.d.ts +9 -0
- package/dist/types/uuid.d.ts +1 -0
- package/dist/types/video.d.ts +22 -0
- package/package.json +41 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Use of this SDK is subject to our [Terms of Use](https://rixl.com/en/mediasdk-terms/).
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RIXL Media SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [RIXL Media SDK](https://rixl.com/en/docs/mediasdk) enables you to build custom media experiences with RIXL's core technology through a highly optimized TypeScript library.
|
|
4
|
+
|
|
5
|
+
The library provides a framework-agnostic foundation for media playback experiences: fetching, caching, player state stores, analytics, and pure helpers for progress, buffering, chapters, captions, and more. It powers the UI components in `@rixl/media-react`, `@rixl/media-vue`, `@rixl/media-svelte`, `@rixl/media-angular`, `@rixl/media-astro`, and `@rixl/media-lit`, and can be used directly from any JavaScript/TypeScript project (vanilla, custom web components, Node.js).
|
|
6
|
+
|
|
7
|
+
Use of this SDK is subject to our [Terms of Use](https://rixl.com/en/mediasdk-terms/).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
For headless usage, install the core package. For UI components, install only your framework package (`@rixl/media-react`, `@rixl/media-vue`, `@rixl/media-svelte`, `@rixl/media-angular`, `@rixl/media-astro`, or `@rixl/media-lit`); it installs core automatically and exposes the same APIs through its `/core` subpath.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @rixl/media-core
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @rixl/media-core
|
|
17
|
+
# or
|
|
18
|
+
bun add @rixl/media-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Media Fetching
|
|
22
|
+
|
|
23
|
+
| Function | Description |
|
|
24
|
+
| --------------------------------------- | ----------------------------------- |
|
|
25
|
+
| `getImage(imageId)` | Fetches image data by ID |
|
|
26
|
+
| `getVideo(videoId)` | Fetches video data by ID |
|
|
27
|
+
| `getFeedPost(feedId, postId)` | Fetches a single post from a feed |
|
|
28
|
+
| `getFeedPosts(feedId, offset?, limit?)` | Fetches paginated posts from a feed |
|
|
29
|
+
|
|
30
|
+
### Usage:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Fetch image data
|
|
34
|
+
const imageData = await getImage("image-id");
|
|
35
|
+
|
|
36
|
+
// Fetch video data
|
|
37
|
+
const videoData = await getVideo("video-id");
|
|
38
|
+
|
|
39
|
+
// Fetch a single post from a feed
|
|
40
|
+
const post = await getFeedPost("feed-id", "post-id");
|
|
41
|
+
|
|
42
|
+
// Fetch paginated posts from a feed
|
|
43
|
+
const posts = await getFeedPosts("feed-id", 0, 10);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### TypeScript Types
|
|
47
|
+
|
|
48
|
+
The library exports the following TypeScript types:
|
|
49
|
+
|
|
50
|
+
| Type | Description |
|
|
51
|
+
| ------------------- | ---------------------------------------------- |
|
|
52
|
+
| `ImageData` | Image metadata and URLs |
|
|
53
|
+
| `VideoData` | Video metadata, URLs, and playback information |
|
|
54
|
+
| `FeedPost` | Feed post content and metadata |
|
|
55
|
+
| `FeedPostsResponse` | Paginated response for feed posts |
|
|
56
|
+
| `FileStatus` | Status of a media file |
|
|
57
|
+
| `FileData` | Generic file metadata |
|
|
58
|
+
|
|
59
|
+
## Player State
|
|
60
|
+
|
|
61
|
+
Reactive [nanostores](https://github.com/nanostores/nanostores) for media playback state. All stores are framework-agnostic; consume them in React via `@nanostores/react`, in Vue via `@nanostores/vue`, etc.
|
|
62
|
+
|
|
63
|
+
| Store | Description |
|
|
64
|
+
| ----------------------------- | ----------------------------------------------------------------------- |
|
|
65
|
+
| `playerStateStore` | Per-player playback state (mute, volume, rate, tracks, fullscreen, PiP) |
|
|
66
|
+
| `controlsStore` | UI controls visibility/state |
|
|
67
|
+
| `settingsStore` | User settings (caption styling, language preferences, playback speed) |
|
|
68
|
+
| `postStore` | Active post/content metadata |
|
|
69
|
+
| `analyticsStore` | Watched-segments tracking for analytics payloads |
|
|
70
|
+
| `feedStore` | Feed interaction state (transitions, initialization) |
|
|
71
|
+
| `feedCacheStore` | Cached feed pages |
|
|
72
|
+
| `playerContainerElementStore` | Reference to the active player container element |
|
|
73
|
+
| `playerMediaElementStore` | Reference to the active media element |
|
|
74
|
+
|
|
75
|
+
## Progress Bar & Player UI Actions
|
|
76
|
+
|
|
77
|
+
| Function | Description |
|
|
78
|
+
| ----------------------------------------------- | --------------------------------------------------- |
|
|
79
|
+
| `initProgressBar({video, ...})` | Wires drag/seek interactions to a video element |
|
|
80
|
+
| `showPlayerControls()` / `hidePlayerControls()` | Toggle controls visibility |
|
|
81
|
+
| `togglePlayerFullscreen()` | Enter/exit fullscreen for the active player |
|
|
82
|
+
| `togglePlayerPictureInPicture()` | Enter/exit picture-in-picture for the active player |
|
|
83
|
+
|
|
84
|
+
## Pure Helpers
|
|
85
|
+
|
|
86
|
+
Selected utilities (fully tree-shakeable):
|
|
87
|
+
|
|
88
|
+
- **Progress / buffer:** `computeProgressFraction`, `computeBufferRanges`, `computeBufferedAhead`
|
|
89
|
+
- **Chapters & heatmap:** `calculateChaptersWithEnd`, `calculateHeatmapSlices`, `sliceHeatmapData`, `validateChapters`
|
|
90
|
+
- **Subtitles & captions:** `extractSubtitleTracks`, `processCues`, `findNextCue`, `calculateFontSize`, `constrainPercentagePosition`
|
|
91
|
+
- **Volume / fullscreen / PiP:** `calculateVolumeToSet`, `shouldBeMuted`, `isEffectivelyMuted`, `isDocumentFullscreen`, `togglePictureInPicture`
|
|
92
|
+
- **Persistence:** `getPersistedProgress`, `persistProgress`, `getPersistedAudioTrack`, `getPersistedSubtitleTrack`, `loadSettings`, `saveSettings`
|
|
93
|
+
- **HLS / quality:** `sortLevelsByQuality`, `generateLevelName`, `getQualityLabel`, `isHlsStream`
|
|
94
|
+
- **Analytics:** `postContentEvent`, `buildVideoAnalyticsPayload`, `buildContentViewEvent`, `calculateWatchDuration`
|
|
95
|
+
- **Formatting:** `formatTime`, `formatNumber`, `formatNumberSm`, `clamp`, `hexToRGB`, `hexToRGBAString`
|
|
96
|
+
|
|
97
|
+
## API Reference
|
|
98
|
+
|
|
99
|
+
All data is fetched from `https://api.rixl.com`.
|
|
100
|
+
|
|
101
|
+
### REST Endpoints
|
|
102
|
+
|
|
103
|
+
| Endpoint | Description |
|
|
104
|
+
| --------------------------------------------------------------- | --------------------------------------------- |
|
|
105
|
+
| `GET /images/{imageId}` | Fetch image metadata and URLs |
|
|
106
|
+
| `GET /videos/{videoId}` | Fetch video metadata and playback information |
|
|
107
|
+
| `GET /feeds/{feedId}/posts?offset=...&limit=...` | Fetch paginated feed posts |
|
|
108
|
+
| `GET /feeds/{feedId}/posts/{postId}` | Fetch a specific feed post |
|
|
109
|
+
| `GET /feeds/{feedId}/creators/{creatorId}?offset=...&limit=...` | Fetch posts by a specific creator |
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
For complete documentation, tutorials, and examples, visit our **[SDK Documentation](https://docs.rixl.com/en/sdk/getting-started)**.
|
|
114
|
+
|
|
115
|
+
## Need Help?
|
|
116
|
+
|
|
117
|
+
- [Developer Support](https://support.rixl.com)
|
|
118
|
+
- [Developer Forum](https://forum.rixl.com)
|
|
119
|
+
- [Premier Developer Support](https://rixl.com/pricing) (priority support)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BuildEngagementParams, BuildErrorParams, BuildInteractionParams } from "../lib/analytics-builders.js";
|
|
2
|
+
import { AnalyticsEvent } from "../types/analytics.js";
|
|
3
|
+
export declare function postAnalytics(events: AnalyticsEvent[]): Promise<void>;
|
|
4
|
+
export declare function postEngagement(params: BuildEngagementParams): void;
|
|
5
|
+
export declare function postInteraction(params: BuildInteractionParams): void;
|
|
6
|
+
export declare function postError(params: BuildErrorParams): void;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves once the events outstanding at call time have been sent, rather than
|
|
9
|
+
* once a flush has merely been started. Two paths used to return early: a call
|
|
10
|
+
* during an active flush reported a drain that had not happened, and a call
|
|
11
|
+
* while the only outstanding batch was already in flight (so the queue looked
|
|
12
|
+
* empty) returned before that request finished. Delegating to flushQueue makes
|
|
13
|
+
* it wait for whichever drain covers the outstanding work — starting one now,
|
|
14
|
+
* bypassing the timer, when nothing is running yet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function flushAnalytics(): Promise<void>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { analyticsContextStore } from "../store/analytics-context-store.js";
|
|
2
|
+
import { getOrCreateDeviceId } from "../store/analytics-device-store.js";
|
|
3
|
+
import { getOrCreateSessionId, hasSessionStarted, markSessionStarted } from "../store/analytics-session-store.js";
|
|
4
|
+
import { buildEngagementEvent, buildErrorEvent, buildInteractionEvent, buildSessionStartEvent } from "../lib/analytics-builders.js";
|
|
5
|
+
import { filterUndefined } from "../lib/filter-undefined.js";
|
|
6
|
+
import { toWireEvent } from "../lib/analytics-wire.js";
|
|
7
|
+
import { analyticsV1EventsServiceTrackEvents } from "@rixl/sdk";
|
|
8
|
+
|
|
9
|
+
// src/api/analytics.ts
|
|
10
|
+
const MAX_RETRIES = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Statuses worth sending again. 5xx and these are transient by definition;
|
|
13
|
+
* every other 4xx means the endpoint understood the payload and rejected it,
|
|
14
|
+
* so repeating it only wastes attempts and holds up the queue behind a batch
|
|
15
|
+
* that can never drain.
|
|
16
|
+
*/
|
|
17
|
+
const SERVER_ERROR_MIN_STATUS = 500;
|
|
18
|
+
const RETRYABLE_CLIENT_STATUSES = /* @__PURE__ */ new Set([
|
|
19
|
+
408,
|
|
20
|
+
425,
|
|
21
|
+
429
|
|
22
|
+
]);
|
|
23
|
+
function isRetryableStatus(status) {
|
|
24
|
+
return status >= SERVER_ERROR_MIN_STATUS || RETRYABLE_CLIENT_STATUSES.has(status);
|
|
25
|
+
}
|
|
26
|
+
function deliveryError(message, retryable) {
|
|
27
|
+
return Object.assign(new Error(message), { retryable });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Anything that is not a classified delivery failure — a thrown network error,
|
|
31
|
+
* an aborted request — has no response behind it and is assumed transient, so
|
|
32
|
+
* events are kept rather than discarded on an unknown fault.
|
|
33
|
+
*/
|
|
34
|
+
function isRetryableError(error) {
|
|
35
|
+
return error instanceof Error && "retryable" in error ? error.retryable : true;
|
|
36
|
+
}
|
|
37
|
+
let queue = [];
|
|
38
|
+
let isFlushing = false;
|
|
39
|
+
let flushTimer = null;
|
|
40
|
+
/**
|
|
41
|
+
* The drain in progress, or null when none is running. flushAnalytics awaits
|
|
42
|
+
* it so an explicit flush resolves only once the outstanding request finishes,
|
|
43
|
+
* not the moment the queue looks empty because its batch is already in flight.
|
|
44
|
+
*/
|
|
45
|
+
let flushing = null;
|
|
46
|
+
/**
|
|
47
|
+
* Callers waiting on the events currently queued but not yet handed to a
|
|
48
|
+
* request. One deferred covers the whole queue: a batch is taken and its
|
|
49
|
+
* waiters settled together, and anything enqueued afterwards starts a new one.
|
|
50
|
+
*/
|
|
51
|
+
let pending = null;
|
|
52
|
+
function createDeferred() {
|
|
53
|
+
let resolve = () => {};
|
|
54
|
+
return {
|
|
55
|
+
promise: new Promise((res) => {
|
|
56
|
+
resolve = res;
|
|
57
|
+
}),
|
|
58
|
+
resolve
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Settles and clears the current deferred, if any. */
|
|
62
|
+
function settlePending() {
|
|
63
|
+
const waiting = pending;
|
|
64
|
+
pending = null;
|
|
65
|
+
waiting?.resolve();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* No timer is armed while a flush is running. The running flush drains what
|
|
69
|
+
* arrives during its own request, so arming one here would let it fire mid
|
|
70
|
+
* request, find `isFlushing` set, and return having abandoned its waiters.
|
|
71
|
+
*/
|
|
72
|
+
function scheduleFlush() {
|
|
73
|
+
pending ??= createDeferred();
|
|
74
|
+
if (flushTimer === null && !isFlushing) flushTimer = setTimeout(() => {
|
|
75
|
+
flushTimer = null;
|
|
76
|
+
flushQueue();
|
|
77
|
+
}, 0);
|
|
78
|
+
return pending.promise;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Starts a drain unless one is already running, and hands back a promise that
|
|
82
|
+
* settles when the whole cycle finishes — the request in flight included, not
|
|
83
|
+
* just the events still waiting in the queue. Nudge callers `void` it; only
|
|
84
|
+
* flushAnalytics awaits it, so an explicit flush cannot resolve early while a
|
|
85
|
+
* request it should cover is still outstanding.
|
|
86
|
+
*/
|
|
87
|
+
function flushQueue() {
|
|
88
|
+
if (isFlushing) return flushing ?? Promise.resolve();
|
|
89
|
+
if (queue.length === 0) return Promise.resolve();
|
|
90
|
+
const run = drainQueue().finally(() => {
|
|
91
|
+
if (flushing === run) flushing = null;
|
|
92
|
+
});
|
|
93
|
+
flushing = run;
|
|
94
|
+
return run;
|
|
95
|
+
}
|
|
96
|
+
async function drainQueue() {
|
|
97
|
+
isFlushing = true;
|
|
98
|
+
if (flushTimer) {
|
|
99
|
+
clearTimeout(flushTimer);
|
|
100
|
+
flushTimer = null;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
while (queue.length > 0) {
|
|
104
|
+
const batch = queue;
|
|
105
|
+
queue = [];
|
|
106
|
+
const waiters = pending;
|
|
107
|
+
pending = null;
|
|
108
|
+
try {
|
|
109
|
+
await performFlush(batch);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
handleFailedBatch(batch, error);
|
|
112
|
+
waiters?.resolve();
|
|
113
|
+
settlePending();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
waiters?.resolve();
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
isFlushing = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Transient failures keep their events for a later flush; a rejected payload is
|
|
124
|
+
* dropped, since requeueing it would block everything behind a batch the
|
|
125
|
+
* endpoint will refuse again.
|
|
126
|
+
*/
|
|
127
|
+
function handleFailedBatch(batch, error) {
|
|
128
|
+
console.error("[Analytics] Failed to flush queue:", error);
|
|
129
|
+
if (isRetryableError(error)) queue.unshift(...batch);
|
|
130
|
+
}
|
|
131
|
+
async function performFlush(events) {
|
|
132
|
+
const options = {
|
|
133
|
+
context: analyticsContextStore.get(),
|
|
134
|
+
deviceId: getOrCreateDeviceId(),
|
|
135
|
+
sessionId: getOrCreateSessionId()
|
|
136
|
+
};
|
|
137
|
+
const carriesSessionStart = !hasSessionStarted();
|
|
138
|
+
await sendWithRetry(buildTrackRequest(buildPayloadEvents(events, options.context, carriesSessionStart), options));
|
|
139
|
+
if (carriesSessionStart) markSessionStarted();
|
|
140
|
+
}
|
|
141
|
+
function buildPayloadEvents(events, context, withSessionStart) {
|
|
142
|
+
const payloadEvents = [];
|
|
143
|
+
if (withSessionStart) payloadEvents.push(buildSessionStartEventFromContext(context));
|
|
144
|
+
payloadEvents.push(...events);
|
|
145
|
+
return payloadEvents;
|
|
146
|
+
}
|
|
147
|
+
function buildTrackRequest(events, options) {
|
|
148
|
+
const metadata = filterUndefined({
|
|
149
|
+
user_id: options.context.user_id,
|
|
150
|
+
country: options.context.country,
|
|
151
|
+
city: options.context.city,
|
|
152
|
+
region: options.context.region,
|
|
153
|
+
device: options.context.device,
|
|
154
|
+
os: options.context.os,
|
|
155
|
+
os_version: options.context.os_version,
|
|
156
|
+
language: options.context.language,
|
|
157
|
+
browser: options.context.browser
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
events: events.map((event) => toWireEvent(event, options)),
|
|
161
|
+
...metadata
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function sendWithRetry(request) {
|
|
165
|
+
let lastError;
|
|
166
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) try {
|
|
167
|
+
await sendEvents(request);
|
|
168
|
+
return;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
lastError = error;
|
|
171
|
+
if (!isRetryableError(error)) break;
|
|
172
|
+
}
|
|
173
|
+
throw lastError;
|
|
174
|
+
}
|
|
175
|
+
async function sendEvents(request) {
|
|
176
|
+
const { response } = await analyticsV1EventsServiceTrackEvents({ body: request });
|
|
177
|
+
if (response && !response.ok) {
|
|
178
|
+
console.error(`[Analytics] Failed to post events: ${response.status} ${response.statusText}`);
|
|
179
|
+
throw deliveryError(`Analytics request failed with ${response.status}`, isRetryableStatus(response.status));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function buildSessionStartEventFromContext(context) {
|
|
183
|
+
return buildSessionStartEvent({
|
|
184
|
+
browser: context.browser,
|
|
185
|
+
language: context.language,
|
|
186
|
+
screenResolution: context.screen_resolution,
|
|
187
|
+
platform: context.platform,
|
|
188
|
+
tgPlatform: context.tg_platform,
|
|
189
|
+
tgVersion: context.tg_version,
|
|
190
|
+
timezone: context.timezone,
|
|
191
|
+
timezoneOffset: context.timezone_offset,
|
|
192
|
+
colorDepth: context.color_depth,
|
|
193
|
+
touchSupport: context.touch_support,
|
|
194
|
+
country: context.country,
|
|
195
|
+
userId: context.user_id,
|
|
196
|
+
utmSource: context.utm_source,
|
|
197
|
+
utmCampaign: context.utm_campaign
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function postAnalytics(events) {
|
|
201
|
+
if (events.length === 0) return;
|
|
202
|
+
queue.push(...events);
|
|
203
|
+
return scheduleFlush();
|
|
204
|
+
}
|
|
205
|
+
function postEngagement(params) {
|
|
206
|
+
postAnalytics([buildEngagementEvent(params)]);
|
|
207
|
+
}
|
|
208
|
+
function postInteraction(params) {
|
|
209
|
+
postAnalytics([buildInteractionEvent(params)]);
|
|
210
|
+
}
|
|
211
|
+
function postError(params) {
|
|
212
|
+
postAnalytics([buildErrorEvent(params)]);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolves once the events outstanding at call time have been sent, rather than
|
|
216
|
+
* once a flush has merely been started. Two paths used to return early: a call
|
|
217
|
+
* during an active flush reported a drain that had not happened, and a call
|
|
218
|
+
* while the only outstanding batch was already in flight (so the queue looked
|
|
219
|
+
* empty) returned before that request finished. Delegating to flushQueue makes
|
|
220
|
+
* it wait for whichever drain covers the outstanding work — starting one now,
|
|
221
|
+
* bypassing the timer, when nothing is running yet.
|
|
222
|
+
*/
|
|
223
|
+
async function flushAnalytics() {
|
|
224
|
+
return flushQueue();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export { flushAnalytics, postAnalytics, postEngagement, postError, postInteraction };
|
package/dist/api/feed.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { postsV1PostServiceGetPost3, postsV1PostServiceListPosts3 } from "@rixl/sdk";
|
|
2
|
+
|
|
3
|
+
// src/api/feed.ts
|
|
4
|
+
function toType(type) {
|
|
5
|
+
if (type === "MEDIA_TYPE_IMAGE") return "image";
|
|
6
|
+
if (type === "MEDIA_TYPE_VIDEO") return "video";
|
|
7
|
+
return "image";
|
|
8
|
+
}
|
|
9
|
+
function toFeedPost(post) {
|
|
10
|
+
const withMedia = post;
|
|
11
|
+
return {
|
|
12
|
+
id: post.id ?? "",
|
|
13
|
+
feedId: post.feed_id ?? "",
|
|
14
|
+
creatorId: post.creator_id ?? "",
|
|
15
|
+
type: toType(post.type),
|
|
16
|
+
description: post.description ?? "",
|
|
17
|
+
createdAt: post.created_at ?? "",
|
|
18
|
+
image: withMedia.image,
|
|
19
|
+
video: withMedia.video
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function toFeedPostsResponse(data, offset, limit) {
|
|
23
|
+
const rawTotal = data.total ?? 0;
|
|
24
|
+
return {
|
|
25
|
+
posts: (data.posts ?? []).map(toFeedPost),
|
|
26
|
+
total: typeof rawTotal === "number" ? rawTotal : Number(rawTotal),
|
|
27
|
+
offset: data.offset ?? offset,
|
|
28
|
+
limit: data.limit ?? limit
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const fetchFeedPosts = async (feedId, offset = 0, limit = 10) => {
|
|
32
|
+
const { data, error, response } = await postsV1PostServiceListPosts3({
|
|
33
|
+
path: { feed_id: feedId },
|
|
34
|
+
query: {
|
|
35
|
+
"pagination.offset": offset,
|
|
36
|
+
"pagination.limit": limit
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (error || !data) throw new Error(`HTTP error! status: ${response?.status ?? 500}`);
|
|
40
|
+
return toFeedPostsResponse(data, offset, limit);
|
|
41
|
+
};
|
|
42
|
+
const fetchFeedPost = async (feedId, postId) => {
|
|
43
|
+
const { data, error, response } = await postsV1PostServiceGetPost3({ path: {
|
|
44
|
+
feed_id: feedId,
|
|
45
|
+
post_id: postId
|
|
46
|
+
} });
|
|
47
|
+
if (error) throw new Error(`HTTP error! status: ${response?.status ?? 500}`);
|
|
48
|
+
if (!data?.post) throw new Error(`HTTP error! status: 404`);
|
|
49
|
+
return toFeedPost(data.post);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export { fetchFeedPost, fetchFeedPosts };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { analyticsV1HeatmapServiceGetVideoHeatmap } from "@rixl/sdk";
|
|
2
|
+
|
|
3
|
+
// src/api/heatmap.ts
|
|
4
|
+
function toNumber(value) {
|
|
5
|
+
if (value === void 0) return 0;
|
|
6
|
+
return typeof value === "number" ? value : Number(value);
|
|
7
|
+
}
|
|
8
|
+
function toDomainHeatmap(data) {
|
|
9
|
+
return {
|
|
10
|
+
video_id: data.video_id ?? "",
|
|
11
|
+
total_duration_ms: toNumber(data.total_duration_ms),
|
|
12
|
+
data: data.data ?? []
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function getVideoHeatmap({ videoId, buckets = 101 }) {
|
|
16
|
+
try {
|
|
17
|
+
const { data, error } = await analyticsV1HeatmapServiceGetVideoHeatmap({
|
|
18
|
+
path: { video_id: videoId },
|
|
19
|
+
query: { buckets }
|
|
20
|
+
});
|
|
21
|
+
if (error || !data) return null;
|
|
22
|
+
return toDomainHeatmap(data);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { getVideoHeatmap };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { HotSegment } from "../types/hot-segments.js";
|
|
2
|
+
export interface GetHotSegmentsOptions {
|
|
3
|
+
videoId: string;
|
|
4
|
+
startDate?: string;
|
|
5
|
+
endDate?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getHotSegments({ videoId, startDate, endDate }: GetHotSegmentsOptions): Promise<HotSegment[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { analyticsV1HeatmapServiceGetHotSegments } from "@rixl/sdk";
|
|
2
|
+
|
|
3
|
+
// src/api/hot-segments.ts
|
|
4
|
+
async function getHotSegments({ videoId, startDate, endDate }) {
|
|
5
|
+
try {
|
|
6
|
+
const query = {};
|
|
7
|
+
if (startDate) query["range.start"] = startDate;
|
|
8
|
+
if (endDate) query["range.end"] = endDate;
|
|
9
|
+
const { data, error } = await analyticsV1HeatmapServiceGetHotSegments({
|
|
10
|
+
path: { video_id: videoId },
|
|
11
|
+
query: Object.keys(query).length > 0 ? query : void 0
|
|
12
|
+
});
|
|
13
|
+
if (error) return [];
|
|
14
|
+
return data?.segments ?? [];
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { getHotSegments };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { imagesV1ImageServiceGetImage } from "@rixl/sdk";
|
|
2
|
+
|
|
3
|
+
// src/api/image.ts
|
|
4
|
+
const fetchImage = async (imageId) => {
|
|
5
|
+
const { data, error, response } = await imagesV1ImageServiceGetImage({ path: { image_id: imageId } });
|
|
6
|
+
if (error) throw new Error(`HTTP error! status: ${response?.status ?? 500}`);
|
|
7
|
+
if (!data?.image) throw new Error(`HTTP error! status: 404`);
|
|
8
|
+
return data.image;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { fetchImage };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { initializeAnalyticsContext } from "../lib/analytics-initialize.js";
|
|
2
|
+
import { client, connect } from "@rixl/sdk";
|
|
3
|
+
|
|
4
|
+
// src/api/rixl-client.ts
|
|
5
|
+
function getRixlClient() {
|
|
6
|
+
return client;
|
|
7
|
+
}
|
|
8
|
+
async function connect$1(config) {
|
|
9
|
+
const result = await connect(config);
|
|
10
|
+
initializeAnalyticsContext();
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { connect$1 as connect, getRixlClient };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface CreateImageUploadOptions {
|
|
2
|
+
projectId: string;
|
|
3
|
+
orgId?: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ImageUploadResult {
|
|
7
|
+
imageId: string;
|
|
8
|
+
uploadUrl: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createImageUpload(options: CreateImageUploadOptions): Promise<ImageUploadResult>;
|
|
11
|
+
export declare function uploadImage(file: Blob | Uint8Array, options: CreateImageUploadOptions): Promise<ImageUploadResult>;
|
|
12
|
+
export interface CreateVideoUploadOptions {
|
|
13
|
+
projectId: string;
|
|
14
|
+
orgId?: string;
|
|
15
|
+
name: string;
|
|
16
|
+
videoQuality?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface VideoUploadResult {
|
|
19
|
+
videoId: string;
|
|
20
|
+
posterId: string;
|
|
21
|
+
videoUploadUrl: string;
|
|
22
|
+
posterUploadUrl: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function createVideoUpload(options: CreateVideoUploadOptions): Promise<VideoUploadResult>;
|
|
25
|
+
export declare function uploadVideo(videoFile: Blob | Uint8Array, posterFile: Blob | Uint8Array, options: CreateVideoUploadOptions): Promise<VideoUploadResult>;
|
|
26
|
+
export interface AudioTrackUploadItem {
|
|
27
|
+
languageCode: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
fileName: string;
|
|
30
|
+
format: string;
|
|
31
|
+
size?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface AudioTrackUploadTarget {
|
|
34
|
+
id: string;
|
|
35
|
+
languageCode?: string;
|
|
36
|
+
uploadUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface AudioTrackUploadResult {
|
|
39
|
+
targets: AudioTrackUploadTarget[];
|
|
40
|
+
}
|
|
41
|
+
export declare function createAudioTrackUpload(projectId: string, videoId: string, items: AudioTrackUploadItem[]): Promise<AudioTrackUploadResult>;
|
|
42
|
+
export interface SubtitleUploadItem {
|
|
43
|
+
languageCode: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
fileName: string;
|
|
46
|
+
format: string;
|
|
47
|
+
size?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface SubtitleUploadTarget {
|
|
50
|
+
id: string;
|
|
51
|
+
languageCode?: string;
|
|
52
|
+
uploadUrl?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface SubtitleUploadResult {
|
|
55
|
+
targets: SubtitleUploadTarget[];
|
|
56
|
+
}
|
|
57
|
+
export declare function createSubtitleUpload(projectId: string, videoId: string, items: SubtitleUploadItem[]): Promise<SubtitleUploadResult>;
|
|
58
|
+
export interface CreatePostUploadOptions {
|
|
59
|
+
projectId: string;
|
|
60
|
+
feedId: string;
|
|
61
|
+
orgId?: string;
|
|
62
|
+
creatorId: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
contentType: string;
|
|
65
|
+
fileName: string;
|
|
66
|
+
videoQuality?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface PostUploadResult {
|
|
69
|
+
postId: string;
|
|
70
|
+
contentId: string;
|
|
71
|
+
posterId: string;
|
|
72
|
+
contentUploadUrl: string;
|
|
73
|
+
posterUploadUrl: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function uploadPost(contentFile: Blob | Uint8Array, posterFile: Blob | Uint8Array, options: CreatePostUploadOptions): Promise<PostUploadResult>;
|
|
76
|
+
export declare function createPostUpload(options: CreatePostUploadOptions): Promise<PostUploadResult>;
|