ugcinc 4.6.3 → 4.7.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/README.md CHANGED
@@ -58,7 +58,7 @@ All client methods return the same response envelope:
58
58
 
59
59
  ```typescript
60
60
  type ApiResponse<T> =
61
- | { ok: true; code: 200; message: string; data: T }
61
+ | { ok: true; code: 200; message: string; data: T; nextCursor?: string | null }
62
62
  | { ok: false; code: number; message: string };
63
63
  ```
64
64
 
@@ -74,6 +74,25 @@ if (posts.ok) {
74
74
  }
75
75
  ```
76
76
 
77
+ ## Pagination
78
+
79
+ `accounts.getAccounts()` and `posts.getPosts()` support keyset pagination via `limit`/`cursor`.
80
+ Omit `limit` to fetch everything matching your filters (the default, unpaginated behavior); pass
81
+ `limit` to page through results newest-first, using each response's `nextCursor` to fetch the next
82
+ page (`null`/absent means there are no more pages):
83
+
84
+ ```typescript
85
+ let cursor: string | undefined;
86
+ const allPosts = [];
87
+
88
+ do {
89
+ const res = await client.posts.getPosts({ limit: 100, cursor });
90
+ if (!res.ok) break;
91
+ allPosts.push(...res.data);
92
+ cursor = res.nextCursor ?? undefined;
93
+ } while (cursor);
94
+ ```
95
+
77
96
  ## Useful Exports
78
97
 
79
98
  - `UGCClient` for API access
@@ -74,6 +74,10 @@ export interface GetAccountsParams {
74
74
  org_group?: string;
75
75
  user_group?: string;
76
76
  status?: 'uninitialized' | 'pending' | 'initialized' | 'setup' | 'warming' | 'warmed' | 'needs_replacement' | 'replacing' | 'failed' | 'deleted' | 'reclaimed';
77
+ /** Max rows to return. Omit to fetch all matching accounts (no pagination). */
78
+ limit?: number;
79
+ /** Opaque cursor from a previous response's `nextCursor`, to fetch the next page. */
80
+ cursor?: string;
77
81
  }
78
82
  export interface GetAccountStatsParams {
79
83
  accountIds?: string[];
@@ -239,6 +243,9 @@ export interface TroubleshootParams {
239
243
  export declare class AccountsClient extends BaseClient {
240
244
  /**
241
245
  * Get accounts with optional filters
246
+ *
247
+ * Pass `limit` to paginate; the response's `nextCursor` (when present) can be
248
+ * passed back as `cursor` to fetch the next page, ordered newest-first.
242
249
  */
243
250
  getAccounts(params?: GetAccountsParams): Promise<ApiResponse<Account[]>>;
244
251
  /**
package/dist/accounts.js CHANGED
@@ -8,6 +8,9 @@ const base_1 = require("./base");
8
8
  class AccountsClient extends base_1.BaseClient {
9
9
  /**
10
10
  * Get accounts with optional filters
11
+ *
12
+ * Pass `limit` to paginate; the response's `nextCursor` (when present) can be
13
+ * passed back as `cursor` to fetch the next page, ordered newest-first.
11
14
  */
12
15
  async getAccounts(params) {
13
16
  return this.post('/accounts', params ?? {});
package/dist/base.js CHANGED
@@ -44,6 +44,7 @@ class BaseClient {
44
44
  code: rawData.code,
45
45
  message: rawData.message ?? 'Success',
46
46
  data: rawData.data,
47
+ ...(rawData.nextCursor !== undefined ? { nextCursor: rawData.nextCursor } : {}),
47
48
  };
48
49
  }
49
50
  else {
package/dist/posts.d.ts CHANGED
@@ -32,6 +32,10 @@ export interface GetPostsParams {
32
32
  startDate?: string;
33
33
  endDate?: string;
34
34
  includeHidden?: boolean;
35
+ /** Max rows to return. Omit to fetch all matching posts (no pagination). */
36
+ limit?: number;
37
+ /** Opaque cursor from a previous response's `nextCursor`, to fetch the next page. */
38
+ cursor?: string;
35
39
  }
36
40
  export interface CreateSlideshowParams {
37
41
  accountId: string | null;
@@ -131,6 +135,9 @@ export interface PreviewScheduleResult {
131
135
  export declare class PostsClient extends BaseClient {
132
136
  /**
133
137
  * Get posts with optional filters
138
+ *
139
+ * Pass `limit` to paginate; the response's `nextCursor` (when present) can be
140
+ * passed back as `cursor` to fetch the next page, ordered newest-first.
134
141
  */
135
142
  getPosts(params?: GetPostsParams): Promise<ApiResponse<Post[]>>;
136
143
  /**
package/dist/posts.js CHANGED
@@ -8,6 +8,9 @@ const base_1 = require("./base");
8
8
  class PostsClient extends base_1.BaseClient {
9
9
  /**
10
10
  * Get posts with optional filters
11
+ *
12
+ * Pass `limit` to paginate; the response's `nextCursor` (when present) can be
13
+ * passed back as `cursor` to fetch the next page, ordered newest-first.
11
14
  */
12
15
  async getPosts(params) {
13
16
  return this.post('/post', params ?? {});
@@ -12,6 +12,14 @@ const react_1 = require("react");
12
12
  const remotion_1 = require("remotion");
13
13
  const defaults_1 = require("../utils/defaults");
14
14
  const text_1 = require("../utils/text");
15
+ /**
16
+ * Preview sync threshold: remotion hard-seeks the media tag when it drifts
17
+ * further than this from the timeline (default ~0.65s). Fractional playback
18
+ * speeds accumulate drift, so the default produces large visible "rubberband"
19
+ * jumps; a tight threshold keeps corrections to a few frames. Preview-only —
20
+ * ignored during server rendering.
21
+ */
22
+ const ACCEPTABLE_TIME_SHIFT_SECONDS = 0.2;
15
23
  /**
16
24
  * Map FitMode to CSS object-fit
17
25
  */
@@ -102,7 +110,7 @@ function VideoElement({ segment, src, startFrame, durationInFrames, scale = 1 })
102
110
  const effectiveDurationMs = loopSourceDurationMs / speed;
103
111
  return Math.max(1, Math.round((effectiveDurationMs / 1000) * fps));
104
112
  }, [loop, loopSourceDurationMs, speed, fps]);
105
- const videoElement = ((0, jsx_runtime_1.jsx)(remotion_1.OffthreadVideo, { src: src, style: videoStyle, startFrom: startFromFrames, playbackRate: speed, volume: volume }));
113
+ const videoElement = ((0, jsx_runtime_1.jsx)(remotion_1.OffthreadVideo, { src: src, style: videoStyle, startFrom: startFromFrames, playbackRate: speed, volume: volume, acceptableTimeShiftInSeconds: ACCEPTABLE_TIME_SHIFT_SECONDS }));
106
114
  return ((0, jsx_runtime_1.jsx)("div", { style: containerStyle, children: loop && loopDurationInFrames ? ((0, jsx_runtime_1.jsx)(remotion_1.Loop, { durationInFrames: loopDurationInFrames, children: videoElement })) : (videoElement) }));
107
115
  }
108
116
  exports.default = VideoElement;
@@ -134,27 +134,28 @@ function calculateSegmentTimings(config, fps) {
134
134
  * ```
