org.devtcg.five.util.streaming.StreamMediaPlayer.java Source code

Java tutorial

Introduction

Here is the source code for org.devtcg.five.util.streaming.StreamMediaPlayer.java

Source

/*
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package org.devtcg.five.util.streaming;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.RequestLine;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;

import android.util.Log;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;

/**
 * Extended MediaPlayer to introduce arbitrary input support.  Uses a
 * local HTTP server to provide the effect of streaming.
 * 
 * Keep in mind that the MediaPlayer imposes overhead with this hack as it
 * stores locally a read-ahead cache of the stream on internal storage.
 * 
 * Ensure that you call {@link release()} to shutdown the server socket.
 */
public class StreamMediaPlayer extends MediaPlayer {
    public static final String TAG = "StreamMediaPlayer";

    protected StreamingHttpServer mServer;

    /* Flag to help us work around Android issue 959. */
    protected boolean mUsed;

    /* See our setDataSource implementation.  We need to store these
     * here in case we have to reset the MediaPlayer to work around
     * some lame Android bug. */
    protected OnBufferingUpdateListener mBufferingUpdateListener;
    protected OnCompletionListener mCompletionListener;
    protected OnErrorListener mErrorListener;
    protected OnPreparedListener mPreparedListener;
    protected OnSeekCompleteListener mSeekCompleteListener;

    public StreamMediaPlayer() {
        super();
        mUsed = false;
    }

    @Override
    public void setDataSource(Context context, Uri uri)
            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
        Log.d(TAG, "setDataSource(context,uri)");
        mUsed = true;

