com.shonshampain.streamrecorder.util.StreamProxy.java Source code

Java tutorial

Introduction

Here is the source code for com.shonshampain.streamrecorder.util.StreamProxy.java

Source

// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.shonshampain.streamrecorder.util;

import android.content.Context;
import android.os.Handler;
import android.util.Log;

import com.shonshampain.streamrecorder.application.StreamRecorderApplication;
import com.shonshampain.streamrecorder.events.ProxyAcceptingConnectionsEvent;
import com.shonshampain.streamrecorder.events.RipEvent;
import com.shonshampain.streamrecorder.events.ThrottleStreamRequest;
import com.shonshampain.streamrecorder.helpers.FileHelper;
import com.shonshampain.streamrecorder.helpers.NotificationHelper;
import com.shonshampain.streamrecorder.managers.StreamManager;

import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseFactory;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ClientConnectionOperator;
import org.apache.http.conn.OperatedClientConnection;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.DefaultClientConnection;
import org.apache.http.impl.conn.DefaultClientConnectionOperator;
import org.apache.http.impl.conn.DefaultResponseParser;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.io.HttpMessageParser;
import org.apache.http.io.SessionInputBuffer;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicLineParser;
import org.apache.http.message.ParserCursor;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.CharArrayBuffer;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.StringTokenizer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import de.greenrobot.event.EventBus;

public class StreamProxy implements Runnable {

    private static final String TAG = "StreamProxy";
    private static final boolean DBG = false;
    private static final boolean DBG_READS = false;
    private static final boolean DBG_WRITES = false;
    private static final boolean DBG_META = false;

    private static final int STREAM_STALLED_TIMEOUT = 10 * 1000;
    private static final int MAX_RETRIES = 10;

    private int port = 0;
    private int notificationId;
    private static Context context;

    public int getPort() {
        return port;
    }

    private static boolean isRunning = true;
    private static ServerSocket socket;
    private static Thread thread;
    private String localFile;

    FileHelper fileHelper;
    FileOutputStream fos;
    MetaDataExtractor mde;

    private StreamProxy() {
    }

    private static StreamProxy streamProxy;

    public static StreamProxy getInstance(Context context) {
        Logger.logEvent("StreamProxy:getInstance()");
        StreamProxy.context = context;
        if (streamProxy == null) {
            Logger.logEvent("StreamProxy instance is null, creating new instance");
            streamProxy = new StreamProxy();
        }
        if (thread != null) {
            Logger.logEvent("StreamProxy stale thread, killing existing thread");
            stop();
        }
        Logger.logEvent("StreamProxy exiting getInstance()");
        return streamProxy;
    }