135
135
  */
136
136
  function VideoEditorComposition({ config, sources = {}, textContent = {}, }) {
137
- const frame = (0, remotion_1.useCurrentFrame)();
138
- const { fps, durationInFrames } = (0, remotion_1.useVideoConfig)();
137
+ const { fps } = (0, remotion_1.useVideoConfig)();
139
138
  // Calculate segment timings
140
139
  const segmentTimings = (0, react_1.useMemo)(() => calculateSegmentTimings(config, fps), [config, fps]);
141
- // Get active visual segments at current frame, sorted by zIndex
142
- const activeVisualSegments = (0, react_1.useMemo)(() => {
140
+ // All visual segments, statically sorted by zIndex (stable sort keeps the
141
+ // original channel order for ties). Every segment is always rendered inside
142
+ // a <Sequence> window, which handles per-frame visibility — mounting is no
143
+ // longer gated on the current frame, so upcoming media can premount.
144
+ const visualTimings = (0, react_1.useMemo)(() => {
143
145
  return segmentTimings
144
- .filter(({ segment, startFrame, endFrame }) => {
145
- // Only visual segments (including sequences)
146
- if (segment.type !== 'video' && segment.type !== 'image' && segment.type !== 'text' && segment.type !== 'image-sequence' && segment.type !== 'video-sequence') {
147
- return false;
148
- }
149
- // Check if active at current frame
150
- return frame >= startFrame && frame < endFrame;
146
+ .filter(({ segment }) => {
147
+ return segment.type === 'video' || segment.type === 'image' || segment.type === 'text' || segment.type === 'image-sequence' || segment.type === 'video-sequence';
151
148
  })
152
149
  .sort((a, b) => {
153
150
  const aZ = a.segment.zIndex ?? 0;
154
151
  const bZ = b.segment.zIndex ?? 0;
155
152
  return aZ - bZ;
156
153
  });
157
- }, [segmentTimings, frame]);
154
+ }, [segmentTimings]);
155
+ // Premount window for video sequences: in the Player, upcoming videos
156
+ // mount invisibly this many frames early so they buffer and pre-seek
157
+ // before becoming visible. No effect on server rendering.
158
+ const premountFrames = Math.round(fps * 2);
158
159
  // Get source URL for a segment
159
160
  const getSource = (segment) => {
160
161
  if (segment.source) {
@@ -170,25 +171,25 @@ function VideoEditorComposition({ config, sources = {}, textContent = {}, }) {
170
171
  const audioTimings = (0, react_1.useMemo)(() => {
171
172
  return segmentTimings.filter(({ segment }) => segment.type === 'audio');
172
173
  }, [segmentTimings]);
173
- return ((0, jsx_runtime_1.jsxs)(remotion_1.AbsoluteFill, { style: { backgroundColor: '#000000' }, children: [activeVisualSegments.map(({ segment, startFrame, durationInFrames }) => {
174
+ return ((0, jsx_runtime_1.jsxs)(remotion_1.AbsoluteFill, { style: { backgroundColor: '#000000' }, children: [visualTimings.map(({ segment, startFrame, durationInFrames }) => {
174
175
  if (segment.type === 'text') {
175
176
  const textSegment = segment;
176
177
  const text = getTextContent(textSegment);
177
- return ((0, jsx_runtime_1.jsx)(TextElement_1.TextElement, { segment: { ...textSegment, text }, scale: 1 }, segment.id));
178
+ return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: startFrame, durationInFrames: durationInFrames, children: (0, jsx_runtime_1.jsx)(TextElement_1.TextElement, { segment: { ...textSegment, text }, scale: 1 }) }, segment.id));
178
179
  }
179
180
  if (segment.type === 'image') {
180
181
  const src = getSource(segment);
181
182
  if (!src) {
182
183
  return null;
183
184
  }
184
- return ((0, jsx_runtime_1.jsx)(ImageElement_1.ImageElement, { segment: segment, src: src, startFrame: startFrame, scale: 1 }, segment.id));
185
+ return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: startFrame, durationInFrames: durationInFrames, children: (0, jsx_runtime_1.jsx)(ImageElement_1.ImageElement, { segment: segment, src: src, startFrame: 0, scale: 1 }) }, segment.id));
185
186
  }
186
187
  if (segment.type === 'video') {
187
188
  const src = getSource(segment);
188
189
  if (!src) {
189
190
  return null;
190
191
  }
191
- return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: startFrame, durationInFrames: durationInFrames, children: (0, jsx_runtime_1.jsx)(VideoElement_1.VideoElement, { segment: segment, src: src, startFrame: 0, durationInFrames: durationInFrames, scale: 1 }) }, segment.id));
192
+ return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: startFrame, durationInFrames: durationInFrames, premountFor: premountFrames, children: (0, jsx_runtime_1.jsx)(VideoElement_1.VideoElement, { segment: segment, src: src, startFrame: 0, durationInFrames: durationInFrames, scale: 1 }) }, segment.id));
192
193
  }
193
194
  if (segment.type === 'image-sequence') {
194
195
  const sequenceSegment = segment;
@@ -196,32 +197,29 @@ function VideoEditorComposition({ config, sources = {}, textContent = {}, }) {
196
197
  if (items.length === 0) {
197
198
  return null;
198
199
  }
199
- // Calculate which item should be shown at current frame
200
- const frameIntoSegment = frame - startFrame;
201
- const itemDurationFrames = Math.round((sequenceSegment.defaultDuration / 1000) * fps);
202
- const currentItemIndex = Math.floor(frameIntoSegment / itemDurationFrames);
203
- const currentItemUrl = items[currentItemIndex];
204
- if (currentItemIndex >= items.length || !currentItemUrl) {
205
- return null;
206
- }
207
- const itemStartFrame = startFrame + currentItemIndex * itemDurationFrames;
208
- const syntheticImageSegment = {
209
- id: `${segment.id}-item-${currentItemIndex}`,
210
- type: 'image',
211
- source: currentItemUrl,
212
- order: segment.order,
213
- offset: { type: 'absolute', value: 0 },
214
- xOffset: sequenceSegment.xOffset,
215
- yOffset: sequenceSegment.yOffset,
216
- width: sequenceSegment.width,
217
- height: sequenceSegment.height,
218
- fit: sequenceSegment.fit,
219
- rotation: sequenceSegment.rotation,
220
- opacity: sequenceSegment.opacity,
221
- zIndex: sequenceSegment.zIndex,
222
- fadeIn: sequenceSegment.fadeIn,
223
- };
224
- return ((0, jsx_runtime_1.jsx)(ImageElement_1.ImageElement, { segment: syntheticImageSegment, src: currentItemUrl, startFrame: itemStartFrame, scale: 1 }, `${segment.id}-${currentItemUrl}`));
200
+ const itemDurationFrames = Math.max(1, Math.round((sequenceSegment.defaultDuration / 1000) * fps));
201
+ return items.map((itemUrl, itemIndex) => {
202
+ if (!itemUrl) {
203
+ return null;
204
+ }
205
+ const syntheticImageSegment = {
206
+ id: `${segment.id}-item-${itemIndex}`,
207
+ type: 'image',
208
+ source: itemUrl,
209
+ order: segment.order,
210
+ offset: { type: 'absolute', value: 0 },
211
+ xOffset: sequenceSegment.xOffset,
212
+ yOffset: sequenceSegment.yOffset,
213
+ width: sequenceSegment.width,
214
+ height: sequenceSegment.height,
215
+ fit: sequenceSegment.fit,
216
+ rotation: sequenceSegment.rotation,
217
+ opacity: sequenceSegment.opacity,
218
+ zIndex: sequenceSegment.zIndex,
219
+ fadeIn: sequenceSegment.fadeIn,
220
+ };
221
+ return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: startFrame + itemIndex * itemDurationFrames, durationInFrames: itemDurationFrames, children: (0, jsx_runtime_1.jsx)(ImageElement_1.ImageElement, { segment: syntheticImageSegment, src: itemUrl, startFrame: 0, scale: 1 }) }, `${segment.id}-item-${itemIndex}`));
222
+ });
225
223
  }
226
224
  if (segment.type === 'video-sequence') {
227
225
  const sequenceSegment = segment;
@@ -230,44 +228,33 @@ function VideoEditorComposition({ config, sources = {}, textContent = {}, }) {
230
228
  if (items.length === 0) {
231
229
  return null;
232
230
  }
233
- // Calculate which item should be shown at current frame
234
- const frameIntoSegment = frame - startFrame;
235
231
  let accumulatedFrames = 0;
236
- let currentItemIndex = -1;
237
- let itemStartFrame = startFrame;
238
- for (let i = 0; i < items.length; i++) {
239
- const itemDurationMs = durations[i] ?? 5000;
240
- const itemDurationFrames = Math.round((itemDurationMs / 1000) * fps);
241
- if (frameIntoSegment < accumulatedFrames + itemDurationFrames) {
242
- currentItemIndex = i;
243
- itemStartFrame = startFrame + accumulatedFrames;
244
- break;
245
- }
232
+ return items.map((itemUrl, itemIndex) => {
233
+ const itemDurationMs = durations[itemIndex] ?? 5000;
234
+ const itemDurationFrames = Math.max(1, Math.round((itemDurationMs / 1000) * fps));
235
+ const itemStartFrame = startFrame + accumulatedFrames;
246
236
  accumulatedFrames += itemDurationFrames;
247
- }
248
- const currentItemUrl = items[currentItemIndex];
249
- if (currentItemIndex < 0 || currentItemIndex >= items.length || !currentItemUrl) {
250
- return null;
251
- }
252
- const itemDurationMs = durations[currentItemIndex] ?? 5000;
253
- const itemDurationFrames = Math.round((itemDurationMs / 1000) * fps);
254
- const syntheticVideoSegment = {
255
- id: `${segment.id}-item-${currentItemIndex}`,
256
- type: 'video',
257
- source: currentItemUrl,
258
- order: segment.order,
259
- offset: { type: 'absolute', value: 0 },
260
- xOffset: sequenceSegment.xOffset,
261
- yOffset: sequenceSegment.yOffset,
262
- width: sequenceSegment.width,
263
- height: sequenceSegment.height,
264
- fit: sequenceSegment.fit,
265
- rotation: sequenceSegment.rotation,
266
- opacity: sequenceSegment.opacity,
267
- zIndex: sequenceSegment.zIndex,
268
- fadeIn: sequenceSegment.fadeIn,
269
- };
270
- return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: itemStartFrame, durationInFrames: itemDurationFrames, children: (0, jsx_runtime_1.jsx)(VideoElement_1.VideoElement, { segment: syntheticVideoSegment, src: currentItemUrl, startFrame: 0, durationInFrames: itemDurationFrames, scale: 1 }) }, `${segment.id}-${currentItemUrl}`));
237
+ if (!itemUrl) {
238
+ return null;
239
+ }
240
+ const syntheticVideoSegment = {
241
+ id: `${segment.id}-item-${itemIndex}`,
242
+ type: 'video',
243
+ source: itemUrl,
244
+ order: segment.order,
245
+ offset: { type: 'absolute', value: 0 },
246
+ xOffset: sequenceSegment.xOffset,
247
+ yOffset: sequenceSegment.yOffset,
248
+ width: sequenceSegment.width,
249
+ height: sequenceSegment.height,
250
+ fit: sequenceSegment.fit,
251
+ rotation: sequenceSegment.rotation,
252
+ opacity: sequenceSegment.opacity,
253
+ zIndex: sequenceSegment.zIndex,
254
+ fadeIn: sequenceSegment.fadeIn,
255
+ };
256
+ return ((0, jsx_runtime_1.jsx)(remotion_1.Sequence, { from: itemStartFrame, durationInFrames: itemDurationFrames, premountFor: premountFrames, children: (0, jsx_runtime_1.jsx)(VideoElement_1.VideoElement, { segment: syntheticVideoSegment, src: itemUrl, startFrame: 0, durationInFrames: itemDurationFrames, scale: 1 }) }, `${segment.id}-item-${itemIndex}`));
257
+ });
271
258
  }
272
259
  return null;
273
260
  }), audioTimings.map(({ segment, startFrame, durationInFrames }) => {
@@ -11,6 +11,8 @@ exports.accountTools = [
11
11
  org_group: zod_1.z.string().optional().describe('Filter by org group'),
12
12
  user_group: zod_1.z.string().optional().describe('Filter by user group'),
13
13
  status: zod_1.z.enum(['uninitialized', 'pending', 'initialized', 'setup', 'warming', 'warmed', 'needs_replacement', 'replacing', 'failed', 'deleted']).optional().describe('Filter by account status'),
14
+ limit: zod_1.z.number().optional().describe('Max rows to return, for pagination'),
15
+ cursor: zod_1.z.string().optional().describe('Opaque cursor from a previous response\'s nextCursor, to fetch the next page'),
14
16
  }).optional().describe('Optional filters'),
15
17
  execute: async (client, params) => {
16
18
  return client.accounts.getAccounts(params ?? undefined);
@@ -11,6 +11,8 @@ exports.postTools = [
11
11
  postIds: zod_1.z.array(zod_1.z.string()).optional().describe('Filter by specific post IDs'),
12
12
  startDate: zod_1.z.string().optional().describe('Start date (ISO 8601)'),
13
13
  endDate: zod_1.z.string().optional().describe('End date (ISO 8601)'),
14
+ limit: zod_1.z.number().optional().describe('Max rows to return, for pagination'),
15
+ cursor: zod_1.z.string().optional().describe('Opaque cursor from a previous response\'s nextCursor, to fetch the next page'),
14
16
  }).optional(),
15
17
  execute: async (client, params) => {
16
18
  return client.posts.getPosts(params ?? undefined);
package/dist/types.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface SuccessResponse<T> {
6
6
  message: string;
7
7
  data: T;
8
8
  ok: true;
9
+ /** Present on keyset-paginated endpoints (see `limit`/`cursor` params). Pass back as `cursor` to fetch the next page; `null`/absent means no more pages. */
10
+ nextCursor?: string | null;
9
11
  }
10
12
  export interface ErrorResponse {
11
13
  code: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc",
3
- "version": "4.6.3",
3
+ "version": "4.7.1",
4
4
  "description": "TypeScript/JavaScript client for the UGC Inc API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",