30

I modified the MJPEG viewer code from Android and MJPEG to work using an AsyncTask (and thus work on Ice Cream Sandwich (ICS), 4.0.4) and here is my code.

If anyone has any suggestions on how to optimize, cleanup, or do something more proper with the code please let me know. Two issues I'd appreciate help addressing:

  • If you have the device on a stream then lock the screen and unlock the screen it does not resume playing until you either kill and resume the app or rotate the screen. All my attempts at using OnResume() to do something or other resulted in app crashes.

  • In particular I'd like to get the AsyncTask back in MjpegInputStream.java but was not able to get that to work.

MjpegActivity.java:

package com.demo.mjpeg;

import java.io.IOException;
import java.net.URI;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import com.demo.mjpeg.MjpegView.MjpegInputStream;
import com.demo.mjpeg.MjpegView.MjpegView;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;

public class MjpegActivity extends Activity {
    private static final String TAG = "MjpegActivity";

    private MjpegView mv;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //sample public cam
        String URL = "http://trackfield.webcam.oregonstate.edu/axis-cgi/mjpg/video.cgi?resolution=800x600&amp%3bdummy=1333689998337";

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
        WindowManager.LayoutParams.FLAG_FULLSCREEN);

        mv = new MjpegView(this);
        setContentView(mv);        

        new DoRead().execute(URL);
    }

    public void onPause() {
        super.onPause();
        mv.stopPlayback();
    }

    public class DoRead extends AsyncTask<String, Void, MjpegInputStream> {
        protected MjpegInputStream doInBackground(String... url) {
            //TODO: if camera has authentication deal with it and don't just not work
            HttpResponse res = null;
            DefaultHttpClient httpclient = new DefaultHttpClient();     
            Log.d(TAG, "1. Sending http request");
            try {
                res = httpclient.execute(new HttpGet(URI.create(url[0])));
                Log.d(TAG, "2. Request finished, status = " + res.getStatusLine().getStatusCode());
                if(res.getStatusLine().getStatusCode()==401){
                    //You must turn off camera User Access Control before this will work
                    return null;
                }
                return new MjpegInputStream(res.getEntity().getContent());  
            } catch (ClientProtocolException e) {
                e.printStackTrace();
                Log.d(TAG, "Request failed-ClientProtocolException", e);
                //Error connecting to camera
            } catch (IOException e) {
                e.printStackTrace();
                Log.d(TAG, "Request failed-IOException", e);
                //Error connecting to camera
            }

            return null;
        }

        protected void onPostExecute(MjpegInputStream result) {
            mv.setSource(result);
            mv.setDisplayMode(MjpegView.SIZE_BEST_FIT);
            mv.showFps(true);
        }
    }
}

MjpegInputStream.java:

package com.demo.mjpeg.MjpegView;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

public class MjpegInputStream extends DataInputStream {
    private static final String TAG = "MjpegInputStream";

    private final byte[] SOI_MARKER = { (byte) 0xFF, (byte) 0xD8 };
    private final byte[] EOF_MARKER = { (byte) 0xFF, (byte) 0xD9 };
    private final String CONTENT_LENGTH = "Content-Length";
    private final static int HEADER_MAX_LENGTH = 100;
    private final static int FRAME_MAX_LENGTH = 40000 + HEADER_MAX_LENGTH;
    private int mContentLength = -1;

    public MjpegInputStream(InputStream in) {
        super(new BufferedInputStream(in, FRAME_MAX_LENGTH));
    }

    private int getEndOfSeqeunce(DataInputStream in, byte[] sequence) throws IOException {
        int seqIndex = 0;
        byte c;
        for(int i=0; i < FRAME_MAX_LENGTH; i++) {
            c = (byte) in.readUnsignedByte();
            if(c == sequence[seqIndex]) {
                seqIndex++;
                if(seqIndex == sequence.length) {
                    return i + 1;
                }
            } else {
                seqIndex = 0;
            }
        }
        return -1;
    }

    private int getStartOfSequence(DataInputStream in, byte[] sequence) throws IOException {
        int end = getEndOfSeqeunce(in, sequence);
        return (end < 0) ? (-1) : (end - sequence.length);
    }