    private void init() {
        try {
            socket = new ServerSocket(port, 0, InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }));
            socket.setSoTimeout(5000);
            port = socket.getLocalPort();
            Logger.d(DBG, TAG, "port " + port + " obtained");
        } catch (IOException e) {
            Logger.e(TAG, "Error initializing server", e);
        }
    }

    public void start() {
        Logger.logEvent("StreamProxy:start()");
        isRunning = true;
        init();
        EventBus.getDefault().register(this);
        thread = new Thread(this);
        thread.start();
        Handler handler = new Handler();
        Runnable postAcceptingConnectionsRunnable = new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post(new ProxyAcceptingConnectionsEvent());
                Logger.logEvent("Proxy is accepting connections");
            }
        };
        // This is a bit of a hack.
        // We want to post the event after socket.accept,
        // but after socket.accept, we're in a wait state
        // This is good enough.
        handler.postDelayed(postAcceptingConnectionsRunnable, 1000);
    }

    public static void stop() {
        Logger.logEvent(">> Stopping StreamProxy");
        if (socket != null && !socket.isClosed()) {
            try {
                socket.close();
                socket = null;
            } catch (IOException ioe) {
                Logger.e(TAG, "Error closing socket", ioe);
            }
        }
        isRunning = false;
        if (thread == null) {
            Logger.e(TAG, "Cannot stop proxy; it has not been started.");
            throw new IllegalStateException("Cannot stop proxy; it has not been started.");
        }
        thread.interrupt();
        try {
            thread.join(5000);
        } catch (InterruptedException e) {
            Logger.e(TAG, "Interrupted exception on stop()", e);
        }
        thread = null;
        EventBus.getDefault().unregister(streamProxy);
        context = null;
    }

    private int retryCount;

    @Override
    public void run() {
        Socket client = null;
        HttpRequest request;

        Logger.d(DBG, TAG, "running");
        while (isRunning) {
            try {
                Logger.d(DBG, TAG, "Waiting on a connection");
                ExecutorService executor = Executors.newFixedThreadPool(1);
                Callable<Socket> task = new Callable<Socket>() {
                    @Override
                    public Socket call() throws Exception {
                        return socket.accept();
                    }
                };
                Future<Socket> future = executor.submit(task);
                try {
                    client = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS);
                } catch (TimeoutException to) {
                    startOver(null, null, FailType.SocketAccept, null);
                    continue;
                } catch (InterruptedException ie) {
                    Logger.e(TAG, "The read operation was interrupted");
                } catch (ExecutionException ee) {
                    startOver(null, null, FailType.SocketAccept, null);
                    continue;
                }
                if (client == null) {
                    continue;
                }
                Logger.d(DBG, TAG, "client connected");
                request = readRequest(client);
                Logger.d(DBG, TAG, "calling process request...");
                if (request != null) {
                    processRequest(request, client);
                }
                Logger.d(DBG, TAG, "...and we're back");
            } catch (SocketTimeoutException e) {
                Logger.d(DBG, TAG, "Socket timed out");
            } catch (IOException e) {
                Log.e(TAG, "Error connecting to client", e);
            }
        }
        Logger.d(DBG, TAG, "Proxy interrupted. Shutting down.");
    }

    private HttpRequest readRequest(Socket client) {
        HttpRequest request;
        InputStream is;
        String firstLine;
        try {
            is = client.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
            firstLine = reader.readLine();
        } catch (IOException e) {
            Logger.e(TAG, "Error parsing request", e);
            return null;
        }

        if (firstLine == null) {
            Logger.d(DBG, TAG, "Proxy client closed connection without a request.");
            return null;
        }

        StringTokenizer st = new StringTokenizer(firstLine);
        String method = st.nextToken();
        String uri = st.nextToken();
        Logger.d(DBG, TAG, uri);
        String realUri = uri.substring(1);
        Logger.d(DBG, TAG, realUri);
        request = new BasicHttpRequest(method, realUri);
        return request;
    }

    private HttpResponse download(String url) {
        DefaultHttpClient seed = new DefaultHttpClient();
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        SingleClientConnManager mgr = new MyClientConnManager(seed.getParams(), registry);
        final DefaultHttpClient http = new DefaultHttpClient(mgr, seed.getParams());
        final HttpGet method = new HttpGet(url);
        method.addHeader("Icy-MetaData", "1");
        HttpResponse response;
        Logger.d(DBG, TAG, "starting download");
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Callable<HttpResponse> readTask = new Callable<HttpResponse>() {
            @Override
            public HttpResponse call() throws Exception {
                return http.execute(method);
            }
        };
        Future<HttpResponse> future = executor.submit(readTask);
        try {
            response = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (TimeoutException to) {
            return null;
        } catch (InterruptedException ie) {
            Logger.e(TAG, "The read operation was interrupted");
            return null;
        } catch (ExecutionException ee) {
            return null;
        }
        Logger.d(DBG, TAG, "downloaded");
        return response;
    }

    private void processRequest(HttpRequest request, Socket client) throws IllegalStateException, IOException {
        if (request == null) {
            return;
        }
        String url = request.getRequestLine().getUri();
        int mi = 0;
        Logger.d(DBG, TAG, "processing: " + url);
        Logger.d(DBG, TAG, "request: " + request.getRequestLine());
        for (Header h : request.getAllHeaders()) {
            Logger.d(DBG, TAG, "header: [" + h.getName() + "] = [" + h.getValue() + "]");
        }
        HttpResponse realResponse = download(url);
        if (realResponse == null) {
            startOver(null, client, FailType.CantDownload, null);
            return;
        }

        Logger.d(DBG, TAG, "downloading...");

        final InputStream data = realResponse.getEntity().getContent();
        StatusLine line = realResponse.getStatusLine();
        HttpResponse response = new BasicHttpResponse(line);
        response.setHeaders(realResponse.getAllHeaders());

        Logger.d(DBG, TAG, "reading headers");
        StringBuilder httpString = new StringBuilder();
        httpString.append(response.getStatusLine().toString());

        httpString.append("\n");
        for (Header h : response.getAllHeaders()) {
            if (h.getName().equalsIgnoreCase("Transfer-Encoding")) {
                /* The KCRW stream specifies chunked encoding in their headers,
                 * however, when we read their stream, the data gets "unchunked".
                 * Therefore, we cannot advertise a chunked encoding unless
                 * we actually re-chunk the data; which we are not.
                 */
                httpString.append("Accept-Ranges: bytes\r\n");
                httpString.append("Content-Length: 9999999999\r\n");
            } else {
                if (h.getName().equalsIgnoreCase("icy-metaint")) {
                    mi = Integer.parseInt(h.getValue());
                    Logger.d(DBG, TAG, "Creating new meta data extractor with interval: " + mi);
                    mde = new MetaDataExtractor(mi);
                }
                httpString.append(h.getName()).append(": ").append(h.getValue()).append("\r\n");
            }
        }
        httpString.append("\n");
        Logger.d(DBG, TAG, "headers done: [" + httpString + "]");

        try {
            byte[] buffer = httpString.toString().getBytes();
            int readBytes;
            Logger.d(DBG_WRITES, TAG, "writing headers to client");
            client.getOutputStream().write(buffer, 0, buffer.length);

            // Start streaming content.
            final byte[] buff = new byte[1024 * 50];
            boolean endOfStream = false;
            ExecutorService executor = Executors.newFixedThreadPool(1);

            while (isRunning && !endOfStream) {
                Callable<Integer> readTask = new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return data.read(buff, 0, buff.length);
                    }
                };
                Future<Integer> future = executor.submit(readTask);
                try {
                    readBytes = future.get(STREAM_STALLED_TIMEOUT, TimeUnit.MILLISECONDS);
                } catch (TimeoutException to) {
                    startOver(data, client, FailType.Stall, null);
                    return;
                } catch (InterruptedException ie) {
                    Logger.e(TAG, "The read operation was interrupted");
                    continue;
                }
                endOfStream = readBytes == -1;
                if (!endOfStream) {
                    Logger.d(DBG_READS, TAG, "Raw read: " + readBytes + " bytes");
                    if (mi > 0) {
                        readBytes = mde.processBuffer(buff, readBytes);
                        Logger.d(DBG_META, TAG,
                                "Status: " + mde.getStatus() + ", running count: " + mde.getRunningCount());
                    }
                    Logger.d(DBG_WRITES, TAG, "writing " + readBytes + " bytes of content to client");
                    client.getOutputStream().write(buff, 0, readBytes);
                    if (fileHelper != null) {
                        Logger.d(DBG, TAG, "writing " + readBytes + " bytes of content to file");
                        //                        NotificationHelper.build(context, "StreamRecorder Rip Control", "writing " + readBytes + " bytes", notificationId);
                        fileHelper.write(fos, buff, readBytes);
                    }
                }
            }
        } catch (Exception e) {
            startOver(data, client, FailType.Unexpected, e);
        }
    }

    private enum FailType {
        Stall, Unexpected, CantDownload, SocketAccept
    }

    private void startOver(InputStream data, Socket client, FailType type, Exception e) throws IOException {
        if (data != null) {
            Logger.e(TAG, "Closing data connection");
            data.close();
        }
        if (client != null && !client.isClosed()) {
            Logger.e(TAG, "Closing client connection");
            client.close();
        }
        ++retryCount;
        switch (type) {
        case Stall:
            Logger.e(TAG, "Stream has stalled, rethrottling(" + retryCount + ") the data connection");
            break;
        case Unexpected:
            Logger.e(TAG, "Unexpected exception: " + e.getLocalizedMessage() + " retrying(" + retryCount + ")");
            break;
        case CantDownload:
            Logger.e(TAG, "Cannot download, retrying(" + retryCount + ")");
            break;
        case SocketAccept:
            Logger.e(TAG, "Cannot accept on the socket, retrying(" + retryCount + ")");
            break;
        }
        if (retryCount == MAX_RETRIES) {
            Logger.e(TAG, "Retry count exceeded, stopping stream service");
            StreamManager.getInstance().stop(StreamRecorderApplication.getContext());
        } else {
            EventBus.getDefault().post(new ThrottleStreamRequest());
        }
    }

    @SuppressWarnings("unused")
    public void onEventMainThread(RipEvent event) {
        if (event.playlist != null) {
            Logger.d(DBG, TAG, "Ripping file: [" + event.playlist.localFile + "]");
            Logger.logEvent("Ripping file: [" + event.playlist.localFile + "]");
            fileHelper = new FileHelper();
            localFile = event.playlist.localFile;
            fos = fileHelper.open("" + localFile);
            notificationId = event.playlist._id;
            NotificationHelper.build(context, "StreamRecorder Rip Control", "Opening file[" + localFile + "]",
                    notificationId);
        } else {
            Logger.d(DBG, TAG, "Closing file");
            Logger.logEvent("Closing file");
            fileHelper.close(fos);
            fileHelper = null;
            NotificationHelper.build(context, "StreamRecorder Rip Control",
                    "file[" + localFile + "] closed, size[" + FileHelper.size(localFile) + "]", notificationId);
        }
    }

    private class IcyLineParser extends BasicLineParser {
        private static final String ICY_PROTOCOL_NAME = "ICY";

        private IcyLineParser() {
            super();
        }

        @Override
        public boolean hasProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor) {
            boolean superFound = super.hasProtocolVersion(buffer, cursor);
            if (superFound) {
                return true;
            }
            int index = cursor.getPos();

            final int protolength = ICY_PROTOCOL_NAME.length();

            if (buffer.length() < protolength)
                return false; // not long enough for "HTTP/1.1"

            if (index < 0) {
                // end of line, no tolerance for trailing whitespace
                // this works only for single-digit major and minor version
                index = buffer.length() - protolength;
            } else if (index == 0) {
                // beginning of line, tolerate leading whitespace
                while ((index < buffer.length()) && HTTP.isWhitespace(buffer.charAt(index))) {
                    index++;
                }
            } // else within line, don't tolerate whitespace

            return index + protolength <= buffer.length()
                    && buffer.substring(index, index + protolength).equals(ICY_PROTOCOL_NAME);

        }

        @Override
        public ProtocolVersion parseProtocolVersion(CharArrayBuffer buffer, ParserCursor cursor)
                throws ParseException {

            if (buffer == null) {
                throw new IllegalArgumentException("Char array buffer may not be null");
            }
            if (cursor == null) {
                throw new IllegalArgumentException("Parser cursor may not be null");
            }

            final int protolength = ICY_PROTOCOL_NAME.length();

            int indexFrom = cursor.getPos();
            int indexTo = cursor.getUpperBound();

            skipWhitespace(buffer, cursor);

            int i = cursor.getPos();

            // long enough for "HTTP/1.1"?
            if (i + protolength + 4 > indexTo) {
                throw new ParseException("Not a valid protocol version: " + buffer.substring(indexFrom, indexTo));
            }

            // check the protocol name and slash
            if (!buffer.substring(i, i + protolength).equals(ICY_PROTOCOL_NAME)) {
                return super.parseProtocolVersion(buffer, cursor);
            }

            cursor.updatePos(i + protolength);

            return createProtocolVersion(1, 0);
        }

        @Override
        public StatusLine parseStatusLine(CharArrayBuffer buffer, ParserCursor cursor) throws ParseException {
            return super.parseStatusLine(buffer, cursor);
        }
    }

    class MyClientConnection extends DefaultClientConnection {
        @Override
        protected HttpMessageParser createResponseParser(final SessionInputBuffer buffer,
                final HttpResponseFactory responseFactory, final HttpParams params) {
            return new DefaultResponseParser(buffer, new IcyLineParser(), responseFactory, params);
        }
    }

    class MyClientConnectionOperator extends DefaultClientConnectionOperator {
        public MyClientConnectionOperator(final SchemeRegistry sr) {
            super(sr);
        }

        @Override
        public OperatedClientConnection createConnection() {
            return new MyClientConnection();
        }
    }

    class MyClientConnManager extends SingleClientConnManager {
        private MyClientConnManager(HttpParams params, SchemeRegistry schreg) {
            super(params, schreg);
        }

        @Override
        protected ClientConnectionOperator createConnectionOperator(final SchemeRegistry sr) {
            return new MyClientConnectionOperator(sr);
        }
    }

}