Compare commits

...

7 commits

Author SHA1 Message Date
Andrew Kelley
091a24ee9d derping around. ugh 2014-04-01 23:28:07 -07:00
Andrew Kelley
1631fdd10f refactor encoding to be always ahead of playhead 2014-04-01 10:40:21 -07:00
Andrew Kelley
4e1c926cd5 fix time keeping when no streamers or encoders attached 2014-03-31 22:38:20 -07:00
Andrew Kelley
a9a4796922 debug stuff 2014-03-31 18:34:13 -07:00
Andrew Kelley
54fd53000b more reliable headless playback 2014-03-31 18:34:13 -07:00
Andrew Kelley
259526c82b add debug thing 2014-03-31 18:34:13 -07:00
Andrew Kelley
c5f31269d2 ability to attach and detach encoder and player.
* admin option to disable and enable hardware playback. Closes #129
 * only attach encoder when streamers are connected. Closes #143
2014-03-31 18:34:13 -07:00
6 changed files with 345 additions and 126 deletions

View file

@ -58,7 +58,7 @@ var NEXT_FILE_COUNT = OPEN_FILE_COUNT - PREV_FILE_COUNT;
// when a streaming client connects we send them many buffers quickly
// in order to get the stream started, then we slow down.
var instantBufferBytes = 220 * 1024;
var ENCODE_QUEUE_DURATION = 5;
var DB_SCALE = Math.log(10.0) * 0.05;
var REPLAYGAIN_PREAMP = 0.75;
@ -90,9 +90,6 @@ function Player(db, musicDirectory) {
maxAsync: 1,
});
this.groovePlayer = null; // initialized by initialize method
this.groovePlaylist = null; // initialized by initialize method
this.playlist = {};
this.currentTrack = null;
this.tracksInOrder = []; // another way to look at playlist
@ -101,6 +98,7 @@ function Player(db, musicDirectory) {
this.invalidPaths = {}; // files that could not be opened
this.repeat = Player.REPEAT_OFF;
this.hardwarePlayback = null;
this.isPlaying = false;
this.trackStartDate = null;
this.pausedTime = 0;
@ -115,21 +113,29 @@ function Player(db, musicDirectory) {
this.headerBuffers = [];
this.recentBuffers = [];
this.recentBuffersByteCount = 0;
this.newHeaderBuffers = [];
this.openStreamers = [];
this.lastEncodeItem = null;
this.lastEncodePos = null;
this.expectHeaders = true;
this.groovePlaylist = groove.createPlaylist();
this.groovePlayer = groove.createPlayer();
this.grooveEncoder = groove.createEncoder();
this.manualTimeInterval = null;
this.pendingEncoderAttachDetach = null;
this.desiredEncoderAttachState = false;
this.flushEncodedInterval = null;
this.groovePlaylist.pause();
this.volume = this.groovePlaylist.volume;
this.grooveEncoder.formatShortName = "mp3";
this.grooveEncoder.codecShortName = "mp3";
this.grooveEncoder.bitRate = 256 * 1000;
}
Player.prototype.initialize = function(cb) {
var self = this;
var pend = new Pend();
pend.go(initPlayer);
pend.go(initLibrary);
pend.wait(function(err) {
initLibrary(function(err) {
if (err) return cb(err);
self.requestUpdateDb();
playlistChanged(self);
@ -137,102 +143,6 @@ Player.prototype.initialize = function(cb) {
cacheAllOptions(cb);
});
function initPlayer(cb) {
var groovePlaylist = groove.createPlaylist();
var groovePlayer = groove.createPlayer();
var grooveEncoder = groove.createEncoder();
grooveEncoder.formatShortName = "mp3";
grooveEncoder.codecShortName = "mp3";
grooveEncoder.bitRate = 256 * 1000;
var pend = new Pend();
pend.go(function(cb) {
groovePlayer.attach(groovePlaylist, cb);
});
pend.go(function(cb) {
grooveEncoder.attach(groovePlaylist, cb);
});
pend.wait(doneAttaching);
function doneAttaching(err) {
if (err) {
cb(err);
return;
}
self.groovePlaylist = groovePlaylist;
self.groovePlayer = groovePlayer;
self.grooveEncoder = grooveEncoder;
self.groovePlaylist.pause();
self.volume = self.groovePlaylist.volume;
self.groovePlayer.on('nowplaying', onNowPlaying);
self.flushEncodedInterval = setInterval(flushEncoded, 10);
cb();
function flushEncoded() {
// poll the encoder for more buffers until either there are no buffers
// available or we get enough buffered
while (1) {
var bufferedSeconds = self.secondsIntoFuture(self.lastEncodeItem, self.lastEncodePos);
if (bufferedSeconds > 0.5 && self.recentBuffersByteCount >= instantBufferBytes) return;
var buf = self.grooveEncoder.getBuffer();
if (!buf) return;
if (buf.buffer) {
if (buf.item) {
if (self.expectHeaders) {
console.log("encoder: got first non-header");
self.headerBuffers = self.newHeaderBuffers;
self.newHeaderBuffers = [];
self.expectHeaders = false;
}
self.recentBuffers.push(buf.buffer);
self.recentBuffersByteCount += buf.buffer.length;
while (self.recentBuffers.length > 0 &&
self.recentBuffersByteCount - self.recentBuffers[0].length >= instantBufferBytes)
{
self.recentBuffersByteCount -= self.recentBuffers.shift().length;
}
for (var i = 0; i < self.openStreamers.length; i += 1) {
self.openStreamers[i].write(buf.buffer);
}
self.lastEncodeItem = buf.item;
self.lastEncodePos = buf.pos;
} else if (self.expectHeaders) {
// this is a header
console.log("encoder: got header");
self.newHeaderBuffers.push(buf.buffer);
} else {
// it's a footer, ignore the fuck out of it
console.info("ignoring encoded audio footer");
}
} else {
// end of playlist sentinel
console.log("encoder: end of playlist sentinel");
self.expectHeaders = true;
}
}
}
function onNowPlaying() {
var playHead = self.groovePlayer.position();
var decodeHead = self.groovePlaylist.position();
if (playHead.item) {
var nowMs = (new Date()).getTime();
var posMs = playHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[playHead.item.id];
playlistChanged(self);
self.emit('currentTrack');
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("end of playlist");
self.currentTrack = null;
playlistChanged(self);
self.emit('currentTrack');
}
}
}
}
function initLibrary(cb) {
var pend = new Pend();
pend.go(cacheAllDb);
@ -271,6 +181,7 @@ Player.prototype.initialize = function(cb) {
dynamicModeOn: null,
dynamicModeHistorySize: null,
dynamicModeFutureSize: null,
hardwarePlayback: null,
};
var pend = new Pend();
for (var name in options) {
@ -290,6 +201,12 @@ Player.prototype.initialize = function(cb) {
if (options.dynamicModeFutureSize != null) {
self.setDynamicModeFutureSize(options.dynamicModeFutureSize);
}
var hardwarePlaybackValue = options.hardwarePlayback == null ? true : options.hardwarePlayback;
if (hardwarePlaybackValue) {
self.setHardwarePlayback(hardwarePlaybackValue);
} else {
self.refreshManualTimeInterval();
}
cb();
});
@ -374,6 +291,181 @@ Player.prototype.initialize = function(cb) {
}
};
function startEncoderAttach(self, cb) {
self.desiredEncoderAttachState = true;
if (!self.pendingEncoderAttachDetach) {
self.pendingEncoderAttachDetach = true;
self.grooveEncoder.attach(self.groovePlaylist, function(err) {
if (err) return cb(err);
self.pendingEncoderAttachDetach = false;
if (!self.desiredEncoderAttachState) startEncoderDetach(self, cb);
});
}
}
function startEncoderDetach(self, cb) {
self.desiredEncoderAttachState = false;
if (!self.pendingEncoderAttachDetach) {
self.pendingEncoderAttachDetach = true;
self.grooveEncoder.detach(function(err) {
if (err) return cb(err);
self.pendingEncoderAttachDetach = false;
if (self.desiredEncoderAttachState) startEncoderAttach(self, cb);
});
}
}
Player.prototype.refreshManualTimeInterval = function() {
var self = this;
var wantManualTime = !self.hardwarePlayback && !self.desiredEncoderAttachState;
if (wantManualTime && !self.manualTimeInterval) {
self.manualTimeInterval = setInterval(checkNowPlaying, 100);
} else if (!wantManualTime && self.manualTimeInterval) {
clearInterval(self.manualTimeInterval);
self.manualTimeInterval = null;
}
function checkNowPlaying() {
if (!self.currentTrack) return;
if (!self.isPlaying) return;
var now = new Date();
var dbFile = self.libraryIndex.trackTable[self.currentTrack.key];
var nextTrackBegin = new Date(self.trackStartDate.getTime() + dbFile.duration * 1000);
if (now > nextTrackBegin) {
self.currentTrack = self.tracksInOrder[self.currentTrack.index + 1];
self.trackStartDate = nextTrackBegin;
playlistChanged(self);
self.emit('currentTrack');
}
}
};
Player.prototype.getBufferedSeconds = function() {
if (this.recentBuffers.length < 2) return 0;
var curBuf = this.recentBuffers[0];
var curSongId = curBuf.item.id;
var prevBuf = curBuf;
var totalTime = 0;
for (var i = 1; i < this.recentBuffers.length - 1; i += 1) {
var buf = this.recentBuffers[i];
var thisSongId = buf.item.id;
if (thisSongId !== curSongId) {
curSongId = thisSongId;
totalTime += prevBuf.pos - curBuf.pos;
curBuf = buf;
}
prevBuf = buf;
}
totalTime += prevBuf.pos - curBuf.pos;
return totalTime;
};
Player.prototype.attachEncoder = function(cb) {
var self = this;
cb = cb || logIfError;
if (self.flushEncodedInterval) return cb();
console.info("first streamer connected - attaching encoder");
self.flushEncodedInterval = setInterval(flushEncoded, 20);
self.grooveEncoder.on('buffer', onBuffer);
self.refreshManualTimeInterval();
startEncoderAttach(self, cb);
function onBuffer() {
if (!self.desiredEncoderAttachState) return;
if (self.hardwarePlayback) return;
var encodeHead = self.grooveEncoder.position();
var decodeHead = self.groovePlaylist.position();
var prevCurrentTrack = self.currentTrack;
if (encodeHead.item) {
var nowMs = (new Date()).getTime();
var posMs = encodeHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[encodeHead.item.id];
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("encoder: end of playlist");
self.currentTrack = null;
}
if (prevCurrentTrack !== self.currentTrack) {
playlistChanged(self);
self.emit('currentTrack');
}
}
function flushEncoded() {
// get rid of old items
var buf;
while (buf = self.recentBuffers[0]) {
var thisBufTrack = self.grooveItems[buf.item.id];
if (!thisBufTrack) return;
if (thisBufTrack !== self.currentTrack || self.getCurPos() > buf.pos) {
self.recentBuffers.shift();
} else {
break;
}
}
// poll the encoder for more buffers until either there are no buffers
// available or we get enough buffered
while (self.getBufferedSeconds() < ENCODE_QUEUE_DURATION) {
buf = self.grooveEncoder.getBuffer();
if (!buf) break;
if (buf.buffer) {
if (buf.item) {
if (self.expectHeaders) {
console.log("encoder: got first non-header");
self.headerBuffers = self.newHeaderBuffers;
self.newHeaderBuffers = [];
self.expectHeaders = false;
}
self.recentBuffers.push(buf);
for (var i = 0; i < self.openStreamers.length; i += 1) {
self.openStreamers[i].write(buf.buffer);
}
} else if (self.expectHeaders) {
// this is a header
console.log("encoder: got header");
self.newHeaderBuffers.push(buf.buffer);
} else {
// it's a footer, ignore the fuck out of it
console.info("ignoring encoded audio footer");
}
} else {
// end of playlist sentinel
console.log("encoder: end of playlist sentinel");
self.expectHeaders = true;
}
}
}
function logIfError(err) {
if (err) {
console.error("Unable to attach encoder:", err.stack);
}
}
};
Player.prototype.detachEncoder = function(cb) {
cb = cb || logIfError;
this.clearEncodedBuffer();
clearInterval(this.flushEncodedInterval);
this.flushEncodedInterval = null;
startEncoderDetach(this, cb);
this.refreshManualTimeInterval();
this.grooveEncoder.removeAllListeners();
function logIfError(err) {
if (err) {
console.error("Unable to attach encoder:", err.stack);
}
}
};
Player.prototype.requestUpdateDb = function(dirName, forceRescan, cb) {
var fullPath = path.resolve(this.musicDirectory, dirName || "");
this.dirScanQueue.add(fullPath, {
@ -536,23 +628,73 @@ Player.prototype.getOrCreateDir = function (dirName, stat) {
return dirEntry;
};
Player.prototype.getCurPos = function() {
return this.isPlaying ?
((new Date() - this.trackStartDate) / 1000.0) : this.pausedTime;
};
Player.prototype.secondsIntoFuture = function(groovePlaylistItem, pos) {
if (!groovePlaylistItem || !pos) {
return 0;
Player.prototype.setHardwarePlayback = function(value, cb) {
var self = this;
cb = cb || logIfError;
value = !!value;
if (value === self.hardwarePlayback) return cb();
self.hardwarePlayback = value;
if (self.hardwarePlayback) {
self.groovePlayer = groove.createPlayer();
self.groovePlayer.attach(self.groovePlaylist, onAttachPlayer);
} else {
self.groovePlayer.detach(onDetachPlayer);
}
var item = this.grooveItems[groovePlaylistItem.id];
function onAttachPlayer(err) {
if (err) {
self.hardwarePlayback = false;
return cb(err);
}
self.refreshManualTimeInterval();
self.groovePlayer.on('nowplaying', onNowPlaying);
self.persistOption('hardwarePlayback', self.hardwarePlayback);
self.emit('hardwarePlayback', self.hardwarePlayback);
cb();
}
if (item === this.currentTrack) {
return pos - this.getCurPos();
function onDetachPlayer(err) {
if (err) {
self.hardwarePlayback = true;
} else {
return pos;
self.refreshManualTimeInterval();
self.persistOption('hardwarePlayback', self.hardwarePlayback);
self.emit('hardwarePlayback', self.hardwarePlayback);
}
cb(err);
}
function logIfError(err) {
if (err) {
console.error("Unable to set hardware playback mode:", err.stack);
}
}
function onNowPlaying() {
var playHead = self.getPlayHead();
var decodeHead = self.groovePlaylist.position();
if (playHead.item) {
var nowMs = (new Date()).getTime();
var posMs = playHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[playHead.item.id];
playlistChanged(self);
self.emit('currentTrack');
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("end of playlist");
self.currentTrack = null;
playlistChanged(self);
self.emit('currentTrack');
}
}
};
@ -572,15 +714,20 @@ Player.prototype.streamMiddleware = function(req, resp, next) {
resp.write(headerBuffer);
});
self.recentBuffers.forEach(function(recentBuffer) {
resp.write(recentBuffer);
resp.write(recentBuffer.buffer);
});
console.log("sent", count, "bytes of headers and", self.recentBuffersByteCount,
"bytes of unthrottled data");
self.attachEncoder();
self.openStreamers.push(resp);
req.on('abort', function() {
req.on('close', function() {
for (var i = 0; i < self.openStreamers.length; i += 1) {
if (self.openStreamers[i] === resp) {
self.openStreamers.splice(i, 1);
if (self.openStreamers.length === 0) {
console.info("last streamer disconnected. detaching encoder");
self.detachEncoder();
} else {
console.info("streamer count:", self.openStreamers.length);
}
break;
}
}
@ -1119,7 +1266,6 @@ Player.prototype.clearEncodedBuffer = function() {
while (this.recentBuffers.length > 0) {
this.recentBuffers.shift();
}
this.recentBuffersByteCount = 0;
};
Player.prototype.getSuggestedPath = function(track, filenameHint) {
@ -1348,6 +1494,16 @@ Player.prototype.checkDynamicMode = function() {
}
};
Player.prototype.getPlayHead = function() {
if (this.hardwarePlayback) {
return this.groovePlayer.position();
} else if (this.desiredEncoderAttachState && !this.pendingEncoderAttachDetach) {
return this.grooveEncoder.position();
} else {
return this.groovePlaylist.position();
}
};
function operatorCompare(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}
@ -1401,12 +1557,29 @@ function lazyReplayGainScanPlaylist(self) {
}
function playlistChanged(self) {
if (self.desiredEncoderAttachState && !self.pendingEncoderAttachDetach && !self.hardwarePlayback) {
var encodeHead = self.grooveEncoder.position();
var decodeHead = self.groovePlaylist.position();
var prevCurrentTrack = self.currentTrack;
if (encodeHead.item) {
var nowMs = (new Date()).getTime();
var posMs = encodeHead.pos * 1000;
self.trackStartDate = new Date(nowMs - posMs);
self.currentTrack = self.grooveItems[encodeHead.item.id];
} else if (!decodeHead.item) {
// both play head and decode head are null. end of playlist.
console.log("encoder: end of playlist");
self.currentTrack = null;
}
if (prevCurrentTrack !== self.currentTrack) {
playlistChanged(self);
self.emit('currentTrack');
}
}
cacheTracksArray(self);
disambiguateSortKeys(self);
self.lastEncodeItem = null;
self.lastEncodePos = null;
if (self.currentTrack) {
self.tracksInOrder.forEach(function(track, index) {
var withinPrev = (self.currentTrack.index - index) <= PREV_FILE_COUNT;
@ -1425,6 +1598,11 @@ function playlistChanged(self) {
self.pausedTime = 0;
}
checkUpdateGroovePlaylist(self);
console.log("Begin Groove Playlist:");
self.groovePlaylist.items().forEach(function(item, index) {
console.log(index, item.file.filename);
});
console.log("End Groove Playlist:");
self.checkDynamicMode();
self.emit('playlistUpdate');
@ -1457,7 +1635,7 @@ function checkUpdateGroovePlaylist(self) {
}
var groovePlaylist = self.groovePlaylist.items();
var playHead = self.groovePlayer.position();
var playHead = self.getPlayHead();
var playHeadItemId = playHead.item && playHead.item.id;
var groovePlIndex = 0;
var grooveItem;

View file

@ -66,6 +66,13 @@ PlayerServer.actions = {
self.player.setDynamicModeFutureSize(size);
},
},
'hardwarePlayback': {
permission: 'admin',
args: 'boolean',
fn: function(self, client, isOn) {
self.player.setHardwarePlayback(isOn);
},
},
'importUrl': {
permission: 'control',
args: 'object',
@ -205,6 +212,7 @@ PlayerServer.prototype.initialize = function() {
self.player.on('repeatUpdate', addSubscription('repeat', getRepeat));
self.player.on('volumeUpdate', addSubscription('volume', getVolume));
self.player.on('playlistUpdate', addSubscription('playlist', serializePlaylist));
self.player.on('hardwarePlayback', addSubscription('hardwarePlayback', getHardwarePlayback));
var onLibraryUpdate = addSubscription('library', serializeLibrary);
self.player.on('addDbTrack', onLibraryUpdate);
@ -257,6 +265,10 @@ PlayerServer.prototype.initialize = function() {
return new Date();
}
function getHardwarePlayback(client) {
return self.player.hardwarePlayback;
}
function getRepeat(client) {
return self.player.repeat;
}

View file

@ -26,7 +26,7 @@
"zfill": "0.0.1",
"requireindex": "^1.1.0",
"mess": "~0.1.1",
"groove": "~1.3.0",
"groove": "^1.3.2",
"osenv": "0.0.3",
"level": "~0.18.0",
"findit": "~1.1.1",

View file

@ -9,6 +9,7 @@ var Socket = require('./socket');
var uuid = require('uuid');
var dynamicModeOn = false;
var hardwarePlaybackOn = false;
var selection = {
ids: {
@ -433,6 +434,7 @@ var $toggleScrobble = $('#toggle-scrobble');
var $shortcuts = $('#shortcuts');
var $playlistMenu = $('#menu-playlist');
var $libraryMenu = $('#menu-library');
var $toggleHardwarePlayback = $('#toggle-hardware-playback');
function saveLocalState(){
localStorage.setItem('state', JSON.stringify(localState));
@ -1960,8 +1962,16 @@ function updateSettingsAuthUi() {
streamUrlDom.setAttribute('href', streaming.getUrl());
}
function updateSettingsAdminUi() {
$toggleHardwarePlayback
.button('option', 'label', hardwarePlaybackOn ? 'On' : 'Off')
.prop('checked', hardwarePlaybackOn)
.button('refresh');
}
function setUpSettingsUi(){
$toggleScrobble.button();
$toggleHardwarePlayback.button();
$lastFmSignOut.button();
$settingsAuthCancel.button();
$settingsAuthSave.button();
@ -1994,6 +2004,11 @@ function setUpSettingsUi(){
socket.send(msg, params);
updateLastFmSettingsUi();
});
$toggleHardwarePlayback.on('click', function(event) {
var value = $(this).prop('checked');
socket.send('hardwarePlayback', value);
updateSettingsAdminUi();
});
$settingsAuthEdit.on('click', function(event) {
settings_ui.auth.show_edit = true;
updateSettingsAuthUi();
@ -2300,6 +2315,10 @@ $document.ready(function(){
});
return;
}
socket.on('hardwarePlayback', function(isOn) {
hardwarePlaybackOn = isOn;
updateSettingsAdminUi();
});
socket.on('LastFmApiKey', updateLastFmApiKey);
socket.on('permissions', function(data){
permissions = data;
@ -2324,6 +2343,7 @@ $document.ready(function(){
});
socket.on('connect', function(){
socket.send('subscribe', {name: 'dynamicModeOn'});
socket.send('subscribe', {name: 'hardwarePlayback'});
sendAuth();
load_status = LoadStatus.GoodToGo;
render();

View file

@ -77,6 +77,8 @@ function updatePlayer() {
stillBuffering = true;
} else {
audio.pause();
audio.src = "";
audio.load();
stillBuffering = false;
}
actuallyStreaming = shouldStream;

View file

@ -112,6 +112,13 @@
</p>
</div>
</div>
<div class="section">
<h1>Admin</h1>
<p>
Hardware audio playback is
<input type="checkbox" id="toggle-hardware-playback"><label for="toggle-hardware-playback">On</label>
</p>
</div>
<div class="section">
<h1>About</h1>
<ul>