Skip to content

Commit 6f7ec21

Browse files
Update websocket notifier and add subscriptions example (#185)
## Description Part of FCE-1827 ## Changes ### Transcription example Created an example under `examples/transcription`. - It uses Bun as a runtime to check compatibility. - The backend provides a single endpoint `/peers` which just returns a peer token. - The peer is created with subscriptions enabled. - When a peer connects, the example opens a connection to Gemini Live API to start transcribing. ### WS Notifier Refactored the WS Notifier to use the builtin websocket client implementation from node/bun/deno. - The old version of the notifier used [this library](https://github.com/theturtle32/WebSocket-Node). This library [doesn't work on Bun](theturtle32/WebSocket-Node#454) and its maintenance is spotty. - Events are now not mapped to JSON. I don't know why this was done in the first place and broke type safety. - Event handlers now accept the `ServerMessage_<MessageType>` type instead of `ServerMessage` for better ergonomics. > [!NOTE] > The ws notifier change is breaking, but I don't think we should care. Adapting to the new API is easy and there's a 90% chance noone uses the notifier anyway. ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
1 parent 40d72da commit 6f7ec21

21 files changed

Lines changed: 994 additions & 2679 deletions

File tree

examples/room-manager/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import Fastify, { FastifyRequest } from 'fastify';
22
import cors from '@fastify/cors';
33
import fastifyEnv from '@fastify/env';
44
import fastifySwagger from '@fastify/swagger';
5-
import { ServerMessage } from '@fishjam-cloud/js-server-sdk/proto';
65
import healthcheck from 'fastify-healthcheck';
6+
import { ServerMessage } from '@fishjam-cloud/js-server-sdk';
77

88
import { configSchema } from './config';
99
import { rooms } from './routes';

examples/room-manager/src/plugins/fishjam.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
type RoomConfigVideoCodecEnum,
1212
type ViewerToken,
1313
} from '@fishjam-cloud/js-server-sdk';
14-
import { ServerMessage } from '@fishjam-cloud/js-server-sdk/proto';
1514
import { RoomManagerError } from '../errors';
1615
import { LivestreamData, PeerAccessData } from '../schema';
1716

@@ -24,7 +23,6 @@ declare module 'fastify' {
2423
roomType?: RoomConfigRoomTypeEnum,
2524
isPublic?: boolean
2625
) => Promise<PeerAccessData>;
27-
handleFishjamMessage: (notification: ServerMessage) => Promise<void>;
2826
getLivestreamViewerToken: (roomName: string) => Promise<ViewerToken>;
2927
getLivestreamStreamerToken: (roomName: string, isPublic: boolean) => Promise<LivestreamData>;
3028
};
@@ -85,51 +83,6 @@ export const fishjamPlugin = fastifyPlugin(async (fastify: FastifyInstance): Pro
8583
return peerAccess;
8684
}
8785

