Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-client/src/hooks/useConnection.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { GenericMetadata } from "@fishjam-cloud/ts-client";
import { httpToWebsocketUrl, resolveFishjamUrl } from "@fishjam-cloud/tsunami";
import { useCallback, useContext } from "react";

import { FishjamClientContext } from "../contexts/fishjamClient";
import { useFishjamId } from "../contexts/fishjamId";
import { PeerStatusContext } from "../contexts/peerStatus";
import { httpToWebsocketUrl, resolveFishjamUrl } from "../utils/fishjamUrl";
import { useReconnection } from "./internal/useReconnection";

export interface JoinRoomConfig<PeerMetadata extends GenericMetadata = GenericMetadata> {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-client/src/hooks/useLivestreamStreamer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LivestreamError, publishLivestream, type PublishLivestreamResult } from "@fishjam-cloud/ts-client";
import { buildLivestreamWhipUrl } from "@fishjam-cloud/tsunami";
import { useCallback, useRef, useState } from "react";

import { useFishjamId } from "../contexts/fishjamId";
import { buildLivestreamWhipUrl } from "../utils/fishjamUrl";

/** @category Livestream */
export type StreamerInputs =
Expand Down
2 changes: 1 addition & 1 deletion packages/react-client/src/hooks/useLivestreamViewer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LivestreamError, receiveLivestream, type ReceiveLivestreamResult } from "@fishjam-cloud/ts-client";
import { buildLivestreamWhepUrl } from "@fishjam-cloud/tsunami";
import { useCallback, useRef, useState } from "react";

import { useFishjamId } from "../contexts/fishjamId";
import { buildLivestreamWhepUrl } from "../utils/fishjamUrl";

export type ConnectViewerConfig = { token: string; streamId?: never } | { streamId: string; token?: never };

Expand Down
99 changes: 13 additions & 86 deletions packages/react-client/src/hooks/usePeers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { EncodingReason, Metadata, Peer, SimulcastConfig, TrackMetadata, Variant } from "@fishjam-cloud/ts-client";
import type { FishjamClient } from "@fishjam-cloud/tsunami";
import type { Metadata, Variant } from "@fishjam-cloud/ts-client";
import { localPeerWithTracks, remotePeerWithTracks } from "@fishjam-cloud/tsunami";
import { useCallback, useContext } from "react";

import { FishjamClientContext } from "../contexts/fishjamClient";
import { FishjamClientStateContext } from "../contexts/fishjamState";
import type { BrandedPeer } from "../types/internal";
import type { PeerId, RemoteTrack, Track, TrackId } from "../types/public";
import type { PeerId, RemoteTrack, Track } from "../types/public";

