mfink mfink - 3 months ago 80
Node.js Question

How to capture the first 10 seconds of an mp3 being streamed over HTTP

disclaimer: newbie to nodeJS and audio parsing

I'm trying to proxy a digital radio stream through an expressJS app with the help of node-icecast which works great. I am getting the radio's mp3 stream, and via node-lame decoding the mp3 to PCM and then sending it to the speakers. All of this just works straight from the github project's readme example:

var lame = require('lame');
var icecast = require('icecast');
var Speaker = require('speaker');

// URL to a known Icecast stream
var url = 'http://firewall.pulsradio.com';

// connect to the remote stream
icecast.get(url, function (res) {

// log the HTTP response headers
console.error(res.headers);

// log any "metadata" events that happen
res.on('metadata', function (metadata) {
var parsed = icecast.parse(metadata);
console.error(parsed);
});

// Let's play the music (assuming MP3 data).
// lame decodes and Speaker sends to speakers!
res.pipe(new lame.Decoder())
.pipe(new Speaker());
});


I'm now trying to setup a service to identify the music using the Doreso API. Problem is I'm working with a stream and don't have the file (and I don't know enough yet about readable and writable streams, and slow learning). I have been looking around for a while at trying to write the stream (ideally to memory) until I had about 10 seconds worth. Then I would pass that portion of audio to my API, however I don't know if that's possible or know where to start with slicing 10 seconds of a stream. I thought possibly trying passing the stream to ffmpeg as it has a -t option for duration, and perhaps that could limit it, however I haven't got that to work yet.

Any suggestions to cut a stream down to 10 seconds would be awesome. Thanks!

Updated: Changed my question as I originally thought I was getting PCM and converting to mp3 ;-) I had it backwards. Now I just want to slice off part of the stream while the stream still feeds the speaker.

Answer

It's not that easy.. but I've managed it this weekend. I would be happy if you guys could point out how to even improve this code. I don't really like the approach of simulating the "end" of a stream. Is there something like "detaching" or "rewiring" parts of a pipe-wiring of streams in node?

First, you should create your very own Writable Stream class which itself creates a lame encoding instance. This writable stream will receive the decoded PCM data.

It works like this:

var stream = require('stream');
var util = require('util');
var fs = require('fs');
var lame = require('lame');
var streamifier = require('streamifier');
var WritableStreamBuffer = require("stream-buffers").WritableStreamBuffer;

var SliceStream = function(lameConfig) {

    stream.Writable.call(this);

    this.encoder = new lame.Encoder(lameConfig);

    // we need a stream buffer to buffer the PCM data
    this.buffer = new WritableStreamBuffer({
        initialSize: (1000 * 1024),      // start as 1 MiB.
        incrementAmount: (150 * 1024)    // grow by 150 KiB each time buffer overflows.
    });
};

util.inherits(SliceStream, stream.Writable);

// some attributes, initialization
SliceStream.prototype.writable = true;
SliceStream.prototype.encoder = null;
SliceStream.prototype.buffer = null;

// will be called each time the decoded steam emits "data" 
// together with a bunch of binary data as Buffer
SliceStream.prototype.write = function(buf) {

    //console.log('bytes recv: ', buf.length);

    this.buffer.write(buf);

    //console.log('buffer size: ', this.buffer.size());
};

// this method will invoke when the setTimeout function
// emits the simulated "end" event. Lets encode to MP3 again...
SliceStream.prototype.end = function(buf) {

    if (arguments.length) {
        this.buffer.write(buf);
    }
    this.writable = false;

    //console.log('buffer size: ' + this.buffer.size());

    // fetch binary data from buffer
    var PCMBuffer = this.buffer.getContents();

    // create a stream out of the binary buffer data
    streamifier.createReadStream(PCMBuffer).pipe(

        // and pipe it right into the MP3 encoder...
        this.encoder
    );

    // but dont forget to pipe the encoders output 
    // into a writable file stream
    this.encoder.pipe(
        fs.createWriteStream('./fooBar.mp3')
    );
};

Now you can pipe the decoded stream into an instance of your SliceStream class, like this (additional to the other pipes):

icecast.get(streamUrl, function(res) {

    var lameEncoderConfig = {
        // input
        channels: 2,        // 2 channels (left and right)
        bitDepth: 16,       // 16-bit samples
        sampleRate: 44100,   // 44,100 Hz sample rate

        // output
        bitRate: 320,
        outSampleRate: 44100,
        mode: lame.STEREO // STEREO (default), JOINTSTEREO, DUALCHANNEL or MONO
    };
    var decodedStream = res.pipe(new lame.Decoder());

    // pipe decoded PCM stream into a SliceStream instance
    decodedStream.pipe(new SliceStream(lameEncoderConfig));

    // now play it...
    decodedStream.pipe(new Speaker());

    setTimeout(function() {

        // after 10 seconds, emulate an end of the stream.
        res.emit('end');

    }, 10 * 1000 /*milliseconds*/)
});
Comments