88-
async function handleFishjamMessage(notification: ServerMessage): Promise<void> {
89-
Object.entries(notification)
90-
.filter(([_, value]) => value)
91-
.forEach(([name, value]) => {
92-
fastify.log.info({ [name]: value });
93-
});
94-
95-
const peerToBeRemoved = notification.peerCrashed ?? notification.peerDeleted;
96-
97-
if (peerToBeRemoved) {
98-
const { roomId, peerId } = peerToBeRemoved;
99-
100-
const userAccess = [...peerNameToAccessMap.values()].find(
101-
({ room, peer }) => room.id === roomId && peer.id === peerId
102-
);
103-
104-
if (!userAccess) {
105-
fastify.log.info({ name: 'User not found in cache', userAccess });
106-
return;
107-
}
108-
109-
peerNameToAccessMap.delete(userAccess.peer.name);
110-
111-
fastify.log.info({ name: 'Peer deleted from cache', roomId, peerId: peerId });
112-
}
113-
114-
const roomToBeRemovedId = (notification.roomDeleted ?? notification.roomCrashed)?.roomId;
115-
116-
if (roomToBeRemovedId) {
117-
const roomName = roomNameToRoomIdMap.get(roomToBeRemovedId);
118-
if (roomName) roomNameToRoomIdMap.delete(roomName);
119-
120-
const usersToRemove = [...peerNameToAccessMap.values()].filter((user) => user.room.id === roomToBeRemovedId);
121-
122-
usersToRemove.forEach(({ peer }) => {
123-
peerNameToAccessMap.delete(peer.name);
124-
});
125-
126-
fastify.log.info({
127-
name: 'Room and users deleted from cache',
128-
roomId: roomToBeRemovedId,
129-
});
130-
}
131-
}
132-
13386
async function createPeer(roomName: string, peerName: string): Promise<PeerAccessData> {
13487
const roomId = roomNameToRoomIdMap.get(roomName);
13588

@@ -210,7 +163,6 @@ export const fishjamPlugin = fastifyPlugin(async (fastify: FastifyInstance): Pro
210163

211164
fastify.decorate('fishjam', {
212165
getPeerAccess,
213-
handleFishjamMessage,
214166
getLivestreamViewerToken,
215167
getLivestreamStreamerToken,
216168
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FISHJAM_ID=""
2+
FISHJAM_TOKEN=""
3+
GEMINI_API_KEY=""

examples/transcription/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# code coverage
10+
coverage
11+
*.lcov
12+
13+
# logs
14+
logs
15+
_.log
16+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17+
18+
# dotenv environment variable files
19+
.env
20+
.env.development.local
21+
.env.test.local
22+
.env.production.local
23+
.env.local
24+
25+
# caches
26+
.eslintcache
27+
.cache
28+
*.tsbuildinfo
29+
30+
# IntelliJ based IDEs
31+
.idea
32+
33+
# Finder (MacOS) folder config
34+
.DS_Store

examples/transcription/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Transcription with Fishjam and Gemini Live API
2+
3+
This example shows how to integrate Fishjam with the Gemini Live API.
4+
It makes use of the peer subscriptions feature in Fishjam.
5+
6+
## Development
7+
8+
To start the development server you must first copy `.env.example` to `.env`.
9+
10+
Then you need to set the following variables:
11+
- `FISHJAM_ID`: your Fishjam ID, which you can get at <https://fishjam.io>
12+
- `FISHJAM_TOKEN`: your Fishjam management token, which you can get at <https://fishjam.io>
13+
- `GEMINI_API_TOKEN`: your Gemini API token, which you can get at <https://aistudio.google.com/app/apikey>
14+
15+
Once you've set up your environment variables, all you need to do is run the following command:
16+
17+
```bash
18+
yarn dev
19+
```
20+
21+
When the server is running, you can obtain peer tokens by going to <http://localhost:3000/peers>.
22+
23+
When you connect the created peers, you will see their transcriptions in the terminal as logs.
24+
You can connect peers with the [fishjam minimal-react example](https://github.com/fishjam-cloud/web-client-sdk/tree/main/examples/react-client).
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "app",
3+
"version": "1.0.50",
4+
"scripts": {
5+
"test": "echo \"Error: no test specified\" && exit 1",
6+
"dev": "bun run --watch src/index.ts"
7+
},
8+
"dependencies": {
9+
"@fishjam-cloud/js-server-sdk": "workspace:*",
10+
"@google/genai": "^1.13.0",
11+
"@grotto/logysia": "^0.1.6",
12+
"bun": "^1.2.20",
13+
"elysia": "latest"
14+
},
15+
"devDependencies": {
16+
"@types/bun": "^1",
17+
"bun-types": "latest"
18+
},
19+
"module": "src/index.js"
20+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const TRANSCRIPTION_MODEL = 'gemini-live-2.5-flash-preview';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Elysia } from 'elysia';
2+
import { FishjamService } from '../service/fishjam';
3+
4+
export const peerController = (fishjam: FishjamService) =>
5+
new Elysia().get('/peers', async () => {
6+
const { peer: _peer, peerToken } = await fishjam.createPeer();
7+
return { token: peerToken };
8+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare global {
2+
namespace NodeJS {
3+
interface ProcessEnv {
4+
FISHJAM_ID?: string;
5+
FISHJAM_TOKEN?: string;
6+
GEMINI_API_KEY?: string;
7+
}
8+
}
9+
}
10+
11+
export {};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Elysia } from 'elysia';
2+
import { peerController } from './controllers/peers';
3+
import { FishjamService } from './service/fishjam';
4+
import { TranscriptionService } from './service/transcription';
5+
6+
if (!process.env.FISHJAM_ID || !process.env.FISHJAM_TOKEN || !process.env.GEMINI_API_KEY) {
7+
throw Error('Environment variables FISHJAM_ID, FISHJAM_TOKEN and GEMINI_API_KEY are required.');
8+
}
9+
10+
const fishjamConfig = {
11+
fishjamUrl: 'http://localhost:5002',
12+
fishjamId: process.env.FISHJAM_ID,
13+
managementToken: process.env.FISHJAM_TOKEN,
14+
};
15+
const fishjam = new FishjamService(fishjamConfig);
16+
17+
new TranscriptionService(fishjamConfig, process.env.GEMINI_API_KEY);
18+
19+
const app = new Elysia().use(peerController(fishjam)).listen(3000);
20+
21+
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);

0 commit comments

Comments
 (0)