    private int parseContentLength(byte[] headerBytes) throws IOException, NumberFormatException {
        ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes);
        Properties props = new Properties();
        props.load(headerIn);
        return Integer.parseInt(props.getProperty(CONTENT_LENGTH));
    }   

    public Bitmap readMjpegFrame() throws IOException {
        mark(FRAME_MAX_LENGTH);
        int headerLen = getStartOfSequence(this, SOI_MARKER);
        reset();
        byte[] header = new byte[headerLen];
        readFully(header);
        try {
            mContentLength = parseContentLength(header);
        } catch (NumberFormatException nfe) { 
            nfe.getStackTrace();
            Log.d(TAG, "catch NumberFormatException hit", nfe);
            mContentLength = getEndOfSeqeunce(this, EOF_MARKER); 
        }
        reset();
        byte[] frameData = new byte[mContentLength];
        skipBytes(headerLen);
        readFully(frameData);
        return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData));
    }
}

MjpegView.java:

package com.demo.mjpeg.MjpegView;

import java.io.IOException;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MjpegView extends SurfaceView implements SurfaceHolder.Callback {
    private static final String TAG = "MjpegView";

    public final static int POSITION_UPPER_LEFT  = 9;
    public final static int POSITION_UPPER_RIGHT = 3;
    public final static int POSITION_LOWER_LEFT  = 12;
    public final static int POSITION_LOWER_RIGHT = 6;

    public final static int SIZE_STANDARD   = 1; 
    public final static int SIZE_BEST_FIT   = 4;
    public final static int SIZE_FULLSCREEN = 8;

    private MjpegViewThread thread;
    private MjpegInputStream mIn = null;    
    private boolean showFps = false;
    private boolean mRun = false;
    private boolean surfaceDone = false;    
    private Paint overlayPaint;
    private int overlayTextColor;
    private int overlayBackgroundColor;
    private int ovlPos;
    private int dispWidth;
    private int dispHeight;
    private int displayMode;

    public class MjpegViewThread extends Thread {
        private SurfaceHolder mSurfaceHolder;
        private int frameCounter = 0;
        private long start;
        private Bitmap ovl;

        public MjpegViewThread(SurfaceHolder surfaceHolder, Context context) {
            mSurfaceHolder = surfaceHolder;
        }

        private Rect destRect(int bmw, int bmh) {
            int tempx;
            int tempy;
            if (displayMode == MjpegView.SIZE_STANDARD) {
                tempx = (dispWidth / 2) - (bmw / 2);
                tempy = (dispHeight / 2) - (bmh / 2);
                return new Rect(tempx, tempy, bmw + tempx, bmh + tempy);
            }
            if (displayMode == MjpegView.SIZE_BEST_FIT) {
                float bmasp = (float) bmw / (float) bmh;
                bmw = dispWidth;
                bmh = (int) (dispWidth / bmasp);
                if (bmh > dispHeight) {
                    bmh = dispHeight;
                    bmw = (int) (dispHeight * bmasp);
                }
                tempx = (dispWidth / 2) - (bmw / 2);
                tempy = (dispHeight / 2) - (bmh / 2);
                return new Rect(tempx, tempy, bmw + tempx, bmh + tempy);
            }
            if (displayMode == MjpegView.SIZE_FULLSCREEN){
                return new Rect(0, 0, dispWidth, dispHeight);
            }
            return null;
        }

        public void setSurfaceSize(int width, int height) {
            synchronized(mSurfaceHolder) {
                dispWidth = width;
                dispHeight = height;
            }
        }

        private Bitmap makeFpsOverlay(Paint p, String text) {
            Rect b = new Rect();
            p.getTextBounds(text, 0, text.length(), b);
            int bwidth  = b.width()+2;
            int bheight = b.height()+2;
            Bitmap bm = Bitmap.createBitmap(bwidth, bheight, Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(bm);
            p.setColor(overlayBackgroundColor);
            c.drawRect(0, 0, bwidth, bheight, p);
            p.setColor(overlayTextColor);
            c.drawText(text, -b.left+1, (bheight/2)-((p.ascent()+p.descent())/2)+1, p);
            return bm;           
        }

        public void run() {
            start = System.currentTimeMillis();
            PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.DST_OVER);
            Bitmap bm;
            int width;
            int height;
            Rect destRect;
            Canvas c = null;
            Paint p = new Paint();
            String fps;
            while (mRun) {
                if(surfaceDone) {
                    try {
                        c = mSurfaceHolder.lockCanvas();
                        synchronized (mSurfaceHolder) {
                            try {
                                bm = mIn.readMjpegFrame();
                                destRect = destRect(bm.getWidth(),bm.getHeight());
                                c.drawColor(Color.BLACK);
                                c.drawBitmap(bm, null, destRect, p);
                                if(showFps) {
                                    p.setXfermode(mode);
                                    if(ovl != null) {
                                        height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom-ovl.getHeight();
                                        width  = ((ovlPos & 8) == 8) ? destRect.left : destRect.right -ovl.getWidth();
                                        c.drawBitmap(ovl, width, height, null);
                                    }
                                    p.setXfermode(null);
                                    frameCounter++;
                                    if((System.currentTimeMillis() - start) >= 1000) {
                                        fps = String.valueOf(frameCounter)+" fps";
                                        frameCounter = 0; 
                                        start = System.currentTimeMillis();
                                        ovl = makeFpsOverlay(overlayPaint, fps);
                                    }
                                }
                            } catch (IOException e) {
                                e.getStackTrace();
                                Log.d(TAG, "catch IOException hit in run", e);
                            }
                        }
                    } finally { 
                        if (c != null) {
                            mSurfaceHolder.unlockCanvasAndPost(c); 
                        }
                    }
                }
            }
        }
    }

    private void init(Context context) {
        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
        thread = new MjpegViewThread(holder, context);
        setFocusable(true);
        overlayPaint = new Paint();
        overlayPaint.setTextAlign(Paint.Align.LEFT);
        overlayPaint.setTextSize(12);
        overlayPaint.setTypeface(Typeface.DEFAULT);
        overlayTextColor = Color.WHITE;
        overlayBackgroundColor = Color.BLACK;
        ovlPos = MjpegView.POSITION_LOWER_RIGHT;
        displayMode = MjpegView.SIZE_STANDARD;
        dispWidth = getWidth();
        dispHeight = getHeight();
    }

    public void startPlayback() { 
        if(mIn != null) {
            mRun = true;
            thread.start();         
        }
    }

    public void stopPlayback() { 
        mRun = false;
        boolean retry = true;
        while(retry) {
            try {
                thread.join();
                retry = false;
            } catch (InterruptedException e) {
                e.getStackTrace();
                Log.d(TAG, "catch IOException hit in stopPlayback", e);
            }
        }
    }

    public MjpegView(Context context, AttributeSet attrs) { 
        super(context, attrs); init(context); 
    }

    public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) { 
        thread.setSurfaceSize(w, h); 
    }

    public void surfaceDestroyed(SurfaceHolder holder) { 
        surfaceDone = false; 
        stopPlayback(); 
    }

    public MjpegView(Context context) { 
        super(context);
        init(context); 
    }

    public void surfaceCreated(SurfaceHolder holder) { 
        surfaceDone = true; 
    }

    public void showFps(boolean b) { 
        showFps = b; 
    }

    public void setSource(MjpegInputStream source) { 
        mIn = source;
        startPlayback();
    }

    public void setOverlayPaint(Paint p) { 
        overlayPaint = p; 
    }

    public void setOverlayTextColor(int c) { 
        overlayTextColor = c; 
    }

    public void setOverlayBackgroundColor(int c) { 
        overlayBackgroundColor = c; 
    }

    public void setOverlayPosition(int p) { 
        ovlPos = p; 
    }

    public void setDisplayMode(int s) { 
        displayMode = s; 
    }
}
Community
  • 1
  • 1
