Building a Radio Server with Node.js

Control Panel

The control panel is the heart of our radio server. We’ll build it using Node.js and the neo-blessed library, which allows us to create a terminal GUI. Our control panel will have four main sections: Playlist, Queue, NowPlaying, and Controls. Each section will be a neo-blessed box, which can be stylized and positioned in the terminal.

const blessed = require('neo-blessed');

class View {
  constructor() {
    this.screen = blessed.screen({
      smartCSR: true,
    });

    this.screen.key(['C-c', 'C-[', 'escape'], () => {
      process.exit(0);
    });
  }

  render() {
    // render entire view
  }
}

Playlist Box

The Playlist box will contain a list of all available songs. We’ll implement a basic class for this, which will be extended by other classes later. The Playlist box will have a scroll method, which allows users to navigate the items with the help of an illusion made by different coloring of blurred and focused items.

class TerminalItemBox extends View {
  constructor(title) {
    super();
    this.box = blessed.box({
      top: 'center',
      left: 'center',
      width: '50%',
      height: '50%',
      content: title,
      border: {
        type: 'line',
      },
      style: {
        fg: 'white',
        bg: 'blue',
        border: {
          fg: '#f0f0f0',
        },
      },
    });

    this.screen.append(this.box);
  }

  scroll() {
    // implement scrolling logic
  }
}

Queue Box

The Queue box will be used for storing the list of songs queued up for streaming. We’ll extend the TerminalItemBox class to implement the Queue box. This class will not only be in charge of the view layer but also contain all the functionalities for streaming and piping data to all the consumers (i.e., clients).

class QueueBox extends TerminalItemBox {
  constructor() {
    super('Queue');
    // implement streaming and piping logic
  }
}

NowPlaying Box

The NowPlaying box will have only one item: the song that is currently played. We’ll implement it by creating an instance and inserting it into the main view.

class NowPlayingBox extends TerminalItemBox {
  constructor() {
    super('Now Playing');
    // implement now playing logic
  }
}

Controls Box

The Controls box will contain a hardcoded list of keyboard keybindings. We’ll stylize it according to our needs, passing config options during instantiation.

class ControlsBox extends TerminalItemBox {
  constructor() {
    super('Controls');
    // implement controls logic
  }
}

Stream Magic

Now that we have our control panel ready, it’s time to implement the streaming logic. We’ll use the Node.js Stream API to achieve one-to-many transfer of data. We’ll create a transform stream (which is both a readable and a writable) and a timer function to slow down the streaming of the chunks.

const { Transform } = require('stream');

class StreamTransformer extends Transform {
  constructor() {
    super({
      transform(chunk, encoding, callback) {
        // implement transform logic
        callback(null, chunk);
      },
    });
  }
}

We’ll use the throttle package from npm to limit the streaming of the chunks to be no faster than the bytes per second we provide. We’ll determine the bitrate for every song that we stream using the @dropb/ffprobe package from npm.

const throttle = require('throttle');
const ffprobe = require('@dropb/ffprobe');

class StreamController {
  constructor() {
    this.stream = new StreamTransformer();
    this.throttle = throttle(1024 * 1024); // 1MB/s
  }

  async startStreaming() {
    const bitrate = await ffprobe.getBitrate('song.mp3');
    this.stream.pipe(this.throttle).pipe(/* client stream */);
  }
}

Server

Finally, we’ll create an HTTP server using Hapi.js. We’ll implement an HTTP endpoint that will register the client as a consumer and add it to our queue’s _sinks map. We’ll also start streaming the data back to the client.

const Hapi = require('@hapi/hapi');

const server = Hapi.server({
  port: 3000,
  host: 'localhost',
});

server.route({
  method: 'GET',
  path: '/stream',
  handler: (request, h) => {
    const streamController = new StreamController();
    streamController.startStreaming();
    return h.response(streamController.stream);
  },
});

server.start();

Our server will also serve static files, providing a handy webpage with some radio controls. The audio element on the webpage will make a request to the endpoint when the page loads.

<!DOCTYPE html>
<html>
  <head>
    <title>Radio Server</title>
  </head>
  <body>
    <audio src="/stream" autoplay></audio>
  </body>
</html>

Leave a Reply