# Project export: Matcha Vibe

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Make the best of every meeting. We use real-time analytics with personalized AI agents to convert valuable human experiences into actionable insights for key decision makers for business and education
- Devpost: https://devpost.com/software/matchavibe
- GitHub: https://github.com/treehacks2025/mockRTMSserver
- Video: https://www.youtube.com/embed/JoYMb-LxBJQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — trasta (21 commits), WesleyBLDC (3 commits), wesley-autocomplete (2 commits)

## Devpost submission (written by the team)

### Inspiration

Sen no Rikyu, the founder of the Japanese tea ceremony, explained its philosophy with this phrase: 一期一会 (ichi-go ichi-e): one time – one meeting. Life is about seizing every moment. Business pitches, lectures, and personal conversations are all fleeting—so make them count. The Problem: Remote meetings lack presence and depth—listeners get distracted, engagement is low, and tracking audience feedback is difficult. If you cannot perceive your audience's status, your meetings are left sub-par. Inspired by the Japanese tea ceremony philosophy, we set out to enhance real-time engagement in virtual meetings: The Solution: AI-driven avatars and immersive environments that make online meetings not just as good as in-person, but even better. Matcha and meditation apps like Otsuka ekkomi brought premium meditative experiences into the digital realm. We asked ourselves: How can we take this further and enable meaningful, premium interactions to be held online? We focused on the educational and healthcare benefits of AI companionship and metrics, and hence redefined virtual meetings to be more dynamic, engaging, and memorable. Thus, we came up with our real-time meeting analytics tools and responsive interactive experience we call Matcha Vibe. We create meetings that match your vibe. Everyone does Zoom meetings. We turn Zoom meetings into personalized experiences tailored to real-time consumer feedback. Why This Matters 90% of communication is non-verbal cues, which are hard to track online. We capture real-time engagement metrics to analyze each meeting participant’s mood, attention, and interaction levels, allowing businesses and educators to adjust dynamically. Seamless group experiences using Zoom’s API—turning meetings into engaging, responsive environments that feel as immersive as real-world interactions. What It Does Real-Time Speech & Gesture Analysis – The AI detects sentiment, engagement levels, and facial expressions to understand participant reactions. Personalized AI Presence – The avatar adapts dynamically depending on the meeting flow from analytics. From Mindfulness to Business Pitches – Whether it's a tea ceremony to guide relaxation or an AI-driven sales coach assessing audience attention, this system is open for personalized interactions for any situation. By integrating AI-driven adaptability, MatchaVibe transforms how we engage in virtual meetings, making online collaboration more immersive and impactful. How We Built It Leveraging Zoom’s API, we integrated real-time analytics to track user engagement, expressions, and speech sentiment. AI-powered avatars were developed using Groq for fast AI processing, ensuring smooth and intelligent avatar behavior. Dynamic backgrounds were created using Luma Apps, adapting the meeting atmosphere in real-time. Facial expression tracking was implemented to recognize stress levels and engagement, providing immediate visual or verbal responses. Groq’s multimodal capabilities powered seamless text, vision, and audio processing, making the AI avatar highly responsive across different forms of input. Gemini 2.0 Flash for sentiment analysis to interpret real-time user feedback. ElevenLabs for AI-driven conversational speech and text-to-speech for seamless user interactions. Luma AI transforms text, images, and video seamlessly to create immersive meeting environments and interactive backgrounds that react to real-time consumer metrics. OpenAI APIs enhance AI-generated conversation, sentiment-driven avatar responses, and dynamic adaptability. Challenges We Ran Into Balancing AI responsiveness with natural human interaction—we refined avatar expressions to feel intuitive rather than robotic. Processing real-time engagement metrics efficiently—Groq allowed us to minimize latency while running AI analytics. Ensuring accessibility and inclusivity—different cultures have different stress triggers and relaxation cues, requiring adaptive feedback mechanisms. Optimizing multimodal AI coordination—seamless real-time text, vision, and audio processing presented computational challenges. Integrating multiple AI models (Gemini, ElevenLabs, Groq) into a single pipeline while maintaining efficiency and reducing response lag. Accomplishments That We're Proud Of Created a fully interactive AI-driven avatar that dynamically reacts to user speech, emotions, and gestures in Zoom meetings. Successfully integrated real-time sentiment and behavior analysis to adjust meeting atmosphere and enhance engagement. Built an AI presence that goes beyond text and voice responses, setting a new standard for digital interaction. Utilized Groq’s multimodal processing to enhance responsiveness across multiple sensory inputs. Implemented Gemini’s sentiment analysis to personalize meeting engagement. Leveraged ElevenLabs AI voice processing to make virtual meetings more interactive and human-like. Enhanced real-time media interactions using Luma AI for seamless content adaptation. By enhancing collaboration with AI-driven insights, Matcha Vibe exemplifies how Zoom’s tools can push the boundaries of digital interaction and connect people more effectively. What’s Next for MatchaVibe? Expanding AI Emotion & Gesture Recognition – Detect stress, disengagement, and excitement with even greater accuracy. Integrating Voice Analysis for Deeper Insights – Use pitch and pacing analysis to understand user confidence and engagement. Customizable AI Avatars for Different Use Cases – Expand beyond tea ceremonies to education, business, and therapy. AI Coaching & Learning Tools – Guide users through public speaking, sales pitches, and mindfulness exercises using AI-driven feedback. Matcha Vibe is redefining virtual engagement, blending AI-driven analytics, personalized avatars, and real-time interactions to create truly immersive digital experiences. From tea ceremonies to boardrooms, we make online meetings more human.

