Skip to content

Instantly share code, notes, and snippets.

@ilyesgouta
Created March 26, 2015 22:16
Show Gist options
  • Save ilyesgouta/800d0f05fd28da0248c9 to your computer and use it in GitHub Desktop.
Save ilyesgouta/800d0f05fd28da0248c9 to your computer and use it in GitHub Desktop.
A node.js snippet to stream media files to a Chromecast device
// A node.js snippet to stream media files to a Chromecast device.
// An ac3 or dts audio stream in matroska containers is transcoded to mp3
// using ffmeg. Seeking using left and right keys won't be available.
var Client = require('castv2-client').Client;
var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
var mdns = require('mdns');
var ffmpeg = require('fluent-ffmpeg');
var mime = require('mime');
var path = require('path');
var send = require('send');
var sprintf = require("sprintf-js").sprintf;
var keypress = require('keypress');
var browser = mdns.createBrowser(mdns.tcp('googlecast'));
var fs = require('fs');
var http = require('http');
var file = process.argv[2];
if (!file) {
console.log('Please specify a media file as an argument.');
return;
}
console.log('media to serve: ' + file);
var Player;
var start;
var current;
var paused;
var httpResponse;
var transcode; // ffmpeg transcoding command
browser.on('serviceUp', function(service) {
console.log('found device "%s" at %s:%d', service.name, service.addresses[0], service.port);
ondeviceup(service.addresses[0]);
browser.stop();
});
browser.start();
keypress(process.stdin);
function ondeviceup(host) {
var client = new Client();
client.connect(host, function() {
console.log('connected, launching app ...');
client.launch(DefaultMediaReceiver, function(err, player) {
var media = {
contentId: 'http://192.168.1.2:8080/content',
contentType: mime.lookup(file),
streamType: 'BUFFERED'
};
console.log('media mime: ' + media.contentType);
var transcodeAudio = false;
// mkv might need transcoding audio (ac3 or dts) to mp3
if (media.contentType == 'video/x-matroska') {
ffmpeg.ffprobe(file, function(err, metadata) {
console.dir(metadata);
if ((metadata.format.nb_streams > 1)
&& (metadata.streams[0].codec_name == 'ac3'
|| metadata.streams[1].codec_name == 'ac3'
|| metadata.streams[0].codec_name == 'dca'
|| metadata.streams[1].codec_name == 'dca')) {
transcodeAudio = true;
console.log('transcoding required -> switching streaming mode to live');
media.streamType = 'LIVE';
}
});
}
transcode = null;
// start the http server
var server = http.createServer(function(request, response) {
if (transcodeAudio) {
httpResponse = response;
transcode = ffmpeg(file)
.videoCodec('copy')
.audioCodec('libmp3lame')
.audioBitrate('192k')
.format('matroska')
.on('error', function(err) {
console.log('an error occurred: ' + err.message);
});
transcode.pipe(response);
} else
send(request, file).pipe(response);
}).listen(8080);
server.on('request', function(request, response) {
var start = 0, end = 0;
var stat = fs.statSync(file);
var range = request.headers['range'];
if (range != null) {
start = parseInt(range.slice(range.indexOf('bytes=') + 6, range.indexOf('-')));
end = parseInt(range.slice(range.indexOf('-') + 1, range.length));
}
if (isNaN(end) || end == 0)
end = stat.size - 1;
response.statusCode = 206;
response.setHeader('Content-Range', 'bytes '
+ start
+ '-'
+ end
+ '/'
+ stat.size);
});
player.on('status', function(status) {
console.log('status broadcast playerState=%s', status.playerState);
});
console.log('app "%s" launched, loading media %s ...', player.session.displayName, media.contentId);
player.load(media, { autoplay: true }, function(err, status) {
console.log('media loaded playerState=%s', status.playerState);
Player = player;
start = new Date();
current = 0;
paused = false;
});
});
});
client.on('error', function(err) {
console.log('Error: %s', err.message);
client.close();
});
}
process.stdin.on('keypress', function (ch, key) {
if (key && key.name == 'space' && Player) {
paused = !paused;
if (paused)
Player.pause();
else
Player.play();
}
if (key && key.name == 'right' && Player) {
var now = new Date();
var diff = now - start;
current = current + diff / 1000 + 10; // +10 sec
start = now;
console.log(sprintf("seeking to: %02d:%02d:%02d",
Math.floor(current / 3600),
Math.floor((current % 3600) / 60),
Math.floor(current % 60) ));
if (transcode) {
transcode.kill();
console.log("restarting trasncoding at " + current + "seconds");
transcode = ffmpeg(file)
.videoCodec('copy')
.audioCodec('libmp3lame')
.audioBitrate('192k')
.format('matroska')
.seekInput(current)
.on('error', function(err) {
console.log('an error occurred: ' + err.message);
});
transcode.pipe(httpResponse);
}
Player.seek(current, function(err, status) {
});
}
if (key && key.name == 'left' && Player) {
var now = new Date();
var diff = now - start;
current = current + diff / 1000 - 10; // -10 sec
start = now;
console.log(sprintf("seeking to: %02d:%02d:%02d",
Math.floor(current / 3600),
Math.floor((current % 3600) / 60),
Math.floor(current % 60) ));
if (transcode) {
transcode.kill();
console.log("restarting trasncoding at " + current + "seconds");
transcode = ffmpeg(file)
.videoCodec('copy')
.audioCodec('libmp3lame')
.audioBitrate('192k')
.format('matroska')
.seekInput(current)
.on('error', function(err) {
console.log('an error occurred: ' + err.message);
});
transcode.pipe(httpResponse);
}
Player.seek(current, function(err, status) {
});
}
if (key && key.ctrl && key.name == 'c')
process.exit();
});
process.stdin.setRawMode(true);
process.stdin.resume();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment