Skip to content

About

A real-time audio room platform featuring live session recording, server-side video pre-rendering, and automated email delivery of downloadable video recordings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

TalkCircle

A two-person video podcast recorder: each participant records locally in the browser, chunks upload straight to S3, and the server stitches both sides into one side-by-side MP4 that is emailed to you.

What it does

  • 1:1 video rooms ("Pod Cells"). Join with a room ID and a name. Two people connect peer to peer over WebRTC, with mic and camera toggles and a synchronized "end call".
  • Local, synchronized recording. Either participant can start recording. The server picks a shared start time (about 5 s ahead) and a session ID, and both browsers start their MediaRecorder at that moment.
  • Background chunk upload. Every 5 s the recorder produces a WebM chunk, which is uploaded directly from the browser to S3 using a pre-signed URL.
  • Recordings page. Enter a session ID to:
    • list and play/download every raw chunk per participant, or
    • request a pre-rendered video: the server merges both recordings side by side and emails you the MP4 link (or a failure email).
  • Automatic cleanup. A cron job deletes raw chunks and merged videos from S3 after 24 hours.

How it works

flowchart LR
  A["Browser A<br/>React + MediaRecorder"]
  B["Browser B<br/>React + MediaRecorder"]
  S["Express + Socket.IO server"]
  S3[("S3 video-recordings/")]
  M[("S3 merged-videos/")]
  E["User inbox"]
  A <-->|"WebRTC media, P2P"| B
  A <-->|"signaling + pre-signed URLs"| S
  B <-->|"signaling + pre-signed URLs"| S
  A -->|"PUT 5 s WebM chunks"| S3
  B -->|"PUT 5 s WebM chunks"| S3
  S -->|"download chunks, FFmpeg concat + hstack + amix"| M
  S -->|"Nodemailer link"| E
Loading

Key engineering decisions

  • Record locally, not from the live stream. The live call quality depends on the network, but the recording does not: each side records its own camera and mic at full quality. This is the core idea behind the app.
  • Direct-to-S3 uploads with pre-signed URLs. The server never proxies video bytes during the call. Over Socket.IO it signs a PutObject URL (valid 10 minutes) for video-recordings/<sessionId>/<userName>/chunk-<index>.webm, and the browser PUTs the chunk itself.
  • Server-side clock for sync. prepare-for-recording carries a start timestamp, and the server broadcasts start-recording-at with one session ID to both peers, so both recordings begin together and share a storage prefix.
  • Async render pipeline. getCompleteLayoutVideo responds immediately ("you will receive the link by email") and does the heavy work in setImmediate:
    1. list the session's chunks in S3 and require exactly 2 participants
    2. download each participant's chunks
    3. concatenate them and re-encode to VP8/Opus WebM (fluent-ffmpeg with ffmpeg-static)
    4. scale both to 640x720, hstack them, mix the two audio tracks with amix, and encode H.264/AAC MP4
    5. upload to merged-videos/ and email the link
  • In-memory room state. UserManager and RoomManager keep rooms in a Map with a hard cap of 2 users. The first user gets send-offer once the second user joins.

Tech stack

Layer Tools
Client React 19, TypeScript, Vite, Redux Toolkit, React Router 7, Tailwind CSS 4, socket.io-client, react-hot-toast
Real time WebRTC (RTCPeerConnection, public STUN servers), Socket.IO 4
Server Node.js, Express 5, TypeScript
Media MediaRecorder (browser), FFmpeg via fluent-ffmpeg + ffmpeg-static
Storage / mail AWS S3 (@aws-sdk/client-s3, s3-request-presigner), Nodemailer, node-cron

Project structure

TalkCircle/
├── client/src/
│   ├── components/        Home, Room (WebRTC + recording), Recordings, About, NavBar
│   ├── utilities/         MediaRecorder.ts (chunked recorder), S3uploader.ts (pre-signed upload)
│   ├── services/          APIs.tsx (SERVER_URL + endpoints), APIcall.tsx (axios wrapper)
│   └── reducers/          Redux store (user name, room ID)
└── server/src/
    ├── index.ts           Express app, Socket.IO, cleanup cron
    ├── managers/          UserManager (socket events), RoomManager (rooms, signaling relay)
    ├── controllers/       videoController.ts (list chunks, render, cleanup)
    ├── routes/            videoRoutes.ts
    ├── utilities/         S3 upload/sign, chunk download, FFmpeg concat + layout, mail
    ├── mailTemplates/     success and failure emails
    └── config/S3Client.ts

Local setup

Prerequisites: Node.js, an S3 bucket, and SMTP credentials supported by Nodemailer. FFmpeg is bundled through ffmpeg-static.

Server (listens on port 3000):

cd server
npm install
npm run dev      # tsc -b, then node ./dist/index.js

Create server/.env:

AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_BUCKET_NAME=
MAIL_HOST=          # Nodemailer "service" name, e.g. gmail
MAIL_USER=
MAIL_PASS=
SERVER_BASE_URL=http://localhost:3000   # used by the cleanup cron to call itself
SECRET_KEY=                             # shared secret for the cleanup endpoint

The bucket needs a CORS rule that allows PUT from the client origin (browsers upload directly). Chunk and video links are built as public https://<bucket>.s3.<region>.amazonaws.com/<key> URLs, so objects must be readable at those URLs for playback and for the render step.

Client (Vite, default http://localhost:5173):

cd client
npm install
npm run dev

Point the client at your server by editing SERVER_URL in client/src/services/APIs.tsx (for example http://localhost:3000).

Other scripts: npm run build, npm run lint, npm run preview (client); npm run build, npm start (server).

API overview

REST (prefix /api, all POST with a JSON body)

Endpoint Body Purpose
/getVideoChunksFromSessionId { sessionId } Returns chunk URLs grouped by participant
/getCompleteLayoutVideo { sessionId, emailId } Starts the merge in the background and emails the MP4 link
/deleteOldVideoCronJob { secretKey } Deletes S3 objects older than 24 h (called every 10 minutes by node-cron)

Socket.IO events

Client to server Server to client
join-room, offer, answer, add-ice-candidate set-remote-user-name, send-offer, offer, answer, add-ice-candidate
prepare-for-recording, stop-recording start-recording-at, stop-recording, recording-error
get-presigned-url (with ack callback)
request-end-call confirm-end-call

What I'd improve next

  • Move rendering to a job queue. The FFmpeg pipeline runs inside the API process, and downloaded chunks in chunksStorage/ are never cleaned up. A worker with a queue (and temp-file cleanup) would make it robust.
  • Know when uploads are finished. Users are asked to wait about 5 minutes before ending a call. Tracking acknowledged chunks per session would remove the guesswork.
  • Configuration. Read SERVER_URL and the server port from environment variables, and restrict CORS (currently *) to the client origin.
  • Access control on recordings. Anyone with a session ID can list chunks or request a render. Private buckets with pre-signed GET links would be safer.
  • Consistent retention. The Recordings page says raw chunks are deleted after 12 hours, but the cleanup job uses a 24-hour threshold for everything.
  • More than two participants, TURN server support for strict NATs, and automated tests.

About

A real-time audio room platform featuring live session recording, server-side video pre-rendering, and automated email delivery of downloadable video recordings.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages