1. Start playback using an external music player, using playlist files. In my case, I use Foobar 2000.
2. Pause the external player early in the song, while it's still buffering.
3. After resuming, at some point playback will stop, and Foobar reports an error along the lines of "Error decoding source file", along with messages about mpeg resync failure.
This was somewhat mitigable by increasing the Foobar buffer size to it's maximum (16MB), and avoiding pausing during the early parts of a song. Once the remaining data all fits in the local buffer, everything's ok.
I used network packet tracers to examine what's happening, and I found that right before the error in decoding, the server sends the same block of bytes twice in a row, which breaks playback.
It turns out that the culprit is the Tomcat Native library, which use native JNI functions to improve performance. It turns out that while improving performance, it also corrupts data along the way. After uninstalling the native library and using regular Tomcat, everything works perfectly.
I'm posting this here in the hopes that anyone else with this problem will find this post and find the solution. Perhaps this should also be documented in the Subsonic instructions? Basically, do not use the Tomcat native library.
At first, I thought it must be a bug in Subsonic, so I dug through the source code for a while. Along the way, I did discover a bug with the RangeOutputStream class, which is used in the streaming process. Basically, the write(byte[] b, int off, int len) method will NOT work properly if off > 0. It just so happens that it's only ever used to write data with an offset of 0, which is why it's ok.
Still, even though it's not currently a problem, I wrote a replacement implementation that works properly. I suggest fixing it in the codebase to avoid problems in the future, to save some frustration if it ever gets used in a way that will break. Here's the method:
- Code: Select all
@Override
public void write(byte[] b, int off, int len) throws IOException {
long bytesToSkip = 0;
if (pos < start) {
// if we haven't reached start yet, skip enough bytes to put us at
// start...
bytesToSkip = start - pos;
// but don't skip more bytes than len, which is the number of bytes
// we're being asked to write.
if (bytesToSkip > len)
bytesToSkip = len;
}
// skip that many bytes, both in our internal position and in the data
// we're being asked to write.
pos += bytesToSkip;
off += bytesToSkip;
len -= bytesToSkip;
long extraBytesAtEnd = 0;
if (pos + len > end) {
// If writing len bytes would put us past end, then there are extra
// bytes that we don't want to write, to keep us from passing end.
extraBytesAtEnd = pos + len - end;
}
// Reduce the number of bytes we write by that amount.
len -= extraBytesAtEnd;
// Write to the underlying stream using our adjusted values for off and
// len.
if (len > 0) {
out.write(b, off, len);
}
// Increment our internal position by len, the bytes actually written,
// plus extraBytesAtEnd, the bytes we didn't write.
pos += len + extraBytesAtEnd;
}
Secondly, I believe that there is an error in StreamController, where it calculates the Content-Length when given a Range in single-file mode:
line 136:
- Code: Select all
contentLength = file.length() - range.getMinimumLong();
I believe that this should be:
- Code: Select all
contentLength = Math.min(range.getMaximumLong(), file.length()) - range.getMinimumLong();
The length of the result will never exceed the size of the range requested.
I hope this helps someone in the future, and I hope you can integrate these changes into the codebase. Thanks!