bbodenmiller
  • 3,101
  • 5
  • 34
  • 50
  • 3
    I created a faster version of the above sample using libjpeg. code https://bitbucket.org/neuralassembly/simplemjpegview demo http://www.youtube.com/watch?v=fumv9p0_vWo – neuralassembly Nov 15 '12 at 17:19
  • If we want to use multiple cameras, what should we do? So we would have a few different URLs and in order to pass the url as an arguement, where should we pass it? – Ayse Apr 11 '13 at 08:53
  • @neuralassembly I clone your bitbucket repo, but managed to use MjpegInputStream & MjpegView to load streaming url in the Fragment. Also use AsyncTaskLoader to do the job. Strangely I can successfully compile and run it on simulator, but the mv is just all black! Is anything I'm missing? – Robert Jun 18 '13 at 09:26
  • @bbodenmiller above code is great! but I just keep run into a loop video playing... – Robert Jun 18 '13 at 09:28
  • @neuralassembly Tried out your code and it works great!, but I have a delay of three seconds, thinking it could be a buffer-issue I'm wondering if there is a way to minimize this issue? – Mazze Jul 02 '13 at 09:35
  • Hi I tried your code and its working but my problem is the streaming is very slow. I am using mjpeg streamer on my raspberry Pi to stream video on port 8080, and on the android side I am using the url like "192.168.1.30:8080/?action=stream";. Any idea ? Thanks in advance.... – Haris Sep 11 '13 at 04:41
  • Might try @neuralassembly's code. – bbodenmiller Sep 11 '13 at 04:59
  • First of all, many thanks for this wonderfull pieace of code. Whoever I have an issue integrating this in a Dialog or inside a layout.. there are huge bars on top and on the bottom of the video, and I've already tried changing the SIZE_* settings. Can anyone help me on this? Thanks in advance. – Henrique de Sousa Oct 16 '13 at 16:00
  • Hello, I'm trying to use your view and having a little problem. If you can stop by http://stackoverflow.com/questions/23319128/mjpegview-with-other-xml-layouts and take a look would be greatly appreciated! Thanks :D – Marcus Gabilheri Apr 27 '14 at 03:57
  • @neuralassembly why this code on `bitbucket.org` is working only when `Bitmap` is 640 x 480 size? I have checked `ImageProc.c` code and always `IMG_WIDTH` is 640 and `IMG_HEIGHT` is 480. Why? I would like to change reosolution of the displaying camera stream image. – woyaru Jun 04 '14 at 12:12
  • What's wrong with this url? i get 401, Unauthorized. My url is http://admin:pucit@192.168.1.126:81/videostream.cgi?user=admin&password=pucit&channel=0&.mjpg – Ahmed Nov 19 '14 at 17:30
  • If I close and reopen the view I get the error: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.graphics.Canvas.drawColor(int)' on a null object reference ... – Alberto M Feb 18 '17 at 06:55

