@scalemule/nextjs 0.1.2 → 0.1.3

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/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { MoneyClient } from '@scalemule/money';
4
4
  export { MoneyClient, MoneyClientConfig, createMoneyClient } from '@scalemule/money';
5
- import { RealtimeService, StorageService, PhotoService, ApiError as ApiError$1 } from '@scalemule/sdk';
5
+ import { RealtimeService, StorageService, PhotoService, VideoService, ApiError as ApiError$1 } from '@scalemule/sdk';
6
6
  import { ScaleMuleClient } from './client.mjs';
7
7
  export { ClientConfig, RequestOptions, createClient } from './client.mjs';
8
8
  import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BIIUrnPr.mjs';
@@ -19,6 +19,8 @@ interface ScaleMuleContextValue {
19
19
  storage: StorageService;
20
20
  /** Base SDK photo service — exposed for `useMedia()` and `photo.uploadViaStorage()` */
21
21
  photo: PhotoService;
22
+ /** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
23
+ video: VideoService;
22
24
  /** Current authenticated user */
23
25
  user: User | null;
24
26
  /** Set the current user */
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { MoneyClient } from '@scalemule/money';
4
4
  export { MoneyClient, MoneyClientConfig, createMoneyClient } from '@scalemule/money';
5
- import { RealtimeService, StorageService, PhotoService, ApiError as ApiError$1 } from '@scalemule/sdk';
5
+ import { RealtimeService, StorageService, PhotoService, VideoService, ApiError as ApiError$1 } from '@scalemule/sdk';
6
6
  import { ScaleMuleClient } from './client.js';
7
7
  export { ClientConfig, RequestOptions, createClient } from './client.js';
8
8
  import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BIIUrnPr.js';
@@ -19,6 +19,8 @@ interface ScaleMuleContextValue {
19
19
  storage: StorageService;
20
20
  /** Base SDK photo service — exposed for `useMedia()` and `photo.uploadViaStorage()` */
21
21
  photo: PhotoService;
22
+ /** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
23
+ video: VideoService;
22
24
  /** Current authenticated user */
23
25
  user: User | null;
24
26
  /** Set the current user */
package/dist/index.js CHANGED
@@ -3505,8 +3505,13 @@ var RealtimeService = class extends ServiceModule {
3505
3505
  var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
3506
3506
  var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
3507
3507
  var VideoService = class extends ServiceModule {
3508
- constructor() {
3509
- super(...arguments);
3508
+ /**
3509
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
3510
+ * top-level {@link ScaleMule} constructor.
3511
+ */
3512
+ constructor(client, storage) {
3513
+ super(client);
3514
+ this.storage = storage;
3510
3515
  this.basePath = "/v1/videos";
3511
3516
  }
3512
3517
  // --------------------------------------------------------------------------
@@ -3580,6 +3585,115 @@ var VideoService = class extends ServiceModule {
3580
3585
  async get(videoId, options) {
3581
3586
  return super._get(`/${videoId}`, options);
3582
3587
  }
3588
+ /**
3589
+ * Register a video from a file already uploaded to scalemule-storage.
3590
+ *
3591
+ * The synchronous handshake for storage-uploaded videos. Verifies the
3592
+ * caller's app owns the storage `file_id` and reads scan status:
3593
+ * - clean → 201, video record created with status='uploading',
3594
+ * transcode worker advances on `scan.file.completed`
3595
+ * - pending → 202, video record created with status='pending_scan',
3596
+ * worker advances when scan completes
3597
+ * - threat → 409 (rejected; row not created)
3598
+ *
3599
+ * Idempotent: calling twice with the same file_id returns the existing
3600
+ * row's data (the underlying INSERT uses ON DUPLICATE KEY UPDATE).
3601
+ *
3602
+ * For storage-backed videos `video_id == file_id` — the response's
3603
+ * `video_id` and `file_id` are identical, and either can be used with
3604
+ * `getStreamUrl()` / `get()`.
3605
+ *
3606
+ * Most callers should use {@link uploadViaStorage} which composes
3607
+ * `storage.uploadPrivate()` + `register()` in a single call.
3608
+ */
3609
+ async register(request, options) {
3610
+ return this.post(
3611
+ "/register",
3612
+ {
3613
+ file_id: request.fileId,
3614
+ sm_user_id: request.userId
3615
+ },
3616
+ options
3617
+ );
3618
+ }
3619
+ /**
3620
+ * Upload a video to storage (browser → S3 direct, private, no
3621
+ * compression) and register it with the video service so transcoding
3622
+ * + HLS streaming work.
3623
+ *
3624
+ * The canonical chat-attachment / progressive-video upload primitive.
3625
+ * Same private-direct-to-S3 path as `storage.uploadPrivate()`, plus a
3626
+ * follow-up `video.register()` call so HLS playback URLs become live
3627
+ * once the transcoder finishes.
3628
+ *
3629
+ * The returned `hls_url_promise` resolves once the video's status
3630
+ * flips to `ready` (or after a 30s timeout — caller can still serve
3631
+ * `original_view_url` as the fallback). Phase 3 of the realtime-chat
3632
+ * media pipeline ADR replaces the poll with a realtime subscription.
3633
+ *
3634
+ * If `register()` fails after a successful storage upload, the file
3635
+ * is *not* lost: the returned `file_id` is still valid as a generic
3636
+ * storage file. SDK logs a warning and resolves `hls_url_promise`
3637
+ * to `null`.
3638
+ */
3639
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
3640
+ const uploadResult = await this.storage.uploadPrivate(file, {
3641
+ filename: uploadOptions?.filename,
3642
+ metadata: uploadOptions?.metadata,
3643
+ onProgress: uploadOptions?.onProgress,
3644
+ signal: uploadOptions?.signal
3645
+ });
3646
+ if (uploadResult.error || !uploadResult.data) {
3647
+ return { data: null, error: uploadResult.error };
3648
+ }
3649
+ const fileInfo = uploadResult.data;
3650
+ const fileId = fileInfo.id;
3651
+ const originalViewUrl = fileInfo.url ?? null;
3652
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
3653
+ if (registerResult.error || !registerResult.data) {
3654
+ console.warn(
3655
+ "[scalemule-sdk] video.register() failed after storage upload; HLS variants unavailable.",
3656
+ registerResult.error
3657
+ );
3658
+ return {
3659
+ data: {
3660
+ file_id: fileId,
3661
+ video_id: fileId,
3662
+ original_view_url: originalViewUrl,
3663
+ hls_url_promise: Promise.resolve(null)
3664
+ },
3665
+ error: null
3666
+ };
3667
+ }
3668
+ const videoId = registerResult.data.video_id;
3669
+ const hlsUrlPromise = this.pollTranscodeComplete(videoId, requestOptions);
3670
+ return {
3671
+ data: {
3672
+ file_id: fileId,
3673
+ video_id: videoId,
3674
+ original_view_url: originalViewUrl,
3675
+ hls_url_promise: hlsUrlPromise
3676
+ },
3677
+ error: null
3678
+ };
3679
+ }
3680
+ /**
3681
+ * Poll {@link get} until the video's status is `ready` or the 30s
3682
+ * timeout fires. Resolves to the HLS master playlist URL on success;
3683
+ * `null` on timeout (caller should fall back to `original_view_url`).
3684
+ */
3685
+ async pollTranscodeComplete(videoId, requestOptions) {
3686
+ const intervalMs = 1e3;
3687
+ const maxAttempts = 30;
3688
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
3689
+ const result = await this.get(videoId, requestOptions);
3690
+ if (result.data?.status === "ready") {
3691
+ return `${this.client.getBaseUrl()}${this.basePath}/${videoId}/playlist.m3u8`;
3692
+ }
3693
+ await new Promise((r) => setTimeout(r, intervalMs));
3694
+ }
3695
+ return null;
3696
+ }
3583
3697
  /**
3584
3698
  * Get the HLS master playlist URL for streaming.
3585
3699
  * Returns the playlist URL that can be passed to a video player.
@@ -6310,7 +6424,7 @@ var ScaleMule = class {
6310
6424
  this.auth = new AuthService(this._client);
6311
6425
  this.storage = new StorageService(this._client);
6312
6426
  this.realtime = new RealtimeService(this._client);
6313
- this.video = new VideoService(this._client);
6427
+ this.video = new VideoService(this._client, this.storage);
6314
6428
  this.data = new DataService(this._client);
6315
6429
  this.chat = new ChatService(this._client);
6316
6430
  this.conference = new ConferenceService(this._client);
@@ -7424,6 +7538,7 @@ function ScaleMuleProvider({
7424
7538
  realtime: baseClient.realtime,
7425
7539
  storage: baseClient.storage,
7426
7540
  photo: baseClient.photo,
7541
+ video: baseClient.video,
7427
7542
  user,
7428
7543
  setUser: handleSetUser,
7429
7544
  initializing,
@@ -8737,7 +8852,7 @@ function useContent(options = {}) {
8737
8852
  );
8738
8853
  }
8739
8854
  function useMedia() {
8740
- const { storage, photo } = useScaleMule();
8855
+ const { storage, photo, video } = useScaleMule();
8741
8856
  const [uploading, setUploading] = react.useState(false);
8742
8857
  const [error, setError] = react.useState(null);
8743
8858
  const upload = react.useCallback(
@@ -8768,6 +8883,21 @@ function useMedia() {
8768
8883
  is_public: false
8769
8884
  };
8770
8885
  }
8886
+ if (mimeType.startsWith("video/") && !isPublic) {
8887
+ const r2 = await video.uploadViaStorage(file, sharedOpts);
8888
+ if (r2.error || !r2.data) {
8889
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8890
+ }
8891
+ return {
8892
+ file_id: r2.data.file_id,
8893
+ photo_id: null,
8894
+ original_view_url: r2.data.original_view_url,
8895
+ optimized_url_promise: Promise.resolve(null),
8896
+ hls_url_promise: r2.data.hls_url_promise,
8897
+ mime_type: mimeType,
8898
+ is_public: false
8899
+ };
8900
+ }
8771
8901
  if (mimeType.startsWith("image/") && isPublic) {
8772
8902
  const r2 = await storage.upload(file, {
8773
8903
  ...sharedOpts,
package/dist/index.mjs CHANGED
@@ -3504,8 +3504,13 @@ var RealtimeService = class extends ServiceModule {
3504
3504
  var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
3505
3505
  var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
3506
3506
  var VideoService = class extends ServiceModule {
3507
- constructor() {
3508
- super(...arguments);
3507
+ /**
3508
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
3509
+ * top-level {@link ScaleMule} constructor.
3510
+ */
3511
+ constructor(client, storage) {
3512
+ super(client);
3513
+ this.storage = storage;
3509
3514
  this.basePath = "/v1/videos";
3510
3515
  }
3511
3516
  // --------------------------------------------------------------------------
@@ -3579,6 +3584,115 @@ var VideoService = class extends ServiceModule {
3579
3584
  async get(videoId, options) {
3580
3585
  return super._get(`/${videoId}`, options);
3581
3586
  }
3587
+ /**
3588
+ * Register a video from a file already uploaded to scalemule-storage.
3589
+ *
3590
+ * The synchronous handshake for storage-uploaded videos. Verifies the
3591
+ * caller's app owns the storage `file_id` and reads scan status:
3592
+ * - clean → 201, video record created with status='uploading',
3593
+ * transcode worker advances on `scan.file.completed`
3594
+ * - pending → 202, video record created with status='pending_scan',
3595
+ * worker advances when scan completes
3596
+ * - threat → 409 (rejected; row not created)
3597
+ *
3598
+ * Idempotent: calling twice with the same file_id returns the existing
3599
+ * row's data (the underlying INSERT uses ON DUPLICATE KEY UPDATE).
3600
+ *
3601
+ * For storage-backed videos `video_id == file_id` — the response's
3602
+ * `video_id` and `file_id` are identical, and either can be used with
3603
+ * `getStreamUrl()` / `get()`.
3604
+ *
3605
+ * Most callers should use {@link uploadViaStorage} which composes
3606
+ * `storage.uploadPrivate()` + `register()` in a single call.
3607
+ */
3608
+ async register(request, options) {
3609
+ return this.post(
3610
+ "/register",
3611
+ {
3612
+ file_id: request.fileId,
3613
+ sm_user_id: request.userId
3614
+ },
3615
+ options
3616
+ );
3617
+ }
3618
+ /**
3619
+ * Upload a video to storage (browser → S3 direct, private, no
3620
+ * compression) and register it with the video service so transcoding
3621
+ * + HLS streaming work.
3622
+ *
3623
+ * The canonical chat-attachment / progressive-video upload primitive.
3624
+ * Same private-direct-to-S3 path as `storage.uploadPrivate()`, plus a
3625
+ * follow-up `video.register()` call so HLS playback URLs become live
3626
+ * once the transcoder finishes.
3627
+ *
3628
+ * The returned `hls_url_promise` resolves once the video's status
3629
+ * flips to `ready` (or after a 30s timeout — caller can still serve
3630
+ * `original_view_url` as the fallback). Phase 3 of the realtime-chat
3631
+ * media pipeline ADR replaces the poll with a realtime subscription.
3632
+ *
3633
+ * If `register()` fails after a successful storage upload, the file
3634
+ * is *not* lost: the returned `file_id` is still valid as a generic
3635
+ * storage file. SDK logs a warning and resolves `hls_url_promise`
3636
+ * to `null`.
3637
+ */
3638
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
3639
+ const uploadResult = await this.storage.uploadPrivate(file, {
3640
+ filename: uploadOptions?.filename,
3641
+ metadata: uploadOptions?.metadata,
3642
+ onProgress: uploadOptions?.onProgress,
3643
+ signal: uploadOptions?.signal
3644
+ });
3645
+ if (uploadResult.error || !uploadResult.data) {
3646
+ return { data: null, error: uploadResult.error };
3647
+ }
3648
+ const fileInfo = uploadResult.data;
3649
+ const fileId = fileInfo.id;
3650
+ const originalViewUrl = fileInfo.url ?? null;
3651
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
3652
+ if (registerResult.error || !registerResult.data) {
3653
+ console.warn(
3654
+ "[scalemule-sdk] video.register() failed after storage upload; HLS variants unavailable.",
3655
+ registerResult.error
3656
+ );
3657
+ return {
3658
+ data: {
3659
+ file_id: fileId,
3660
+ video_id: fileId,
3661
+ original_view_url: originalViewUrl,
3662
+ hls_url_promise: Promise.resolve(null)
3663
+ },
3664
+ error: null
3665
+ };
3666
+ }
3667
+ const videoId = registerResult.data.video_id;
3668
+ const hlsUrlPromise = this.pollTranscodeComplete(videoId, requestOptions);
3669
+ return {
3670
+ data: {
3671
+ file_id: fileId,
3672
+ video_id: videoId,
3673
+ original_view_url: originalViewUrl,
3674
+ hls_url_promise: hlsUrlPromise
3675
+ },
3676
+ error: null
3677
+ };
3678
+ }
3679
+ /**
3680
+ * Poll {@link get} until the video's status is `ready` or the 30s
3681
+ * timeout fires. Resolves to the HLS master playlist URL on success;
3682
+ * `null` on timeout (caller should fall back to `original_view_url`).
3683
+ */
3684
+ async pollTranscodeComplete(videoId, requestOptions) {
3685
+ const intervalMs = 1e3;
3686
+ const maxAttempts = 30;
3687
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
3688
+ const result = await this.get(videoId, requestOptions);
3689
+ if (result.data?.status === "ready") {
3690
+ return `${this.client.getBaseUrl()}${this.basePath}/${videoId}/playlist.m3u8`;
3691
+ }
3692
+ await new Promise((r) => setTimeout(r, intervalMs));
3693
+ }
3694
+ return null;
3695
+ }
3582
3696
  /**
3583
3697
  * Get the HLS master playlist URL for streaming.
3584
3698
  * Returns the playlist URL that can be passed to a video player.
@@ -6309,7 +6423,7 @@ var ScaleMule = class {
6309
6423
  this.auth = new AuthService(this._client);
6310
6424
  this.storage = new StorageService(this._client);
6311
6425
  this.realtime = new RealtimeService(this._client);
6312
- this.video = new VideoService(this._client);
6426
+ this.video = new VideoService(this._client, this.storage);
6313
6427
  this.data = new DataService(this._client);
6314
6428
  this.chat = new ChatService(this._client);
6315
6429
  this.conference = new ConferenceService(this._client);
@@ -7423,6 +7537,7 @@ function ScaleMuleProvider({
7423
7537
  realtime: baseClient.realtime,
7424
7538
  storage: baseClient.storage,
7425
7539
  photo: baseClient.photo,
7540
+ video: baseClient.video,
7426
7541
  user,
7427
7542
  setUser: handleSetUser,
7428
7543
  initializing,
@@ -8736,7 +8851,7 @@ function useContent(options = {}) {
8736
8851
  );
8737
8852
  }
8738
8853
  function useMedia() {
8739
- const { storage, photo } = useScaleMule();
8854
+ const { storage, photo, video } = useScaleMule();
8740
8855
  const [uploading, setUploading] = useState(false);
8741
8856
  const [error, setError] = useState(null);
8742
8857
  const upload = useCallback(
@@ -8767,6 +8882,21 @@ function useMedia() {
8767
8882
  is_public: false
8768
8883
  };
8769
8884
  }
8885
+ if (mimeType.startsWith("video/") && !isPublic) {
8886
+ const r2 = await video.uploadViaStorage(file, sharedOpts);
8887
+ if (r2.error || !r2.data) {
8888
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8889
+ }
8890
+ return {
8891
+ file_id: r2.data.file_id,
8892
+ photo_id: null,
8893
+ original_view_url: r2.data.original_view_url,
8894
+ optimized_url_promise: Promise.resolve(null),
8895
+ hls_url_promise: r2.data.hls_url_promise,
8896
+ mime_type: mimeType,
8897
+ is_public: false
8898
+ };
8899
+ }
8770
8900
  if (mimeType.startsWith("image/") && isPublic) {
8771
8901
  const r2 = await storage.upload(file, {
8772
8902
  ...sharedOpts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@scalemule/money": "^0.0.1",
56
- "@scalemule/sdk": "^0.0.38"
56
+ "@scalemule/sdk": "^0.0.39"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "next": ">=14.0.0",