10

I'm having a real hard time figuring out how to parse a none standard HTTP response.

The none standard response contains ICY 200 OK instead of HTTP 200 OK. Here is a sample URL that sends the none standard HTTP response.
http://50.117.121.162:80

Since Android 4.4 HttpURLConnection will no longer work with these none standard responses. I have tried using the HttpClient by Apache but it doesn't work because of the none standard HTTP response. I have then tried following the guide for adding a custom response parser, but Android doesn't seem have all the classes needed to do it.

I'm really struggling to figure out a solution. Possibly modify the none standard response before it is parsed by the HttpClient or the HttpURLConnection could work but I'm not sure if that is even possible...

Any help would be greatly appreciated.

Jona
  • 12,642
  • 13
  • 82
  • 124
  • Related: http://stackoverflow.com/questions/1963105/listen-to-a-shoutcast-with-android – blahdiblah Nov 04 '13 at 00:44
  • I don't think this question is related to that post. I'm not having issues with knowing how to stream SHOUTcast content and parsing metadata etc... I have all this done and working great. The issue is with the latest Android 4.4 the HttpURLConnection is much more strict causing the issues at a lower level. – Jona Nov 04 '13 at 00:51
  • Wrong language, but take a look at some of the proposed solutions here: http://stackoverflow.com/q/9881305/362536 Basically, I ended up writing code that got in front of the HTTP client to re-write `ICY` as `HTTP/1.0`, and then left it alone. Perhaps you can do something similar for Android? – Brad Nov 04 '13 at 17:47
  • Thanks @Brad for your idea. I was thinking on doing something similar but I'm still exploring the best way to do it... I don't want to break support for other none icy servers. – Jona Nov 04 '13 at 20:03
  • If you check for `ICY ` as the first 4 bytes, and only insert `HTTP/1.0 ` if you have to, then you will still be able to support normal HTTP servers just fine. I've tested the method on 6,000 internet radio servers of all kinds, and it works well. You will just have to find a way of implemented in Android. – Brad Nov 04 '13 at 20:06
  • I think I'll need to do a localhost proxy and verify and update that header. – Jona Nov 06 '13 at 04:46

6 Answers6

3

After a lot of research for a small/lite http client library, I ran into this port of the apache httpclient for android. The library provided a complete support for http connections. Then I simply modified the source code, particularly the BasicLineParser to replace ICY with HTTP/1.0.

Jona
  • 12,642
  • 13
  • 82
  • 124
  • Hi Jona I am having the same problem using `InptStream` to write data to a file. I got the `ch.boye.httpclientandroidlib` library working great with streamscraper but I need it with `InputStream` as well because this is where I am getting the same error you had (`java.net.protocolexception unexpected status line: icy 200`) when writing data to a file using `InputStream` and `OutputStream` with a Shoutcast stream. Can you please share some code how you accomplished this or at least explain how to implement with `InputStream` and `OutputStream`? I'm really lost here. – midiwriter Nov 30 '13 at 00:52
  • Could you please elaborate more and post some code? Specially the modified httpclientandroidlib code. – cprcrack Dec 26 '13 at 03:02
2

I had similar problem with KitKat and had a success with using two classes found here for http post. They are incredibly easy to use and you can modify the protocol params easily too.

slezadav
  • 5,936
  • 6
  • 33
  • 61
  • Hi can you please explain how you utilized these classes for this purpose? I'm having the hardest time figuring this out. Thank you. – midiwriter Dec 09 '13 at 13:50
  • 1
    In HttpClientUtil change method setHttpProtocolParams, there you can specify whether you use ICY or whatever.. – slezadav Dec 10 '13 at 10:34
1

There is another solution to this issue in Android 4.4 but it requires using Apache HttpClient. This is based on possibility of providing custom response parser into Apache Http engine that can change ICY 200 OK to HTTP/1.0 200 OK. This is based on general idea presented in:

http://hc.apache.org/httpcomponents-client-4.2.x/tutorial/html/advanced.html

I have successfully used following code.

public class IcyClientConnection extends DefaultClientConnection{

@Override
protected HttpMessageParser createResponseParser(SessionInputBuffer buffer,
        HttpResponseFactory responseFactory, HttpParams params) {

    return new IcyHttpResponseParser(
            buffer, 
            new BasicLineParser (), 
            responseFactory, 
            params);

}

}


public class IcyClientConnectionOperator extends DefaultClientConnectionOperator {

    public IcyClientConnectionOperator(SchemeRegistry schemes) {
        super(schemes);
    }

    @Override
    public OperatedClientConnection createConnection() {

        return new IcyClientConnection();
    }

}



public class IcyClientConnManager extends SingleClientConnManager  {


    public IcyClientConnManager(HttpParams params, SchemeRegistry schreg) {
        super(params, schreg);
    }

    @Override
    protected ClientConnectionOperator createConnectionOperator(
            SchemeRegistry schreg) {
        return new IcyClientConnectionOperator(schreg);
    }

}

