@maravilla-labs/platform 0.1.26 → 0.1.28

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.js CHANGED
@@ -1,3 +1,55 @@
1
+ // src/media.ts
2
+ var RemoteMediaService = class {
3
+ constructor(baseUrl, headers) {
4
+ this.baseUrl = baseUrl;
5
+ this.headers = headers;
6
+ }
7
+ baseUrl;
8
+ headers;
9
+ async fetch(url, options = {}) {
10
+ const response = await fetch(url, {
11
+ ...options,
12
+ headers: { ...this.headers, ...options.headers }
13
+ });
14
+ if (!response.ok) {
15
+ const error = await response.text();
16
+ throw new Error(`Media API error: ${response.status} - ${error}`);
17
+ }
18
+ return response;
19
+ }
20
+ async createRoom(roomId, settings = {}) {
21
+ const response = await this.fetch(`${this.baseUrl}/api/media/rooms`, {
22
+ method: "POST",
23
+ body: JSON.stringify({ roomId, settings })
24
+ });
25
+ return response.json();
26
+ }
27
+ async deleteRoom(roomId) {
28
+ await this.fetch(`${this.baseUrl}/api/media/rooms/${encodeURIComponent(roomId)}`, {
29
+ method: "DELETE"
30
+ });
31
+ }
32
+ async listRooms() {
33
+ const response = await this.fetch(`${this.baseUrl}/api/media/rooms`);
34
+ return response.json();
35
+ }
36
+ async generateToken(roomId, participant) {
37
+ const response = await this.fetch(
38
+ `${this.baseUrl}/api/media/rooms/${encodeURIComponent(roomId)}/token`,
39
+ {
40
+ method: "POST",
41
+ body: JSON.stringify(participant)
42
+ }
43
+ );
44
+ return response.json();
45
+ }
46
+ async mediaUrl() {
47
+ const response = await this.fetch(`${this.baseUrl}/api/media/url`);
48
+ const data = await response.json();
49
+ return data.url;
50
+ }
51
+ };
52
+
1
53
  // src/remote-client.ts
