Skip to content

DEMO: https://live.andreyponomarev.ru/ Web application for broadcasting live audio and chatting. Conceptually, it is similar to http://mixlr.com but on a smaller scale and intended for self-hosting

Notifications You must be signed in to change notification settings

andreyponomarevru/livestreamer

Repository files navigation

Current Development Status

  • At the moment of writing, React client uses only a fraction of the existing API.

LiveStreamer

About

RU

Многопользовательское веб-приложение для аудио-стриминга и общения в чате. Концептуально, это клон Mixlr, но в меньшем масштабе. Архитектурно, состоит из трёх компонентов: фронт, API и CLI-приложение для стриминга. Аудио-стриминг реализован через HTTP, а чат, интерактивные функции и уведомления — через WebSocket. Чтобы начать стрим, пользователь запускает на своей машине HTTP клиент, который захватывает аудио из ОС и отправляет его на сервер приложения. Другие пользователи, в свою очередь, могут открыть сайт приложения (React.js-клиент) и слушать стрим, общаться в чате, ставить лайки и пользоваться иными интерактивными функциями.

EN

Web application for broadcasting live audio and chatting. Conceptually, it is similar to Mixlr but on a smaller scale.

Suppose you're a dj and you want to broadcast your mix live. All you need to do is to start up this command-line app on your local machine — it captures the live audio output and streams it to the application server, which in turn broadcasts the audio to all connected listeners.

Architecture

                                                        chat over WebSocket + HTTP
            auido stream over HTTPS/1.1           audio stream over HTTP WebSocket
 BROADCASTER -----------------------> || APP SERVER ---------------------------> LISTENERS
(HTTP client)       mp3, 128kbps      ||                   mp3, 128kbps        (React Client)
                                      ||
                              Nginx as reverse proxy
                          translating HTTPS/1.1 to HTTP/2

Stack

  • backend: TypeScript, Node.js (Express.js), PostgreSQL (raw SQL, without ORM), Redis, RabbitMQ
  • frontend: React.js

Documentation

For the Node.js REST API documentation refer to the e2e API tests.

Backend

Node.js REST API ( dir)

Here's a quick overview of features implemented in application server's API:

  • cookie session authentication
  • registration confirmation (by email)
  • forgotten password recovery (by email)
  • soft account deletion
  • RBAC authorization
  • caching of SQL queries (using Redis)
  • rate-limiting
  • cursor pagination
  • chat (over HTTP + WebSocket)

Database (/postgres dir)

Application main database, PostgreSQL.

All tables are created and seeded via migrations files in api/migrations.

WARNING The schema demonstrated on this figure is a bit outdated: I have added/renamed some columns and tables. I left it here just to provide a general idea of how the data is structured.

Secondary database / cache (/redis dir)

Redis is used for storing frequently updated real-time data:

  • caching API requests
  • storing live stream likes, stream metadata
  • storing authenticated user sessions

Message Queue

RabbitMQ queues some tasks to be performed on user sign up (e.g. sending sign up confirmation email)

HTTP Server (/nginx dir)

Nginx is used as reverse proxy in front of the App server.

Besides its main purpose — to proxy HTTP and WebSocket traffic — it is also configured as a rate limiter.

IMPORTANT Nginx is nod dockerized. It is deployed manually.

Frontend (/frontend dir)

React.js app.

WARNING Currently, haven't included this part of the app into the CI/CD pipeline.

Some screenshots (just to give you an idea of how the app looks in different states):

Streaming client (/streaming-client dir)

Command-line Source Client (HTTP client + audio broadcaster) for LiveStreamer app. Use it to broadcast live audio to application server.

The client app consists of two parts:

  • child process capturing all OS audio
  • HTTP client, sending the captured audio to the server in real-time.

To authenticate to the application server, the client uses regular cookie authentication.

Requirements

This command-line app requires two environment variables containing your username and password: BROADCASTER_USERNAME and BROADCASTER_PASSWORD.

How to use

  1. First, log in to the application server: node build/index.js login:prod

  2. Now you can start broadcasting audio. Issue the following command:

    node build/index.js stream:prod
    
    # You can pass additional 'save' option to write the stream to disk while you're streaming:
    node build/index.js stream:prod save
  3. After the stream is finished, log out: node build/index.js logout:prod

    If you have passed the save option in step 2, you will find the .wav file of the recorded stream in the recordings directory.

How it works

High-level overview

The app involves three parties: streaming client (aka broadcaster), Application Server and consuming client (aka listener(s)):

  • source HTTP client (aka broadcaster) (the app and its documentation are in the separate repo) — it captures the audio output from OS and streams it to the app server using regular progressive HTTP streaming
  • application server (this repo) — serves as audio streaming and chat server. It provides REST API used by both Source Client app and React.js client app. App server takes the incoming audio stream and passes it through to listeners.
  • consuming clients (aka listeners). React.js client-side app.

Low-level overview