        try {
            super.setDataSource(context, uri);
        } catch (IllegalStateException e) {
            Log.e(TAG, "Illegal state exception, TODO: try again!");
            throw e;
        }
    }

    @Override
    public void setDataSource(FileDescriptor fd, long offset, long length)
            throws IOException, IllegalArgumentException, IllegalStateException {
        Log.d(TAG, "setDataSource(fd,offset,length)");
        mUsed = true;

        try {
            super.setDataSource(fd, offset, length);
        } catch (IllegalStateException e) {
            Log.e(TAG, "Illegal state exception, TODO: try again!");
            throw e;
        }
    }

    @Override
    public void setDataSource(FileDescriptor fd)
            throws IOException, IllegalArgumentException, IllegalStateException {
        Log.d(TAG, "setDataSource(fd)");
        mUsed = true;

        try {
            super.setDataSource(fd);
        } catch (IllegalStateException e) {
            Log.e(TAG, "Illegal state exception, TODO: try again!");
            throw e;
        }
    }

    @Override
    public void setDataSource(String path) throws IOException, IllegalArgumentException, IllegalStateException {
        Log.d(TAG, "setDataSource(path=" + path + ")");
        mUsed = true;

        try {
            super.setDataSource(path);
        } catch (IllegalStateException e) {
            /* See Android issue 957.  The MediaPlayer can sometimes report
             * illegal state and then recalling this method will fix it. */
            super.reset();
            super.setOnBufferingUpdateListener(mBufferingUpdateListener);
            super.setOnCompletionListener(mCompletionListener);
            super.setOnErrorListener(mErrorListener);
            super.setOnPreparedListener(mPreparedListener);
            super.setOnSeekCompleteListener(mSeekCompleteListener);

            Log.d(TAG, "setDataSource(path=" + path + ") *AGAIN*");
            super.setDataSource(path);
        }
    }

    public void setDataSource(RandomAccessStream in)
            throws IllegalStateException, IllegalArgumentException, IOException {
        Log.d(TAG, "setDataSource(RandomAccessStream)");

        if (mServer != null)
            mServer.reset(in);
        else {
            mServer = new StreamingHttpServer(in);
            mServer.start();
        }

        setDataSource(mServer.makeUri());
    }

    private void resetInternalListeners() {
        mBufferingUpdateListener = null;
        mCompletionListener = null;
        mErrorListener = null;
        mPreparedListener = null;
        mSeekCompleteListener = null;
    }

    public void reset() {
        Log.d(TAG, "reset");

        resetInternalListeners();

        if (mUsed == true)
            super.reset();
        else
            Log.i(TAG, "Ignored unnecessary request to reset()");

        if (mServer != null)
            mServer.reset(null);
    }

    @Override
    public void release() {
        Log.d(TAG, "release");

        if (mServer != null) {
            mServer.shutdown();
            mServer = null;
        }

        resetInternalListeners();

        if (mUsed == true)
            super.release();
        else
            Log.i(TAG, "Ignored unnecessary request to release()");
    }

    @Override
    public void setOnBufferingUpdateListener(OnBufferingUpdateListener l) {
        mBufferingUpdateListener = l;
        super.setOnBufferingUpdateListener(l);
    }

    @Override
    public void setOnCompletionListener(OnCompletionListener l) {
        mCompletionListener = l;
        super.setOnCompletionListener(l);
    }

    @Override
    public void setOnErrorListener(OnErrorListener l) {
        mErrorListener = l;
        super.setOnErrorListener(l);
    }

    @Override
    public void setOnPreparedListener(OnPreparedListener l) {
        mPreparedListener = l;
        super.setOnPreparedListener(l);
    }

    @Override
    public void setOnSeekCompleteListener(OnSeekCompleteListener l) {
        mSeekCompleteListener = l;
        super.setOnSeekCompleteListener(l);
    }

    @Override
    public void pause() throws IllegalStateException {
        Log.d(TAG, "pause");
        super.pause();
    }

    @Override
    public void prepareAsync() throws IllegalStateException {
        Log.d(TAG, "prepareAsync");
        super.prepareAsync();
    }

    @Override
    public void seekTo(int msec) throws IllegalStateException {
        Log.d(TAG, "seekTo(" + msec + ")");
        super.seekTo(msec);
    }

    @Override
    public void start() throws IllegalStateException {
        Log.d(TAG, "start");
        super.start();
    }

    @Override
    public void stop() throws IllegalStateException {
        Log.d(TAG, "stop");

        if (mUsed == true)
            super.stop();
        else
            Log.i(TAG, "Ignored unnecessary request to release()");
    }

    /**
     * Connection-based stream which provides seek and open to facilitate
     * arbitrary media streams.
     */
    public static abstract class RandomAccessStream extends InputStream {
        /**
         * Each access stream must have a unique identifier so that the
         * hack here can differentiate various streams as there is only one
         * static server that will be launched.
         * 
         * @deprecated
         *
         * @return
         *   Unique identifier for the resource described by this stream.
         */
        public String getId() {
            return null;
        }

        /**
         * Construct a new instance of the stream, opened and positioned
         * at the 0th seek position.
         */
        public abstract RandomAccessStream newInstance();

        /**
         * Set stream position.
         */
        public abstract void seek(long pos) throws IOException;

        /**
         * Open the stream.  Called exactly once prior to invocation of any
         * other method besides {@link getId}.
         */
        public abstract void open() throws IOException;

        /**
         * Abort the connection and close the stream.
         */
        public abstract void abort();

        /**
         * Answers the total number of bytes in the stream if deterministic;
         * otherwise, -1.
         */
        public abstract long size();

        /**
         * Provide stream content-type. This is HTTP-specific header
         * information.
         */
        public abstract String getContentType();
    }

    /**
     * Hack to feed the MediaPlayer with an HTTP stream to simulate an
     * arbitrary InputStream.  
     */
    private static class StreamingHttpServer extends LocalHttpServer {
        protected RandomAccessStream mStream;

        public StreamingHttpServer() throws IOException {
            super(0);
            setRequestHandler(mHttpHandler);
        }

        public StreamingHttpServer(RandomAccessStream stream) throws IOException {
            this();
            mStream = stream;
        }

        public void reset(RandomAccessStream stream) {
            super.reset();
            mStream = stream;
        }

        @Override
        public void shutdown() {
            super.shutdown();
            mStream = null;
        }

        public String makeUri() {
            return "http://127.0.0.1:" + getPort() + "/";
        }

        private final HttpRequestHandler mHttpHandler = new HttpRequestHandler() {
            private void interpretRangeThenSeek(HttpRequest req, RandomAccessStream stream) throws IOException {
                Header hdr = req.getLastHeader("Range");

                if (hdr == null)
                    return;

                String rangeStr = hdr.getValue();
                Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d+)");
                Matcher matcher = pattern.matcher(rangeStr);

                if (matcher.matches() == false) {
                    Log.w(TAG, "Failed to parse range header: " + rangeStr);
                    return;
                }

                long low;
                long high;

                try {
                    low = Long.parseLong(matcher.group(1));
                    high = Long.parseLong(matcher.group(2));
                } catch (NumberFormatException e) {
                    Log.w(TAG, "Failed to parse range header: " + rangeStr);
                    return;
                }

                /* We assume that high is actually just the end of the
                 * stream as it was originally defined, so we aren't going
                 * to honor it explicitly. */
                assert stream.size() == high;

                Log.i(TAG, "Serving range " + low + "-" + high);
                stream.seek(low);
            }

            public void handle(HttpRequest request, HttpResponse response, HttpContext context)
                    throws HttpException, IOException {
                RequestLine reqLine = request.getRequestLine();
                Log.v(TAG, "reqLine=" + reqLine);

                String method = reqLine.getMethod().toUpperCase(Locale.ENGLISH);
                if (method.equals("GET") == false) {
                    throw new MethodNotSupportedException(method + " method not supported");
                }

                RandomAccessStream stream = mStream.newInstance();

                /* Side-effect: will open the stream, so we can seek if
                 * necessary. */
                RandomAccessStreamEntity ent = new RandomAccessStreamEntity(stream);

                interpretRangeThenSeek(request, stream);

                response.setHeader("Accept-Ranges", "bytes");
                response.setEntity(ent);
                response.setStatusCode(HttpStatus.SC_OK);
            }
        };

        public class RandomAccessStreamEntity extends AbstractHttpEntity {
            private final static int BUFFER_SIZE = 2048;

            private final RandomAccessStream mStream;
            private final long mLength;
            private boolean mConsumed = false;

            public RandomAccessStreamEntity(RandomAccessStream stream) throws IOException {
                super();

                /*
                 * This seems to be necessary only for _certain_ streams?
                 * Perhaps the issue is with automatic detection?
                 */
                String contentType = stream.getContentType();
                if (contentType != null)
                    setContentType(contentType);

                mStream = stream;

                try {
                    stream.open();
                } catch (IOException e) {
                    Log.e(TAG, "Stream open failure", e);
                    throw e;
                }

                mLength = stream.size();
            }

            public long getContentLength() {
                return mLength;
            }

            public void writeTo(OutputStream outstream) throws IOException {
                Log.i(TAG, "writeTo...");

                try {
                    byte[] b = new byte[BUFFER_SIZE];
                    int n;

                    while ((n = mStream.read(b)) >= 0)
                        outstream.write(b, 0, n);

                    mConsumed = true;
                } finally {
                    Log.i(TAG, "writeTo finished...");
                    mStream.close();
                }
            }

            public void consumeContent() throws IOException {
                throw new RuntimeException("Is this used?");
            }

            public InputStream getContent() throws IOException, IllegalStateException {
                return mStream;
            }

            public boolean isRepeatable() {
                return false;
            }

            public boolean isStreaming() {
                return !mConsumed;
            }
        }
    }
}