android media player - how to disable range request? (broken audio streaming on Nexus 7)
I have a audio streaming app, which runs a local proxy server. The local proxy server makes a http connection to a internet streaming source, gets and buffers locally the streaming data. Then, inside in the app, I use MediaPlayer to connect to the local proxy server, using the method
mediaPlayer.setDataSource(...); // the url of the local proxy server
Everything was fine (with plenty of Android devices and different OS versions - 1.5...4.0), until Nexus 7 release.
In Nexus 7, the media player refuses to play the source from the local proxy server.
When I took a look at the logs, seems like the MediaPlayer uses range requests internally. My local proxy server doesn't handle that. It returns HTTP/1.0 200 OK and the data. However, the media player doesn't like that and throws an exception:
Caused by: libcore.io.ErrnoException ?:??: W/?(?): [ 07-18 00:08:35.333 4962: 5149 E/radiobee ] ?:??: W/?(?): : sendto failed: ECONNRESET (Connection reset by peer) ?:??: W/?(?): at libcore.io.Posix.sendtoBytes(Native Method) ?:??: W/?(?): at libcore.io.Posix.sendto(Posix.java:146) ?:??: W/?(?): at libcore.io.BlockGuardOs.sendto(BlockGuardOs.java: ?:??: W/?(?): at libcore.io.IoBridge.sendto(IoBridge.java:473) We requested a content range, but server didn't support that. (responded with 200)
According to the http specs, if the server responds with HTTP/1.0 instead 1.1, the client must not fire a range request (1.0 doesn't support that anyway),
also if the server doesn't support the range request, it should be fine, if it responds with 200 OK (and this is what I'm doing), but the MediaPlayer implementation on Nexus 7 doesn't like that.
I took a look at this thread : HTTP: How should I respond to "Range: bytes=" when Range is unsupported?
,where they claim that the response with 200 OK must be good enough, but unfortunately it doesn't help.
I'm not sure if this is a problem with Jelly Bean, or a problem with Nexus 7 implementation specifically, but it's still a problem for me which I have to resolve.
Again, there are NO range requests on plenty other Android devices, using the same app. For some reason these range requests are happening now on Nexus 7. (It may happen on other Android devices as well, but again, never happened to me so far).
Any possible way to disable the range requests for MediaPlayer?
If there are none, can anybody suggest a quick fix for my proxy server logic (what exactly it has to return, if it receive this range request?), without changing my other logic, if possible?
Seems like maybe I have to return something like "HTTP/1.0 206 OK\r\nPartial Content\r\n\r\n", but probably there should be some value at the end of the Partial Content - not sure what is should be this one.
Your help would be appreciated.
We've finally solved this in a clean way. In our case we have full control over the streaming server, but i guess you could do this with a local proxy as well. Since Build.VERSION_CODES.ICE_CREAM_SANDWICH it's possible to set headers for the MediaPlayer object. As our app enables the user to seek inside the audio stream, we've implemented this Range header on the client. If the server responds with the proper headers, the MediaPlayer will not try multimple times to request the stream.
That's what our server headers look like now for the android client:
Content-Type: audio/mpeg Accept-Ranges: bytes Content-Length: XXXX Content-Range: bytes XXX-XXX/XXX Status: 206
The important part is the 206 status code (partial content). If you don't send this header, the android client will try to re-request the source, no matter what.
If your player doesn't allow seeking in the stream, you could simply always set the Range header to 0-some arbitrary large number.
I work on a large scale audio streaming app (that uses a local host HTTP proxy to stream audio to MediaPlayer) and I ran into this issue as soon as I got a JellyBean device in my hands at Google I/O 2012.
When quality testing our application on different devices (and with information received from our automated crash logs and user submitted logs), we noticed that certain MediaPlayer implementations behaved in what I would call an erratic (and sometimes downright psychotic) manner.
Without going into too much detail, this is what I saw: some implementations would make multiple requests (some times 5+) for the same URL. These requests were all slightly different from each other in that each one was for a different byte range (usually for the first or last 128 bytes). The conclusion was that the MediaPlayer was trying to find embedded metadata, then would after some point it would give up and just make a regular non-range request.
That is not what the stock JellyBean MediaPlayer implementation is doing, it just an example of the whacky-ness and general fragmentation of the media framework on Android. However, the solution to the above situation was also the solution to the JellyBean problem, and that is:
Have your local proxy respond with chunked encoding
This replaces the Content-Length header with a Tranfer-Encoding: chunked header. This means the requesting client will not know the total length of the resource and thus cannot make a range-request, it just has to deal with the chunks as they are received.
Like I said this is a hack, but it works. It is not without its side effects: media player buffer progress will be incorrect since it doesn't know the length of the audio (is one of them), to get around that you will have to use your own buffering computation (you are streaming from somewhere through your proxy to MediaPlayer, right? So you will know the total length).
When you seek or skip or the connection is lost and MediaPlayer keeps reconnecting to the proxy server, you must send this response with Status 206 after you get the request and range(int) from the client.
String headers += "HTTP/1.0 206 OK\r\n"; headers += "Content-Type: audio/mpeg\r\n"; headers += "Accept-Ranges: bytes\r\n"; headers += "Content-Length: " + (fileSize-range) + "\r\n"; headers += "Content-Range: bytes "+range + "-" + fileSize + "/*\r\n"; headers += "\r\n";