/**
*
Expand All @@ -24,79 +23,6 @@ export type PeerWithTracks<PeerMetadata, ServerMetadata, T extends Track = Track
customAudioTracks: T[];
};

function trackContextToTrack(track: {
metadata?: unknown;
trackId: string;
stream: MediaStream | null;
simulcastConfig?: SimulcastConfig | null;
track: MediaStreamTrack | null;
}): Track {
return {
metadata: track.metadata as TrackMetadata,
trackId: track.trackId as TrackId,
stream: track.stream,
simulcastConfig: track.simulcastConfig ?? null,
track: track.track,
};
}

function trackContextToRemoteTrack(
track: {
metadata?: unknown;
trackId: string;
stream: MediaStream | null;
simulcastConfig?: SimulcastConfig | null;
track: MediaStreamTrack | null;
encoding?: Variant;
encodingReason?: EncodingReason;
},
fishjamClient: FishjamClient,
): RemoteTrack {
return {
...trackContextToTrack(track),
encoding: track.encoding,
encodingReason: track.encodingReason,
setReceivedQuality: (encoding: Variant) => {
fishjamClient.setTargetTrackEncoding(track.trackId, encoding);
},
};
}

function getLocalPeerWithTracks<P, S>(peer: BrandedPeer<P, S>): PeerWithTracks<P, S> {
const tracks = [...peer.tracks.values()].map(trackContextToTrack);

return {
id: peer.id,
metadata: peer.metadata as Peer<P, S>["metadata"],
tracks,
cameraTrack: tracks.find(({ metadata }) => metadata?.type === "camera"),
microphoneTrack: tracks.find(({ metadata }) => metadata?.type === "microphone"),
screenShareVideoTrack: tracks.find(({ metadata }) => metadata?.type === "screenShareVideo"),
screenShareAudioTrack: tracks.find(({ metadata }) => metadata?.type === "screenShareAudio"),
customVideoTracks: tracks.filter(({ metadata }) => metadata?.type === "customVideo"),
customAudioTracks: tracks.filter(({ metadata }) => metadata?.type === "customAudio"),
};
}

function getRemotePeerWithTracks<P, S>(
peer: BrandedPeer<P, S>,
fishjamClient: FishjamClient,
): PeerWithTracks<P, S, RemoteTrack> {
const tracks = [...peer.tracks.values()].map((track) => trackContextToRemoteTrack(track, fishjamClient));

return {
id: peer.id,
metadata: peer.metadata as Peer<P, S>["metadata"],
tracks,
cameraTrack: tracks.find(({ metadata }) => metadata?.type === "camera"),
microphoneTrack: tracks.find(({ metadata }) => metadata?.type === "microphone"),
screenShareVideoTrack: tracks.find(({ metadata }) => metadata?.type === "screenShareVideo"),
screenShareAudioTrack: tracks.find(({ metadata }) => metadata?.type === "screenShareAudio"),
customVideoTracks: tracks.filter(({ metadata }) => metadata?.type === "customVideo"),
customAudioTracks: tracks.filter(({ metadata }) => metadata?.type === "customAudio"),
};
}

/**
* Hook allows to access id, tracks and metadata of the local and remote peers.
*
Expand All @@ -110,18 +36,19 @@ export function usePeers<PeerMetadata = Record<string, unknown>, ServerMetadata
const fishjamClient = useContext(FishjamClientContext);
if (!clientState || !fishjamClient) throw Error("usePeers must be used within FishjamProvider");

const localPeer: PeerWithTracks<PeerMetadata, ServerMetadata> | null = clientState.localPeer
? getLocalPeerWithTracks<PeerMetadata, ServerMetadata>(
clientState.localPeer as BrandedPeer<PeerMetadata, ServerMetadata>,
)
// The tsunami views are structurally identical to the public shapes; the
// casts reintroduce the branded ids and DOM media types of the public API.
const localPeer = clientState.localPeer
? (localPeerWithTracks(clientState.localPeer) as unknown as PeerWithTracks<PeerMetadata, ServerMetadata>)
: null;

const remotePeers: PeerWithTracks<PeerMetadata, ServerMetadata, RemoteTrack>[] = Object.values(clientState.peers).map(
const remotePeers = Object.values(clientState.peers).map(
(peer) =>
getRemotePeerWithTracks<PeerMetadata, ServerMetadata>(
peer as BrandedPeer<PeerMetadata, ServerMetadata>,
fishjamClient.current,
),
remotePeerWithTracks(peer, fishjamClient.current) as unknown as PeerWithTracks<
PeerMetadata,
ServerMetadata,
RemoteTrack
>,
);

const setReceivedTracksQuality = useCallback(
Expand Down
142 changes: 31 additions & 111 deletions packages/react-client/src/hooks/useSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,44 @@
import {
getSandboxLivestream,
getSandboxMoqPublisherAccess,
getSandboxMoqSubscriberAccess,
getSandboxPeerToken,
getSandboxViewerToken,
type MoqAccess,
type RoomType,
} from "@fishjam-cloud/tsunami";
import { useCallback } from "react";

import { MissingSandboxApiUrlError } from "../utils/errors";

type BasicInfo = { id: string; name: string };
type RoomManagerResponse = {
peerToken: string;
url: string;
room: BasicInfo;
peer: BasicInfo;
};

type MoqAccessResponse = {
connection_url: string;
token: string;
};

export type MoqAccess = {
connectionUrl: string;
token: string;
};
export type { MoqAccess, RoomType };

export type UseSandboxProps = {
sandboxApiUrl: string;
};

export type RoomType = "conference" | "livestream" | "audio_only";

export const useSandbox = (props: UseSandboxProps) => {
const sandboxApiUrl = props?.sandboxApiUrl;

const getSandboxPeerToken = useCallback(
async (roomName: string, peerName: string, roomType: RoomType = "conference") => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const url = new URL(sandboxApiUrl);
url.searchParams.set("roomName", roomName);
url.searchParams.set("peerName", peerName);
url.searchParams.set("roomType", roomType);

const res = await fetch(url);

if (!res.ok) {
const message = `Failed to retrieve peer token for peer '${peerName}' in ${roomType} room '${roomName}'.`;
throw new Error(message);
}

const data: RoomManagerResponse = await res.json();
return data.peerToken;
},
[sandboxApiUrl],
);

const getSandboxViewerToken = useCallback(
async (roomName: string) => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const url = new URL(`${sandboxApiUrl}/${roomName}/livestream-viewer-token`);

const res = await fetch(url);
if (!res.ok) {
let message = `Failed to retrieve viewer token for '${roomName}' livestream room.`;
if (res.status === 404) {
message = `A livestream room of name '${roomName}' does not exist.`;
}
throw new Error(message);
}
const data: { token: string } = await res.json();

return data.token;
},
[sandboxApiUrl],
);

const getSandboxLivestream = useCallback(
async (roomName: string, isPublic: boolean = false) => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const url = new URL(`${sandboxApiUrl}/livestream`);
url.searchParams.set("roomName", roomName);
url.searchParams.set("public", isPublic.toString());

const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to retrieve streamer token for '${roomName}' livestream room.`);

const data: { streamerToken: string; room: { id: string; name: string } } = await res.json();
return data;
},
[sandboxApiUrl],
);

const fetchMoqAccess = useCallback(
async (streamName: string, type: "subscriber" | "publisher"): Promise<MoqAccess> => {
if (!sandboxApiUrl) throw new MissingSandboxApiUrlError();

const urlEncodedStreamName = encodeURIComponent(streamName);

const res = await fetch(`${sandboxApiUrl}/moq/${urlEncodedStreamName}/${type}`);
if (!res.ok) throw new Error(`Failed to retrieve MoQ ${type} connection for stream '${streamName}'.`);

const data: MoqAccessResponse = await res.json();
return { connectionUrl: data.connection_url, token: data.token };
},
[sandboxApiUrl],
);

const getSandboxMoqPublisherAccess = useCallback(
async (streamName: string) => fetchMoqAccess(streamName, "publisher"),
[fetchMoqAccess],
);

const getSandboxMoqSubscriberAccess = useCallback(
async (streamName: string) => fetchMoqAccess(streamName, "subscriber"),
[fetchMoqAccess],
);

return {
getSandboxPeerToken,
getSandboxViewerToken,
getSandboxLivestream,
getSandboxMoqPublisherAccess,
getSandboxMoqSubscriberAccess,
getSandboxPeerToken: useCallback(
(roomName: string, peerName: string, roomType: RoomType = "conference") =>
getSandboxPeerToken(sandboxApiUrl, roomName, peerName, roomType),
[sandboxApiUrl],
),
getSandboxViewerToken: useCallback(
(roomName: string) => getSandboxViewerToken(sandboxApiUrl, roomName),
[sandboxApiUrl],
),
getSandboxLivestream: useCallback(
(roomName: string, isPublic: boolean = false) => getSandboxLivestream(sandboxApiUrl, roomName, isPublic),
[sandboxApiUrl],
),
getSandboxMoqPublisherAccess: useCallback(
(streamName: string) => getSandboxMoqPublisherAccess(sandboxApiUrl, streamName),
[sandboxApiUrl],
),
getSandboxMoqSubscriberAccess: useCallback(
(streamName: string) => getSandboxMoqSubscriberAccess(sandboxApiUrl, streamName),
[sandboxApiUrl],
),
};
};
6 changes: 0 additions & 6 deletions packages/react-client/src/utils/errors.ts

This file was deleted.

25 changes: 25 additions & 0 deletions packages/tsunami/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export { WebDeviceManager, type WebDeviceManagerOptions } from "./devices/WebDev
export { type ErrorRecoverability, FishjamError } from "./errors/FishjamError";
export { ClientDisposedError, DeviceManagerMissingError } from "./errors/lifecycleErrors";
export { FishjamClient, type FishjamClientConfig } from "./FishjamClient";
export {
buildLivestreamWhepUrl,
buildLivestreamWhipUrl,
extractDomainFromFishjamId,
httpToWebsocketUrl,
resolveFishjamUrl,
} from "./fishjamUrl";
export type {
BandwidthLimits,
InitializeDevicesResult,
Expand All @@ -40,6 +47,16 @@ export type {
TracksMiddleware,
TracksMiddlewareResult,
} from "./mediaTypes";
export {
getSandboxLivestream,
getSandboxMoqPublisherAccess,
getSandboxMoqSubscriberAccess,
getSandboxPeerToken,
getSandboxViewerToken,
MissingSandboxApiUrlError,
type MoqAccess,
type RoomType,
} from "./sandbox";
export {
type ClientState,
createInitialClientState,
Expand All @@ -48,5 +65,13 @@ export {
type PeerStatus,
type ScreenShareState,
} from "./state/clientState";
export {
localPeerWithTracks,
type PeerTrackView,
type PeerWithTracksView,
type RemotePeerTrackView,
remotePeerWithTracks,
type RemoteTrackQualitySetter,
} from "./state/peerViews";
export { StateStore, type StateStoreOptions, type StoreListener } from "./state/StateStore";
export * from "@fishjam-cloud/ts-client";
Loading