3 Answers3

2

nice work! For your problem with onResume(), isn't it enough when you move the following code from onCreate() to onResume()?

    //sample public cam 
    String URL = "http://trackfield.webcam.oregonstate.edu/axis-cgi/mjpg/video.cgi?resolution=800x600&amp%3bdummy=1333689998337"; 

    mv = new MjpegView(this); 
    setContentView(mv);         

    new DoRead().execute(URL); 

Then you simply recreate the View and new instance of the AsyncTask... I tried it and it works for me...

DC84
  • 53
  • 8
  • Yeah I actually fixed it by moving a bunch of stuff to onResume(). There are also a number of other bugs I found in this code posted here (which was based on even buggier code) that I will post as soon as I get them all fixed. The first one that comes to mind is that in the try block it needs to check if c is null. Another one is that there is no error handling for a source going down midstream. – bbodenmiller May 28 '12 at 10:46
  • You are right... how about your experience on stability? I have noticed on some devices that it can happen that they become unresponsive (CPU @ 100%) using this code... have you ever experienced such problems? – DC84 May 28 '12 at 11:05
  • I've only been able to test using emulator and a Galaxy Nexus but its certainly not the most stable code in the world. I think I may have improved on that area slightly, I will see what I can get fixed and posted today. If your mjpeg stream is giving you a lot of frames per second it will be a rather poor and slow stream on the phone... I had to tone my stream down a bit. 10 fps seems to work okay, anything more I think is just too much processing for the phone to do given this isn't hardware accelerated. – bbodenmiller May 28 '12 at 22:10
  • Thanks for this information, I will make some tests with different cameras. It would be nice to have a look at your improved code, please let me know if you plan to post it here! – DC84 May 31 '12 at 07:45
  • I'll get to it but it might take a few weeks as a lot of other projects are in crunch time at that moment. – bbodenmiller Jun 01 '12 at 06:00
  • Hi there, it really passed some time in the meantime ;-) I just wanted to ask if you have worked on the above code, as mentioned in the past? I would really be interested if you were able to improve the solution! Thx for your Feedback, bye! – DC84 Mar 25 '13 at 08:15
  • Me too, I am really interested to see if you could improve the code above. I have a lot issues when the reception is low. I am looking for improve it. Could you give us some advices? Thanks you! – Juan Pedro Martinez Oct 09 '13 at 11:35
  • It is unlikely that I will ever work on this again as the application is now obsolete. Consider trying @neuralassembly's code mentioned in the comments of the original question. – bbodenmiller Feb 17 '14 at 02:45
  • OK, anyways thank you for the feedback and your help in the past! – DC84 Feb 18 '14 at 07:11
  • What can we do for ip cameras with usernames and passwords? i'm using this url "http://admin:pucit@192.168.1.126:81/videostream.cgi?user=admin&password=pucit&channel=0&.mjpg" but getting 401, Unauthorized – Ahmed Nov 19 '14 at 17:32