2
54
  var RemoteKvNamespace = class {
3
55
  constructor(baseUrl, namespace, headers) {
@@ -5,6 +57,9 @@ var RemoteKvNamespace = class {
5
57
  this.namespace = namespace;
6
58
  this.headers = headers;
7
59
  }
60
+ baseUrl;
61
+ namespace;
62
+ headers;
8
63
  /**
9
64
  * Internal method for making HTTP requests to the dev server.
10
65
  * Handles error responses and authentication headers.
@@ -61,6 +116,8 @@ var RemoteDatabase = class {
61
116
  this.baseUrl = baseUrl;
62
117
  this.headers = headers;
63
118
  }
119
+ baseUrl;
120
+ headers;
64
121
  /**
65
122
  * Internal method for making HTTP requests to the dev server.
66
123
  * Handles error responses and authentication headers.
@@ -124,6 +181,8 @@ var RemoteStorage = class _RemoteStorage {
124
181
  this.baseUrl = baseUrl;
125
182
  this.headers = headers;
126
183
  }
184
+ baseUrl;
185
+ headers;
127
186
  /**
128
187
  * Internal method for making HTTP requests to the dev server.
129
188
  * Handles error responses and authentication headers.
@@ -276,12 +335,14 @@ function createRemoteClient(baseUrl, tenant) {
276
335
  });
277
336
  const db = new RemoteDatabase(baseUrl, headers);
278
337
  const storage = new RemoteStorage(baseUrl, headers);
338
+ const media = new RemoteMediaService(baseUrl, headers);
279
339
  return {
280
340
  env: {
281
341
  KV: kvProxy,
282
342
  DB: db,
283
343
  STORAGE: storage
284
- }
344
+ },
345
+ media
285
346
  };
286
347
  }
287
348
 
@@ -373,6 +434,12 @@ var RenClient = class {
373
434
  "runtime.snapshot.ready",
374
435
  "runtime.worker.started",
375
436
  "runtime.worker.stopped",
437
+ // Realtime channel events
438
+ "realtime.message",
439
+ // Presence events
440
+ "presence.join",
441
+ "presence.leave",
442
+ "presence.update",
376
443
  // Meta events
377
444
  "ren.meta"
378
445
  ];
@@ -450,6 +517,467 @@ async function storageDelete(path, clientId) {
450
517
  return res.json().catch(() => ({}));
451
518
  }
452
519
 
520
+ // src/realtime.ts
521
+ var RealtimeClient = class {
522
+ wsEndpoint;
523
+ clientId;
524
+ ws = null;
525
+ closed = false;
526
+ attempt = 0;
527
+ autoReconnect;
528
+ maxBackoff;
529
+ debug;
530
+ channelListeners = /* @__PURE__ */ new Map();
531
+ globalListeners = /* @__PURE__ */ new Set();
532
+ presenceListeners = /* @__PURE__ */ new Map();
533
+ subscribedChannels = /* @__PURE__ */ new Set();
534
+ pendingMessages = [];
535
+ constructor(opts = {}) {
536
+ this.wsEndpoint = opts.wsEndpoint || this.detectEndpoint();
537
+ this.clientId = opts.clientId || getOrCreateClientId();
538
+ this.autoReconnect = opts.autoReconnect !== false;
539
+ this.maxBackoff = opts.maxBackoffMs ?? 15e3;
540
+ this.debug = !!opts.debug;
541
+ }
542
+ detectEndpoint() {
543
+ if (typeof window !== "undefined") {
544
+ const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
545
+ if (window.location.port === "5173") {
546
+ return `ws://${window.location.hostname}:3001/_rt/ws`;
547
+ }
548
+ return `${proto}//${window.location.host}/_rt/ws`;
549
+ }
550
+ return "ws://localhost:3001/_rt/ws";
551
+ }
552
+ log(...args) {
553
+ if (this.debug) console.debug("[RealtimeClient]", ...args);
554
+ }
555
+ /** Connect to the realtime WebSocket server */
556
+ connect() {
557
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
558
+ const url = `${this.wsEndpoint}?cid=${encodeURIComponent(this.clientId)}`;
559
+ this.log("connecting", url);
560
+ this.closed = false;
561
+ try {
562
+ this.ws = new WebSocket(url);
563
+ } catch (e) {
564
+ this.log("WebSocket constructor failed", e);
565
+ this.scheduleReconnect();
566
+ return;
567
+ }
568
+ this.ws.onopen = () => {
569
+ this.attempt = 0;
570
+ this.log("connected");
571
+ for (const ch of this.subscribedChannels) {
572
+ this.sendRaw({ action: "subscribe", channel: ch });
573
+ }
574
+ for (const msg of this.pendingMessages) {
575
+ this.ws?.send(msg);
576
+ }
577
+ this.pendingMessages = [];
578
+ };
579
+ this.ws.onmessage = (ev) => {
580
+ try {
581
+ const event = JSON.parse(ev.data);
582
+ this.log("received", event);
583
+ this.dispatch(event);
584
+ } catch (e) {
585
+ this.log("malformed message", ev.data, e);
586
+ }
587
+ };
588
+ this.ws.onerror = (ev) => {
589
+ this.log("error", ev);
590
+ };
591
+ this.ws.onclose = () => {
592
+ this.log("disconnected");
593
+ this.ws = null;
594
+ if (!this.closed && this.autoReconnect) {
595
+ this.scheduleReconnect();
596
+ }
597
+ };
598
+ }
599
+ scheduleReconnect() {
600
+ const delay = Math.min(1e3 * Math.pow(2, this.attempt++), this.maxBackoff);
601
+ this.log("reconnecting in", delay, "ms");
602
+ setTimeout(() => this.connect(), delay);
603
+ }
604
+ sendRaw(msg) {
605
+ const json = JSON.stringify(msg);
606
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
607
+ this.ws.send(json);
608
+ } else {
609
+ this.pendingMessages.push(json);
610
+ }
611
+ }
612
+ dispatch(event) {
613
+ this.globalListeners.forEach((cb) => cb(event));
614
+ if (event.channel) {
615
+ const listeners = this.channelListeners.get(event.channel);
616
+ if (listeners) {
617
+ listeners.forEach((cb) => cb(event));
618
+ }
619
+ }
620
+ if (event.event === "presence:join" || event.event === "presence:leave") {
621
+ const presenceSet = this.presenceListeners.get(event.channel);
622
+ if (presenceSet) {
623
+ const member = {
624
+ userId: event.userId || "",
625
+ metadata: event.metadata,
626
+ lastSeen: event.ts
627
+ };
628
+ if (event.event === "presence:join") {
629
+ presenceSet.onJoin.forEach((cb) => cb(member));
630
+ } else {
631
+ presenceSet.onLeave.forEach((cb) => cb(member));
632
+ }
633
+ }
634
+ }
635
+ }
636
+ /** Subscribe to messages on a channel */
637
+ subscribe(channel, callback, options) {
638
+ if (!this.channelListeners.has(channel)) {
639
+ this.channelListeners.set(channel, /* @__PURE__ */ new Set());
640
+ }
641
+ this.channelListeners.get(channel).add(callback);
642
+ if (!this.subscribedChannels.has(channel)) {
643
+ this.subscribedChannels.add(channel);
644
+ const msg = { action: "subscribe", channel };
645
+ if (options?.token) {
646
+ msg.token = options.token;
647
+ }
648
+ this.sendRaw(msg);
649
+ }
650
+ return () => {
651
+ const listeners = this.channelListeners.get(channel);
652
+ if (listeners) {
653
+ listeners.delete(callback);
654
+ if (listeners.size === 0) {
655
+ this.channelListeners.delete(channel);
656
+ this.subscribedChannels.delete(channel);
657
+ this.sendRaw({ action: "unsubscribe", channel });
658
+ }
659
+ }
660
+ };
661
+ }
662
+ /** Listen to all events across all channels */
663
+ onAny(callback) {
664
+ this.globalListeners.add(callback);
665
+ return () => {
666
+ this.globalListeners.delete(callback);
667
+ };
668
+ }
669
+ /** Publish a message to a channel */
670
+ publish(channel, data, options) {
671
+ this.sendRaw({
672
+ action: "publish",
673
+ channel,
674
+ data,
675
+ userId: options?.userId
676
+ });
677
+ }
678
+ /** Get a presence handle for a channel */
679
+ presence(channel) {
680
+ if (!this.presenceListeners.has(channel)) {
681
+ this.presenceListeners.set(channel, {
682
+ onJoin: /* @__PURE__ */ new Set(),
683
+ onLeave: /* @__PURE__ */ new Set()
684
+ });
685
+ }
686
+ const listeners = this.presenceListeners.get(channel);
687
+ return {
688
+ /** Join the channel with presence */
689
+ join: (userId, metadata) => {
690
+ this.sendRaw({
691
+ action: "presence:join",
692
+ channel,
693
+ userId,
694
+ metadata
695
+ });
696
+ },
697
+ /** Leave the channel */
698
+ leave: () => {
699
+ this.sendRaw({ action: "presence:leave", channel });
700
+ },
701
+ /** Listen for users joining */
702
+ onJoin: (callback) => {
703
+ listeners.onJoin.add(callback);
704
+ return () => {
705
+ listeners.onJoin.delete(callback);
706
+ };
707
+ },
708
+ /** Listen for users leaving */
709
+ onLeave: (callback) => {
710
+ listeners.onLeave.add(callback);
711
+ return () => {
712
+ listeners.onLeave.delete(callback);
713
+ };
714
+ }
715
+ };
716
+ }
717
+ /** Get current client ID */
718
+ getClientId() {
719
+ return this.clientId;
720
+ }
721
+ /** Check if connected */
722
+ isConnected() {
723
+ return this.ws?.readyState === WebSocket.OPEN;
724
+ }
725
+ /** Disconnect and stop reconnecting */
726
+ disconnect() {
727
+ this.closed = true;
728
+ this.ws?.close();
729
+ this.ws = null;
730
+ this.pendingMessages = [];
731
+ this.log("disconnected (manual)");
732
+ }
733
+ };
734
+
735
+ // src/media-room.ts
736
+ import {
737
+ Room,
738
+ RoomEvent,
739
+ Track,
740
+ ConnectionState
741
+ } from "livekit-client";
742
+ var MediaRoomEvent = /* @__PURE__ */ ((MediaRoomEvent2) => {
743
+ MediaRoomEvent2["Connected"] = "connected";
744
+ MediaRoomEvent2["Reconnecting"] = "reconnecting";
745
+ MediaRoomEvent2["Reconnected"] = "reconnected";
746
+ MediaRoomEvent2["Disconnected"] = "disconnected";
747
+ MediaRoomEvent2["ParticipantJoined"] = "participantJoined";
748
+ MediaRoomEvent2["ParticipantLeft"] = "participantLeft";
749
+ MediaRoomEvent2["TrackSubscribed"] = "trackSubscribed";
750
+ MediaRoomEvent2["TrackUnsubscribed"] = "trackUnsubscribed";
751
+ MediaRoomEvent2["TrackMuted"] = "trackMuted";
752
+ MediaRoomEvent2["TrackUnmuted"] = "trackUnmuted";
753
+ MediaRoomEvent2["ActiveSpeakersChanged"] = "activeSpeakersChanged";
754
+ MediaRoomEvent2["DataReceived"] = "dataReceived";
755
+ MediaRoomEvent2["RecordingStatusChanged"] = "recordingStatusChanged";
756
+ MediaRoomEvent2["MediaDevicesChanged"] = "mediaDevicesChanged";
757
+ return MediaRoomEvent2;
758
+ })(MediaRoomEvent || {});
759
+ function mapSource(source) {
760
+ switch (source) {
761
+ case Track.Source.Camera:
762
+ return "camera";
763
+ case Track.Source.Microphone:
764
+ return "microphone";
765
+ case Track.Source.ScreenShare:
766
+ return "screen_share";
767
+ case Track.Source.ScreenShareAudio:
768
+ return "screen_share_audio";
769
+ default:
770
+ return "camera";
771
+ }
772
+ }
773
+ function mapParticipant(p) {
774
+ const tracks = [];
775
+ p.trackPublications.forEach((pub) => {
776
+ tracks.push({
777
+ trackSid: pub.trackSid,
778
+ source: mapSource(pub.source),
779
+ kind: pub.kind === Track.Kind.Video ? "video" : "audio",
780
+ muted: pub.isMuted,
781
+ track: pub.track?.mediaStreamTrack
782
+ });
783
+ });
784
+ return {
785
+ identity: p.identity,
786
+ name: p.name,
787
+ metadata: p.metadata,
788
+ isSpeaking: p.isSpeaking,
789
+ audioLevel: p.audioLevel,
790
+ tracks
791
+ };
792
+ }
793
+ function attachTrack(track, element) {
794
+ const stream = new MediaStream([track]);
795
+ element.srcObject = stream;
796
+ if (element instanceof HTMLVideoElement) {
797
+ element.playsInline = true;
798
+ }
799
+ element.play().catch(() => {
800
+ });
801
+ }
802
+ function detachTrack(element) {
803
+ element.srcObject = null;
804
+ }
805
+ var MediaLocalParticipant = class {
806
+ lp;
807
+ /** @internal */
808
+ constructor(lp) {
809
+ this.lp = lp;
810
+ }
811
+ get identity() {
812
+ return this.lp.identity;
813
+ }
814
+ get name() {
815
+ return this.lp.name;
816
+ }
817
+ get metadata() {
818
+ return this.lp.metadata;
819
+ }
820
+ get isSpeaking() {
821
+ return this.lp.isSpeaking;
822
+ }
823
+ get audioLevel() {
824
+ return this.lp.audioLevel;
825
+ }
826
+ get tracks() {
827
+ return mapParticipant(this.lp).tracks;
828
+ }
829
+ // Camera
830
+ get isCameraEnabled() {
831
+ return !!this.lp.getTrackPublication(Track.Source.Camera)?.track && !this.lp.getTrackPublication(Track.Source.Camera)?.isMuted;
832
+ }
833
+ async enableCamera(options) {
834
+ await this.lp.setCameraEnabled(true, {
835
+ deviceId: options?.deviceId,
836
+ resolution: options?.resolution ? { width: options.resolution.width, height: options.resolution.height, frameRate: options.resolution.frameRate } : void 0
837
+ });
838
+ }
839
+ async disableCamera() {
840
+ await this.lp.setCameraEnabled(false);
841
+ }
842
+ // Microphone
843
+ get isMicrophoneEnabled() {
844
+ return !!this.lp.getTrackPublication(Track.Source.Microphone)?.track && !this.lp.getTrackPublication(Track.Source.Microphone)?.isMuted;
845
+ }
846
+ async enableMicrophone(options) {
847
+ await this.lp.setMicrophoneEnabled(true, { deviceId: options?.deviceId });
848
+ }
849
+ async disableMicrophone() {
850
+ await this.lp.setMicrophoneEnabled(false);
851
+ }
852
+ // Screen share
853
+ get isScreenShareEnabled() {
854
+ return !!this.lp.getTrackPublication(Track.Source.ScreenShare)?.track;
855
+ }
856
+ async enableScreenShare(options) {
857
+ await this.lp.setScreenShareEnabled(true, { audio: options?.audio });
858
+ }
859
+ async disableScreenShare() {
860
+ await this.lp.setScreenShareEnabled(false);
861
+ }
862
+ // Metadata
863
+ async setName(name) {
864
+ await this.lp.setName(name);
865
+ }
866
+ async setMetadata(metadata) {
867
+ await this.lp.setMetadata(metadata);
868
+ }
869
+ // Data
870
+ async sendData(data, options) {
871
+ await this.lp.publishData(data, { reliable: options?.reliable ?? true });
872
+ }
873
+ };
874
+ var MediaRoom = class _MediaRoom {
875
+ room;
876
+ listeners = /* @__PURE__ */ new Map();
877
+ _localParticipant;
878
+ constructor() {
879
+ this.room = new Room();
880
+ }
881
+ async connect(url, token, options) {
882
+ const opts = {};
883
+ if (options?.autoSubscribe !== void 0) opts.autoSubscribe = options.autoSubscribe;
884
+ this.room.options.adaptiveStream = options?.adaptiveStream ?? true;
885
+ this.room.options.dynacast = options?.dynacast ?? true;
886
+ this.wireEvents();
887
+ await this.room.connect(url, token, opts);
888
+ this._localParticipant = new MediaLocalParticipant(this.room.localParticipant);
889
+ }
890
+ async disconnect() {
891
+ await this.room.disconnect();
892
+ }
893
+ get state() {
894
+ switch (this.room.state) {
895
+ case ConnectionState.Connected:
896
+ return "connected";
897
+ case ConnectionState.Connecting:
898
+ return "connecting";
899
+ case ConnectionState.Reconnecting:
900
+ return "reconnecting";
901
+ default:
902
+ return "disconnected";
903
+ }
904
+ }
905
+ get localParticipant() {
906
+ return this._localParticipant;
907
+ }
908
+ get participants() {
909
+ const map = /* @__PURE__ */ new Map();
910
+ this.room.remoteParticipants.forEach((p, id) => map.set(id, mapParticipant(p)));
911
+ return map;
912
+ }
913
+ get activeSpeakers() {
914
+ return this.room.activeSpeakers.map(mapParticipant);
915
+ }
916
+ get name() {
917
+ return this.room.name;
918
+ }
919
+ get numParticipants() {
920
+ return this.room.numParticipants;
921
+ }
922
+ get isRecording() {
923
+ return this.room.isRecording;
924
+ }
925
+ // Events
926
+ on(event, callback) {
927
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
928
+ this.listeners.get(event).add(callback);
929
+ return this;
930
+ }
931
+ off(event, callback) {
932
+ this.listeners.get(event)?.delete(callback);
933
+ return this;
934
+ }
935
+ emit(event, ...args) {
936
+ this.listeners.get(event)?.forEach((cb) => cb(...args));
937
+ }
938
+ // Device switching
939
+ async switchCamera(deviceId) {
940
+ await this.room.switchActiveDevice("videoinput", deviceId);
941
+ }
942
+ async switchMicrophone(deviceId) {
943
+ await this.room.switchActiveDevice("audioinput", deviceId);
944
+ }
945
+ async switchSpeaker(deviceId) {
946
+ await this.room.switchActiveDevice("audiooutput", deviceId);
947
+ }
948
+ // Static device enumeration
949
+ static async getDevices() {
950
+ return navigator.mediaDevices.enumerateDevices();
951
+ }
952
+ static async getCameras() {
953
+ return (await _MediaRoom.getDevices()).filter((d) => d.kind === "videoinput");
954
+ }
955
+ static async getMicrophones() {
956
+ return (await _MediaRoom.getDevices()).filter((d) => d.kind === "audioinput");
957
+ }
958
+ static async getSpeakers() {
959
+ return (await _MediaRoom.getDevices()).filter((d) => d.kind === "audiooutput");
960
+ }
961
+ // ─── Event wiring (LiveKit → MediaRoom) ───
962
+ wireEvents() {
963
+ const r = this.room;
964
+ r.on(RoomEvent.Connected, () => this.emit("connected" /* Connected */));
965
+ r.on(RoomEvent.Reconnecting, () => this.emit("reconnecting" /* Reconnecting */));
966
+ r.on(RoomEvent.Reconnected, () => this.emit("reconnected" /* Reconnected */));
967
+ r.on(RoomEvent.Disconnected, (reason) => this.emit("disconnected" /* Disconnected */, reason));
968
+ r.on(RoomEvent.MediaDevicesChanged, () => this.emit("mediaDevicesChanged" /* MediaDevicesChanged */));
969
+ r.on(RoomEvent.RecordingStatusChanged, (recording) => this.emit("recordingStatusChanged" /* RecordingStatusChanged */, recording));
970
+ r.on(RoomEvent.ParticipantConnected, (p) => this.emit("participantJoined" /* ParticipantJoined */, mapParticipant(p)));
971
+ r.on(RoomEvent.ParticipantDisconnected, (p) => this.emit("participantLeft" /* ParticipantLeft */, mapParticipant(p)));
972
+ r.on(RoomEvent.TrackSubscribed, (track, pub, p) => this.emit("trackSubscribed" /* TrackSubscribed */, track.mediaStreamTrack, mapParticipant(p)));
973
+ r.on(RoomEvent.TrackUnsubscribed, (track, pub, p) => this.emit("trackUnsubscribed" /* TrackUnsubscribed */, track.mediaStreamTrack, mapParticipant(p)));
974
+ r.on(RoomEvent.TrackMuted, (pub, p) => this.emit("trackMuted" /* TrackMuted */, mapParticipant(p)));
975
+ r.on(RoomEvent.TrackUnmuted, (pub, p) => this.emit("trackUnmuted" /* TrackUnmuted */, mapParticipant(p)));
976
+ r.on(RoomEvent.ActiveSpeakersChanged, (speakers) => this.emit("activeSpeakersChanged" /* ActiveSpeakersChanged */, speakers.map(mapParticipant)));
977
+ r.on(RoomEvent.DataReceived, (data, p) => this.emit("dataReceived" /* DataReceived */, data, p ? mapParticipant(p) : void 0));
978
+ }
979
+ };
980
+
453
981
  // src/index.ts
454
982
  var cachedPlatform = null;
455
983
  function getPlatform(options) {
@@ -484,8 +1012,15 @@ function clearPlatformCache() {
484
1012
  }
485
1013
  }
486
1014
  export {
1015
+ MediaLocalParticipant,
1016
+ MediaRoom,
1017
+ MediaRoomEvent,
1018
+ RealtimeClient,
1019
+ RemoteMediaService,
487
1020
  RenClient,
1021
+ attachTrack,
488
1022
  clearPlatformCache,
1023
+ detachTrack,
489
1024
  getOrCreateClientId,
490
1025
  getPlatform,
491
1026
  renFetch,