Now you have to extend parser used by default and add code that will change wrong server replay to correct one. Normally code will block on hasProtocolVersion.

public class IcyHttpResponseParser extends DefaultResponseParser{

    private CharArrayBuffer icyLineBuf;

    private int icyMaxGarbageLines = 1000;
    private final HttpResponseFactory icyResponseFactory;




public IcyHttpResponseParser(SessionInputBuffer buffer, LineParser parser,
        HttpResponseFactory responseFactory, HttpParams params) {
    super(buffer, parser, responseFactory, params);

    this.icyLineBuf = new CharArrayBuffer(128);
    icyResponseFactory = responseFactory;
}

@Override
protected HttpMessage parseHead(SessionInputBuffer sessionBuffer)
        throws IOException, HttpException {
    int count = 0;
    ParserCursor cursor = null;
    do {
        // clear the buffer
        this.icyLineBuf.clear();
        final int i = sessionBuffer.readLine(this.icyLineBuf);

        //look for ICY and change to HTTP to provide compatibility with non standard shoutcast servers

        String tmp = icyLineBuf.substring(0, this.icyLineBuf.length());
        if(tmp.contains("ICY ")){
            tmp = tmp.replace("ICY", "HTTP/1.0");
        }
        //copy
        this.icyLineBuf = new CharArrayBuffer(128);
        System.arraycopy(tmp.toCharArray(), 0, icyLineBuf.buffer(), 0, tmp.length());
        icyLineBuf.setLength( tmp.length());


        if (i == -1 && count == 0) {
            // The server just dropped connection on us
            throw new NoHttpResponseException("The target server failed to respond");
        }
        cursor = new ParserCursor(0, this.icyLineBuf.length());
        if (lineParser.hasProtocolVersion(this.icyLineBuf, cursor)) {
            // Got one
            break;
        } else if (i == -1 || count >= this.icyMaxGarbageLines) {
            // Giving up
            throw new ProtocolException("The server failed to respond with a " +
                    "valid HTTP response");
        }
        //if (this.log.isDebugEnabled()) {
        //   this.log.debug("Garbage in response: " + this.lineBuf.toString());
       // }
        count++;
    } while(true);
    //create the status line from the status string
    final StatusLine statusline = lineParser.parseStatusLine(this.icyLineBuf, cursor);
    return this.icyResponseFactory.newHttpResponse(statusline, null);
}
}

Plug in HttpClient:

Scheme http = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
Scheme ftp = new Scheme("ftp", PlainSocketFactory.getSocketFactory(), 21);

SchemeRegistry sr = new SchemeRegistry();
sr.register(http);
sr.register(ftp);

HttpClient httpClient = new DefaultHttpClient(new IcyClientConnManager(params, sr), params);

This is still being tested but initial results are promising.

Michał M
  • 141
  • 1
  • 2
1

Thanks @Michael M, you can even make it simpler by subclassing the BasicLineParser instead of subclassing the DefaultResponseParser.

I've uploaded the code into a gist

To use it:

IcyGetRequest request = new IcyGetRequest(urlStr);
HttpResponse response = request.get();
int responseCode = response.getStatusLine().getStatusCode();
Tom Susel
  • 3,111
  • 1
  • 22
  • 23
0

Create an runnable that creates socket proxy then you will be able to response with HTTP/1.0 instead of ICY , then just connect to this local socket proxy with your player

  • This will add an unneeded overhead... I guess a very simple java http client library would do but I can't seem to find one... :( – Jona Nov 05 '13 at 23:21
  • if you'll just buffer small amount of data and keep stream music to the local "Proxy" it won't overload the application – Adi Milner Nov 06 '13 at 06:55
  • Issues with this is loss of control once a redirect occurs on a particular connection. I have tried doing proxy server but I did run into issues with redirects. – Jona Nov 07 '13 at 12:43
0

Here a modification of the solution from Michal M in case you don't like to create lots of subclasses just to configure already available HttpClient classes.

final SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
HttpClient httpClient = new DefaultHttpClient() {
    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        return new SingleClientConnManager(getParams(), schemeRegistry) {
            @Override
            protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) {
                return new DefaultClientConnectionOperator(schreg) {
                    @Override
                    public OperatedClientConnection createConnection() {
                        return new DefaultClientConnection() {
                            @Override
                            protected HttpMessageParser createResponseParser(SessionInputBuffer buffer, HttpResponseFactory responseFactory, HttpParams params) {
                                return new IcyHttpResponseParser(buffer, new BasicLineParser(), responseFactory, params);
                            }
                        };
                    }
                };
            }
        };
    }
};

Probably there is a way to get the SchemeRegistry obsoleted if one could get hold somehow from within the DefaultHttpClient class.

Community
  • 1
  • 1
Draško Kokić
  • 1,185
  • 1
  • 18
  • 32