2

It'll be helpful for the newbies that if you want to access your ip camera having a username or password , you might want to add this to your DefaultHttpClient and the above code will work for cameras that require authentication

 CredentialsProvider provider = new BasicCredentialsProvider();
            UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("yourusername", "yourpassword");
            provider.setCredentials(AuthScope.ANY, credentials);
            DefaultHttpClient httpclient = new DefaultHttpClient();
            httpclient.setCredentialsProvider(provider);
Ahmed
  • 2,176
  • 5
  • 26
  • 40
0

thanks for the code, it's very helpful

I want to suggest few optimization tips, which are already used in my code, overall performance can be easily increased by a few times.

  1. I removed memory allocations during frame reading, where possible

    private final static int HEADER_MAX_LENGTH = 100;
    private final static int FRAME_MAX_LENGTH = 200000 + HEADER_MAX_LENGTH;
    private final String CONTENT_LENGTH = "Content-Length:";
    private final String CONTENT_END = "\r\n";
    private final static byte[] gFrameData = new byte[FRAME_MAX_LENGTH];
    private final static byte[] gHeader = new byte[HEADER_MAX_LENGTH];
    BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
    
    public Bitmap readMjpegFrame() throws IOException {
    
        mark(FRAME_MAX_LENGTH);
        int headerLen = getStartOfSequence(SOI_MARKER);
    
        if(headerLen < 0)
            return false;
    
        reset();
        readFully(gHeader, 0, headerLen);
    
        int contentLen;
    
        try
        {
            contentLen = parseContentLength(gHeader, headerLen);
        } catch (NumberFormatException nfe) 
        {
            nfe.getStackTrace();
            Log.d(TAG, "catch NumberFormatException hit", nfe);
            contentLen = getEndOfSequence(EOF_MARKER);
        }
    
        readFully(gFrameData, 0, contentLen);
    
        Bitmap bm = BitmapFactory.decodeByteArray(gFrameData, 0, contentLen, bitmapOptions);
        bitmapOptions.inBitmap = bm;
    
        return bm;
    }
    
  2. Optimizing parseContentLength, removing String operations as much as possible

    byte[] CONTENT_LENGTH_BYTES;
    byte[] CONTENT_END_BYTES;
    
    public MjpegInputStream(InputStream in)
    {
        super(new BufferedInputStream(in, FRAME_MAX_LENGTH));
    
        bitmapOptions.inSampleSize = 1;
        bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        bitmapOptions.inPreferQualityOverSpeed = false;
        bitmapOptions.inPurgeable = true;
        try
        {
            CONTENT_LENGTH_BYTES = CONTENT_LENGTH.getBytes("UTF-8");
            CONTENT_END_BYTES = CONTENT_END.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) 
        {
            e.printStackTrace();
        }
    }
    
    private int findPattern(byte[] buffer, int bufferLen, byte[] pattern, int offset)
    {
        int seqIndex = 0;
        for(int i=offset; i < bufferLen; ++i)
        {
            if(buffer[i] == pattern[seqIndex])
            {
                ++seqIndex;
                if(seqIndex == pattern.length)
                {
                    return i + 1;
                }
            } else
            {
                seqIndex = 0;
            }
        }
    
        return -1;
    }
    
    
    
    private int parseContentLength(byte[] headerBytes, int length) throws IOException, NumberFormatException
    {
        int begin = findPattern(headerBytes, length, CONTENT_LENGTH_BYTES, 0);
        int end = findPattern(headerBytes, length, CONTENT_END_BYTES, begin) - CONTENT_END_BYTES.length;
    
        // converting string to int
        int number = 0;
        int radix = 1;
        for(int i = end - 1; i >= begin; --i)
        {
            if(headerBytes[i] > 47 && headerBytes[i] < 58)
            {
                number += (headerBytes[i] - 48) * radix;
                radix *= 10;
            }
        }
    
        return number;
    }
    

There could be mistakes in the code since I was rewriting it for stackoverflow, originally I'm using 2 threads, one is reading frames and another is rendering.

I hope it will help for someone.

Sergej Werfel
  • 1,335
  • 2
  • 14
  • 25
Rusty
  • 143
  • 1
  • 7