The application server is implemented as REST API and provides two main features of the app: audio broadcasting and chat.

  1. To start streaming, the broadcaster should start the command line Source Client app and log in to the application server. Authentication is implemented using a cookie session (stored in Redis).
  2. When the broadcaster starts streaming, the Source Client app sends chunked audio stream in PUT request to /stream endpoint. Application server stores all live stream data (listener count, likes, etc.) in Redis.
  3. Application server detects the start of the stream and sends a notification to the frontend over WebSocket.
  4. On the client side, React receives WebSocket notification and switches into "LIVE" mode, displaying the stream status, timer, number of listeners online, and other data. When the user clicks the 'play' button, React fetches live audio using GET /stream.
  5. During the stream, listeners can "like" the broadcast showing that they like the music by clicking the 'heart' button. After clicking, the button becomes inactive for 10 seconds. The API endpoint allowing to "like" the broadcast is rate limited, so if the client attempts to trick the app by sending multiple "like" requests directly to the API endpoint, Nginx will ban the client's IP for some time.
  6. After the broadcast is finished, all stream data is saved from Redis to PostgreSQL. By default, the finished stream is hidden — it is not visible to listeners; only the user with broadcaster's permissions can log in and publish (make visible to everyone) the finished broadcasts. Broadcaster can also edit/update title, description, links, and other metadata of past broadcasts.
  7. Broadcaster can schedule new broadcasts (this feature is currently supported only by API; React client doesn't provide an interface for this). To do this, the client should send the scheduled broadcast's title and start/end date, and time

All chat functionality, notifications as well as other real-time features are implemented over WebSocket + HTTP. Technically it is possible to implement everything solely over WebSocket, but it would end up in a pretty chaotic and unreliable client-server communication. So, to make the interaction more organized, I utilize both WebSocket and HTTP. For instance, this is how I implemented the chat:

  1. Client sends a chat message to REST API

  2. API saves the message to the database and returns 200 response to the sender.

  3. Then API broadcasts the saved message to all connected clients (except sender) over WebSocket.

    Thus we get the benefits of REST architecture and Websocket protocol at the same time. While WebSocket allows us to do everything in real-time, REST provides the structure and order in client-server communication.

Deployment

WARNING This section is deprecated. I replaced the manual deployment with Bash and the only thing left is to set up the Workflow running these scripts in GitHub Actions. This is on my TODO list.

Frontend

To build and deploy the app to production env, run build-and-deploy.sh script located in livestreamer-frontend directory.

It will start Compose with client container with all production environment variables, compile the code, stop the container and upload the compiled code to the server (/var/www/...) where the app will be served by Nginx.

Backend

  1. SSH into VPS

  2. Pull new image

    # 'api' is the container name
    docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull api
  3. Restart the container. If you need to restart only the containers whose image was updated (as we usually do) add the --no-deps flag.

    docker-compose \
      -f docker-compose.yml \
      -f docker-compose.prod.yml \
      up
        --force-recreate \
        --build
        # + optional `--no-deps` and `-d`
    • Without --force-recreate Compose will use the old image
    • Without -d flag Compose will run in "attached mode" outputing everything to console.

      By default, Docker runs the container in attached mode. In the attached mode, Docker can start the process in the container and attach the console to the process’s standard input, standard output, and standard error (source) So later when we will be automating the deployment with GitHub Actions, always use -d everywhere to detach the terminal from Compose process stdout/stderr. When we write any deployment Bash scripts we should use the '-d' flag as well.

  4. Delete old image to free up the disk space

    docker image prune -f

Testing

  • To run unit tests for the API (/api):
    • in dev env:
      cd api
      docker compose --file docker-compose.dev.yml up api-dev
      docker exec -it api-dev bash
      npm run test:unit
    • in test env: ./ci-cd/run-unit-tests.sh (normally, this should be executed only in CD pipeline but you can use it locally, before pushing the code, to double check that everything works).
  • To run integration tests:
    • in dev env:
      cd api
      docker compose --file docker-compose.dev.yml up
      docker exec -it api-dev bash
      npm run test:int
      ```.
    • in test env: start all containers > ./ci-cd/run-int-test.sh (normally, this should be executed only in CD pipeline but you can use it locally, before pushing the code, to double check that everything works). In both environments, while inside the container you can also run migrations, e.g. npm run migratesql up
  • To run e2e API test:
    • in dev env:
      cd api
      docker compose --file docker-compose.dev.yml up
      docker exec -it api-dev bash
      npm run test:e2e-api
      ```.
    • in test env: start all containers > ./ci-cd/run-int-test.sh (normally, this should be executed only in CD pipeline but you can use it locally, before pushing the code, to double check that everything works). In both environments, while inside the container you can also run migrations, e.g. npm run migratesql up

About

DEMO: https://live.andreyponomarev.ru/ Web application for broadcasting live audio and chatting. Conceptually, it is similar to http://mixlr.com but on a smaller scale and intended for self-hosting

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published