## README (from the GitHub repository)

# Realtime Media Streams Mock Server -- V.1.0

> **Confidential under NDA - Do Not Distribute**<br/>
> The information in this document is confidential and requires an NDA. It is intended only for partners in the Zoom RTMS Beta Developers program. Participation in the RTMS Beta Offering, including access to and use of these RTMS Beta Offering materials, is subject to Zoom's [Beta Program - Terms of Use](https://www.zoom.com/en/trust/beta-terms-and-conditions/?optimizely_user_id=2a2f4ff424d63a314b7536ade4a8c12d&amp_device_id=5039ff16-4ae8-42dc-bf77-49268ac0d6ff&_ics=1733334737195).

Repository: [github.com/zoom/rtms-mock-server-sample](https://github.com/zoom/rtms-mock-server-sample)

This is a mock server that simulates the WebSocket-based streaming of Zoom's Realtime Media Streams (RTMS). It provides a complete development and testing environment for client-server interactions, including media streaming, signaling, and webhook management.

Documentation: [Realtime Media Streams - Beta Developer Documentation](https://drive.google.com/file/d/1UDfgOisarScdSRx0BwuzNU6dkNjaHXsA/view?usp=sharing)

Video guide: [Testing the RTMS mock server](https://success.zoom.us/clips/share/kTCgY9H3TDGyKabbHELfhg)

## Sample client

A sample client (Express server) is available at `./client` to help test this mock server. The client implements connection handling and provides a user interface for testing different media formats.

Sample client features:

- Webhook endpoint implementation
- WebSocket connection handling
- Media streaming controls
- Incoming real time data logs

## Installation & setup

This app requires [FFmpeg](https://github.com/FFmpeg/FFmpeg) and [Node.js version 14]() or higher.

The app can be run locally by cloning and installing packages with npm or on [Docker](https://www.docker.com/).

**npm** <br/>
To setup with npm, install dependencies and run the app:

```bash
cd rtms-mock-server-sample

# Install dependencies
npm install

# Start the server
npm start
```

**Docker** <br/>
To setup with Docker, run the following:

```bash
cd rtms-mock-server-sample

# Option 1: Using docker-compose (recommended)
docker-compose up -d

# Option 2: Manual docker commands

# Build Docker image
docker build -t rtms-mock-server .

# Run the container
docker run -d \
  -p 9092:9092 \
  -p 8081:8081 \
  -v $(pwd)/data:/app/data \
  --name rtms-mock-server \
  rtms-mock-server

# View logs
docker logs -f rtms-mock-server
```

To stop the container:

```bash
docker stop rtms-mock-server
```

To restart the container:

```bash
docker start rtms-mock-server
```

## Using the sample client

Start the server (npm or Docker) and open the mock server at [http://localhost:9092](http://localhost:9092). The sample client at `./client` can now be used to consume media from the mock server.

In a new terminal, run the sample client:

```bash
node client/server.js
```

This opens up a server at `localhost:8000`. For webhook validation, the client will need to be exposed to the internet with a tunnel, like [ngrok](https://ngrok.com/).

```bash
ngrok http 8000
```

The ngrok URL will be used to validate the webhook endpoint. Copy your URL and paste it into the webhook URL field on the mock server (http://localhost:9092). Click validate. In the RTMS server and client you'll see confirmation of the validation.

You can now start a meeting and start streaming media to the client.

Click _Start Meeting_ and provide camera/microphone permissions. The client will start receiving media. Resume, Stop, and Start RTMS to control the media stream.

Media packets are sent to the client every 100ms. The client will log incoming packets to the console.

## Creating your own client

To start, you'll need to create a webhook receiver to handle incoming `meeting.rtms.started` events when streams are available. You'll also need to [validate the webhook URL](https://developers.zoom.us/docs/api/webhooks/#validate-your-webhook-endpoint). You can find the webhook verification token in [rtms_credentials.json](data/rtms_credentials.json).

The mock server will send the following POST requests to your webhook endpoint:

```json
{
  "event": "endpoint.url_validation",
  "payload": {
    "plainToken": "abc123"
  }
}
```

The webhook endpoint should respond with the following:

```json
{
  "plainToken": "abc123",
  "encryptedToken": "encrypted_token_hash"
}
```

When the webhook is validated and the meeting starts, you'll receive a `meeting.rtms.started` webhook payload.

**Meeting Started Webhook Payload:**

```json
{
  "event": "meeting.rtms.started",
  "payload": {
    "operator_id": "user123",
    "object": {
      "meeting_uuid": "uuid",
      "rtms_stream_id": "stream_id",
      "server_urls": "server_urls"
    }
  }
}
```

#### Handling WebSocket Connections to receive RTMS data

Once you receive the server urls in the webhook payload, you need to open a websocket connection with the server url, and send a handshake request in the following format:

**Handshake Request (Client → Mock Server):**

```json
{
  "msg_type": "SIGNALING_HAND_SHAKE_REQ",
  "protocol_version": 1,
  "meeting_uuid": "meeting_uuid",
  "rtms_stream_id": "stream_id",
  "signature": "hmac_sha256_signature"
}
```

**Note:** The `signature` field should be generated using HMAC-SHA256 with the following pattern:

```
signature = HMAC-SHA256(
    key: client_secret,
    message: client_id + meeting_uuid + rtms_stream_id
)
```

The `client_secret` and other credentials can be found in `data/rtms_credentials.json`.

**Handshake Response (Mock Server → Client):**

The media urls are returned in the handshake response if the handshake is successful:

```json
{
  "msg_type": "SIGNALING_HAND_SHAKE_RESP",
  "status": "STATUS_OK",
  "media_urls": {
    "all": "ws://localhost:8081/all",
    "video": "ws://localhost:8081/video",
    "audio": "ws://localhost:8081/audio",
    "transcript": "ws://localhost:8081/transcript"
  }
}
```

The signaling socket will also send you the following messages:

**Keep-Alive Request (Mock Server → Client):**

```json
{
  "msg_type": "KEEP_ALIVE_REQ",
  "timestamp": 1234567890
}
```

**Keep-Alive Response (Client → Mock Server):**

```json
{
  "msg_type": "KEEP_ALIVE_RESP",
  "timestamp": 1234567890
}
```

If you miss three consecutive keep-alive requests, the connection will be closed.

**Session State Update:**

```json
{
  "msg_type": "SESSION_STATE_UPDATE",
  "state": "STARTED", // or "PAUSED", "RESUMED", "STOPPED"
  "stop_reason": "reason",
  "timestamp": 1234567890
}
```

#### Media WebSocket Messages:

When you open a websocket connection wtih the media URLs you need to send the following handshake request:

```json
{
  "msg_type": "DATA_HAND_SHAKE_REQ",
  "protocol_version": 1,
  "meeting_uuid": "meeting_uuid",
  "rtms_stream_id": "stream_id",
  "signature": "hmac_sha256_signature",
  "payload_encryption": false
}
```

Note: The signature is generated using the same method as the signaling handshake.

The mock server will respond with the following message:

```json
{
  "msg_type": "DATA_HAND_SHAKE_RESP",
  "status": "STATUS_OK"
}
```

The media websocket will send you the following messages depending on which media type you are subscribed to:

**Video Data Format:**

```json
{
  "msg_type": "MEDIA_DATA_VIDEO",
  "content": {
    "user_id": 0,
    "data": "base64_encoded_video_frame",
    "timestamp": 1234567890
  }
}
```

**Audio Data Format:**

```json
{
  "msg_type": "MEDIA_DATA_AUDIO",
  "content": {
    "user_id": 0,
    "data": "base64_encoded_audio_chunk",
    "timestamp": 1234567890
  }
}
```

**Transcript Data Format:**

```json
{
  "msg_type": "MEDIA_DATA_TRANSCRIPT",
  "content": {
    "user_id": 0,
    "data": "transcribed text",
    "timestamp": 1234567890
  }
}
```

## License

See LICENSE.md file for details.

## Support

For questions or help needed, join us on the [Realtime Media Streams category](https://devforum.zoom.us/c/rtms) on the Zoom Developer Forum. If you need access to this, pl

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 196 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (31 of 31)

```
.cursorrules
.dockerignore
.gitignore
data/rtms_credentials.json
docker-compose.yml
Dockerfile
license.md
main.js
package.json
public/css/styles.css
public/index.html
public/js/api.js
public/js/audio-processor.js
public/js/config.js
public/js/conversationManager.js
public/js/mediaHandler.js
public/js/uiController.js
public/js/webSocket.js
readme.md
server/config/serverConfig.js
server/constants/messageTypes.js
server/handlers/mediaHandler.js
server/handlers/signalingHandler.js
server/handlers/webhookHandler.js
server/handlers/wsHandler.js
server/setup/serverSetup.js
server/utils/credentialsManager.js
server/utils/mediaUtils.js
server/utils/wsUtils.js
test_client/client_readme.md
test_client/server.js
```

### Dependencies

- package.json: @google/generative-ai@^0.21.0, cors@^2.8.5, dotenv@^16.4.7, express@^4.17.1, form-data@^4.0.2, multer@^1.4.5-lts.1, node-fetch@^2.6.1, openai@^4.85.1, ws@^8.2.3

### Recent commits (newest first)

- Merge pull request #9 from treehacks2025/trasta
- ⚡️ improve
- Merge pull request #8 from treehacks2025/add_avatar_container
- Merge branch 'main' into add_avatar_container
- Merge pull request #7 from treehacks2025/show-summary
- 🐛 rename
- small changes to add stacking video div
- 🐛 fix
- ✨ Add session analysis with Gemini and OpenAI integration
- Merge pull request #6 from treehacks2025/feedback-loop
- 🔧 Update initial conversation state in ConversationManager
- ⚡️ implementation of feedback loop
- fe changes
- Merge pull request #5 from treehacks2025/dashboard
- ✨ add dashboard
- Merge pull request #4 from treehacks2025/conversation
- ✨ add expression tab
- Merge pull request #3 from treehacks2025/conversation
- 🔥 remove unused code
- 🐛 fix

## Key source files (fetched from GitHub, selected and truncated for size)

### license.md

```markdown
Copyright 2025 Zoom Communications, Inc

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

```

### test_client/client_readme.md

```markdown
# Zoom RTMS Client

A client to test the RTMS Mock Server. This client connects to the RTMS Mock Server and sends messages to it.

## Overview

This client:
- Listens for Zoom webhook events
- Handles RTMS connection initialization
- Manages WebSocket connections for both signaling and media data
- Processes real-time media streams from RTMS Mock Server

## Prerequisites

- Node.js (v14 or higher)
- npm
- A Zoom account with RTMS enabled
- RTMS credentials (stored in `data/rtms_credentials.json`)

## Setup

1. Install dependencies:
```

## Using ngrok for Webhook Testing

To receive webhooks from the RTMS Mock Server, your client needs to be publicly accessible. ngrok is a useful tool for this:

1. Install ngrok from [https://ngrok.com/download](https://ngrok.com/download)

2. Start ngrok on port 8000:
```bash
ngrok http 8000
```

3. Copy the generated HTTPS URL (e.g., `https://abc123.ngrok.io`)

4. Use this URL as your webhook endpoint in the RTMS Mock Server UI

Note: The ngrok URL changes each time you restart ngrok unless you have a paid account.

## How It Works

1. The client starts an Express server on port 8000 to receive Zoom webhooks
2. When a meeting (session) is started in RTMS Mock Server:
   - Mock server sends a webhook to the client
   - Client establishes a signaling WebSocket connection
   - After successful handshake, connects to media WebSocket
   - Begins receiving real-time media data



## Limitations

This is a minimal implementation focused on core functionality. For production use, consider adding:
- Data processing
```

### docker-compose.yml

```yaml
version: '3.8'

services:
  rtms-mock:
    build: .
    ports:
      - "9092:9092"  # Handshake server
      - "8081:8081"  # Media server
    volumes:
      - ./data:/usr/src/app/data
    environment:
      - NODE_ENV=production
    restart: unless-stopped 
```

### Dockerfile

```
# Use Node.js LTS version
FROM node:18-slim

# Install FFmpeg for media processing
RUN apt-get update && \
    apt-get install -y ffmpeg && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Create app directory
WORKDIR /usr/src/app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy app source
COPY . .

# Create data directory
RUN mkdir -p data

# Expose ports
EXPOSE 9092 8081

# Start the application
CMD ["node", "main.js"] 
```

### package.json

```
{
  "name": "rtms-mock-server",
  "version": "1.0.0",
  "description": "RTMS Mock Server",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zoom/rtms-mock-server-sample.git"
  },
  "bugs": {
    "url": "https://github.com/zoom/rtms-mock-server-sample/issues"
  },
  "homepage": "https://github.com/zoom/rtms-mock-server-sample#readme",
  "main": "main.js",
  "scripts": {
    "start": "node main.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@google/generative-ai": "^0.21.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.17.1",
    "form-data": "^4.0.2",
    "multer": "^1.4.5-lts.1",
    "node-fetch": "^2.6.1",
    "openai": "^4.85.1",
    "ws": "^8.2.3"
  }
}

```

### main.js

```javascript
const ServerSetup = require('./server/setup/serverSetup');
const WSHandler = require('./server/handlers/wsHandler');
const MediaHandler = require('./server/handlers/mediaHandler');
const CONFIG = require('./server/config/serverConfig');
const express = require('express');
const webhookRouter = require("./server/handlers/webhookHandler");

// Initialize global state
global.isHandshakeServerActive = false;
global.mediaServer = null;
global.signalingWebsocket = null;
global.wss = null;
global.logsWss = null;

// Setup servers
const handshakeServer = ServerSetup.setupHandshakeServer();
const mediaHttpServer = ServerSetup.setupMediaServer();

// Setup WebSocket servers
global.wss = WSHandler.setupWebSocketServer(handshakeServer);
global.isHandshakeServerActive = true;

// Setup media server
function initializeMediaServer() {
    if (!global.mediaServer || global.mediaServer.isClosed) {
        global.mediaServer = MediaHandler.setupMediaServer(mediaHttpServer);
        console.log("Media server initialized");
    }
    return global.mediaServer;
}

// Initial media server setup
initializeMediaServer();

// Handle WebSocket upgrade requests
handshakeServer.on("upgrade", (request, socket, head) => {
    // Ensure media server is initialized before handling upgrade
    initializeMediaServer();
    WSHandler.handleUpgrade(request, socket, head);
});

// Add webhook router
const app = require('express')();
app.use("/", webhookRouter);

console.log("Starting WSS servers...");
```

### test_client/server.js

```javascript
const express = require('express');
const crypto = require('crypto');
const WebSocket = require('ws');

const app = express();
const PORT = 8000;

// Middleware to parse incoming JSON payloads
app.use(express.json());

// Configuration - Replace with actual values, test values can be found in data/rtms_credentials.json
const ZOOM_SECRET_TOKEN = 'DyBoLm8OZoJT2Pi3-kY2px'; // Webhook secret for validation 
const CLIENT_SECRET = 'YZnKVUufg7N18Oej6gHHqNWc7CG5jQ6N'; // Secret key for generating HMAC signatures

// Track active connections
const activeConnections = new Map();

/**
 * Function to generate HMAC signature
 * 
 * @param {string} clientId - The client ID of the RTMS application
 * @param {string} meetingUuid - The UUID of the Zoom meeting
 * @param {string} streamId - The RTMS stream ID
 * @param {string} secret - The secret key used for signing
 * @returns {string} HMAC SHA256 signature
 */
function generateSignature(clientId, meetingUuid, streamId, secret) {
    const message = `${clientId},${meetingUuid},${streamId}`;
    return crypto.createHmac("sha256", secret).update(message).digest("hex");
}

/**
 * Webhook endpoint to receive events from Zoom
 */
app.post('/', (req, res) => {
    const { event, payload } = req.body;

    // Handle Zoom Webhook validation
    if (event === 'endpoint.url_validation' && payload?.plainToken) {
        
        console.log('Received URL validation request:', {
            event,
            plainToken: payload.plainToken
        });

        const hashForValidate = crypto.createHmac('sha256', ZOOM_SECRET_TOKEN)
            .update(payload.plainToken)
            .digest('hex');
            
        const response = {
            plainToken: payload.plainToken,
            encryptedToken: hashForValidate
        };
        
        console.log('Sending URL validation response:', response);
        return res.json(response);
    }

    // Handle RTMS start event
    if (payload?.event === 'meeting.rtms.started' && payload?.payload?.object) {
        console.log('Received RTMS start event. Full request body:', JSON.stringify(req.body, null, 2));
        
        const {
            clientId,
            payload: {
                payload: {
                    object: { meeting_uuid, rtms_stream_id, server_urls }
                }
            }
        } = req.body;

        console.log('Extracted RTMS details:', {
            clientId,
            meeting_uuid,
            rtms_stream_id,
            server_urls
        });

        connectToRTMSWebSocket(clientId, meeting_uuid, rtms_stream_id, server_urls);
    }

    res.sendStatus(200);
});

/**
 * Connects to the RTMS signaling WebSocket server
 * 
 * @param {string} clientId - The client ID
 * @param {string} meetingUuid - The meeting UUID
 * @param {string} streamId - The RTMS stream ID
 * @param {string} serverUrl - WebSocket URL for signaling server
 */
function connectToRTMSWebSocket(clientId, meetingUuid, streamId, serverUrl) {
    const connectionId = `${meetingUuid}_${streamId}`;
    
    // Close existing connection if any
    if (activeConnections.has(connectionId)) {
        activeConnections.get(connectionId).close();
        activeConnections.delete(connectionId);
    }

    const ws = new WebSocket(serverUrl, { rejectUnauthorized: false });
    activeConnections.set(connectionId, ws);

    ws.on("open", () => {
        const signature = generateSignature(clientId, meetingUuid, streamId, CLIENT_SECRET);
        const handshakeMessage = {
            msg_type: "SIGNALING_HAND_SHAKE_REQ",
            protocol_version: 1,
            meeting_uuid: meetingUuid,
            rtms_stream_id: streamId,
            signature: signature
        };
        ws.send(JSON.stringify(handshakeMessage));
    });

    ws.on("message", (data) => {
        const message = JSON.parse(data);
        
        // Handle different message types
        switch (message.msg_type) {
            case "SIGNALING_HAND_SHAKE_RESP":
                if (message.status_code === "STATUS_OK") {
                    const mediaServerUrl = message.media_server.server_urls.all;
                    connectToMediaWebSocket(mediaServerUrl, clientId, meetingUuid, streamId);
                }
                break;
            case "STREAM_STATE_UPDATE":
                if (message.state === "TERMINATED") {
                    ws.close();
                    activeConnections.delete(connectionId);
                }
                break;
            case "KEEP_ALIVE_REQ":
                ws.send(JSON.stringify({
                    msg_type: "KEEP_ALIVE_RESP",
                    timestamp: Date.now()
                }));
                break;
        }
    });

    ws.on("close", () => {
        activeConnections.delete(connectionId);
    });
}

/**
 * Connects to the Media WebSocket server
 * 
 * @param {string} endpoint - WebSocket URL for media server
 * @param {string} clientId - The client ID
 * @param {string} meetingUuid - The meeting UUID
 * @param {string} streamId - The RTMS stream ID
 */
function connectToMediaWebSocket(endpoint, clientId, meetingUuid, streamId) {
    const connectionId = `${meetingUuid}_${streamId}_media`;
    
    // Close existing media connection if any
    if (activeConnections.has(connectionId)) {
        activeConnections.get(connectionId).close();
        activeConnections.delete(connectionId);
    }

    const mediaWs = new WebSocket(endpoint, { rejectUnauthorized: false });
    activeConnections.set(connectionId, mediaWs);

    mediaWs.on("open", () => {
        const mediaSignature = generateSignature(clientId, meetingUuid, streamId, CLIENT_SECRET);
        const dataHandshakeMessage = {
            msg_type: "DATA_HAND_SHAKE_REQ",
            protocol_version: 1,
            meeting_uuid: meetingUuid,
            rtms_stream_id: streamId,
            signature: mediaSignature,
            payload_encryption: false
        };
        mediaWs.send(JSON.stringify(dataHandshakeMessage));

[truncated — 557 more characters]
```

### public/index.html

```html
<!doctype html>
<html>
    <head>
        <title>Tea Ritual</title>
        <link rel="stylesheet" href="/css/styles.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    </head>
    <body>
        <div class="container">
            <div class="main-content">
                <div class="meeting-header">
                    <h2 class="meeting-title">Tea Ritual - <span id="remainingTime">03:00</span></h2>
                </div>

                <div class="form-group">
                    <div class="url-container">
                        <label>Provide Webhook URL. To know more about Zoom Webhooks, visit <a href="https://developers.zoom.us/docs/api/webhooks/" target="_blank">here</a>.</label>

                        <input type="text" id="webhookUrl" placeholder="Enter Webhook URL" />
                        <button id="validateBtn">
                            <i class="fas fa-check-circle"></i> Validate
                        </button>
                    </div>
                </div>

                <div class="video-container">
                    <div class="video-wrapper top-video">
                        <div id="remoteVideo">
                            <video autoplay loop muted playsinline>
                                <source src="https://storage.cdn-luma.com/dream_machine/cd8b5c9e-ba0b-497b-8f58-d75d0d232681/85f13215-6ee9-4e18-9e60-e51614129a8c_resultb1936f037a5d83f6.mp4" type="video/mp4">
                            </video>
                        </div>
                        <audio id="remoteAudio" autoplay></audio>
                    </div>
                    <div class="video-wrapper bottom-video">
                        <video id="mediaVideo" autoplay playsinline muted></video>
                        <audio id="mediaAudio" autoplay></audio>
                    </div>
                </div>

                <div class="controls">
                    <button id="sendBtn" disabled>
                        <i class="fas fa-video"></i> Start Ritual
                    </button>
                    <button id="pauseBtn" disabled>
                        <i class="fas fa-pause"></i> Pause RTMS
                    </button>
                    <button id="resumeBtn" disabled>
                        <i class="fas fa-play"></i> Resume RTMS
                    </button>
                    <button id="stopBtn" disabled>
                        <i class="fas fa-stop"></i> Stop RTMS
                    </button>
                    <button id="startRtmsBtn" disabled>
                        <i class="fas fa-play-circle"></i> Start New Ritual
                    </button>
                    <button id="endBtn" disabled>
                        <i class="fas fa-phone-slash"></i> End Ritual
                    </button>
                </div>
            </div>

            <div class="sidebar">
                <div class="log-tabs">
                    <button class="tab-button active" data-tab="transcripts">Transcripts</button>
                    <button class="tab-button" data-tab="logs">Logs</button>
                    <button class="tab-button" data-tab="expressions">Expressions</button>
                    <button class="tab-button" data-tab="dashboard">Dashboard</button>
                </div>

                <div id="transcripts-container" class="logs-container">
                    <div id="transcript"></div>
                </div>

                <div id="logs-container" class="logs-container" style="display: none;">
                    <div id="system-logs"></div>
                </div>

                <div id="expressions-container" class="logs-container" style="display: none;">
                    <div id="expression-states">
                        <div class="expression-card" id="furrowed-state">
                            <div class="expression-icon"><i class="fas fa-meh"></i></div>
                            <div class="expression-title">Furrowed Brow</div>
                            <div class="expression-status"></div>
                            <div class="expression-confidence"></div>
                            <div class="expression-explanation"></div>
                        </div>
                        <div class="expression-card" id="smiling-state">
                            <div class="expression-icon"><i class="fas fa-smile"></i></div>
                            <div class="expression-title">Smiling</div>
                            <div class="expression-status"></div>
                            <div class="expression-confidence"></div>
                            <div class="expression-explanation"></div>
                        </div>
                        <div class="expression-card" id="relaxed-state">
                            <div class="expression-icon"><i class="fas fa-face-smile"></i></div>
                            <div class="expression-title">Relaxed</div>
                            <div class="expression-status"></div>
                            <div class="expression-confidence"></div>
                            <div class="expression-explanation"></div>
                        </div>
                        <div class="expression-card" id="eyesClosed-state">
                            <div class="expression-icon"><i class="fas fa-eye-slash"></i></div>
                            <div class="expression-title">Eyes Closed</div>
                            <div class="expression-status"></div>
                            <div class="expression-confidence"></div>
                            <div class="expression-explanation"></div>
                        </div>
                        <div class="expression-card" id="eyesFocused-state">
                            <div class="expression-icon"><i class="fas fa-eye"></i></div>
                            <div class="expression-title">Eyes Focused</div>
                            <div cla
[truncated — 9081 more characters]
```

### server/config/serverConfig.js

```javascript
module.exports = {
    HANDSHAKE_PORT: 9092,
    MEDIA_PORT: 8081,
    HOST: "0.0.0.0",
    ENDPOINTS: {
        AUDIO: "/audio",
        VIDEO: "/video",
        TRANSCRIPT: "/transcript",
        ALL: "/all"
    },
    MEDIA: {
        CHUNK_SIZE: 4096,
        INTERVAL_MS: 100
    },
    KEEP_ALIVE_INTERVAL: 5000,
    DIRECTORIES: {
        DATA: "data",
        PCM: "data"
    }
}; 
```

### public/js/config.js

```javascript
const CONFIG = {
    STATES: {
        STARTED: "STARTED",
        PAUSED: "PAUSED",
        RESUMED: "RESUMED",
        STOPPED: "STOPPED"
    },
    MEDIA: {
        VIDEO_CONFIG: {
            mimeType: 'video/webm',
            videoBitsPerSecond: 1000000
        },
        AUDIO_CONFIG: {
            mimeType: 'audio/webm',
            audioBitsPerSecond: 128000
        }
    },
    WS_ENDPOINTS: {
        DEFAULT_PORT: 8081,
        DEFAULT_HOST: '0.0.0.0'
    },
};

// Global state
window.RTMSState = {
    mediaSocket: null,
    mediaStream: null,
    sessionState: CONFIG.STATES.STOPPED,
    isStreamingEnabled: true,
    videoRecorder: null,
    audioRecorder: null,
    recognition: null
}; 
```

[16 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]