Sunday, September 22, 2013

Android Rover Client: Dirt Simple Video Streaming, Part II

After a brief hiatus for summer in Ohio, some travel, and a whole lot of work, I've returned to the rover project and decided to write an Android client to drive the rover from. I started with trying to get the video feed working first.

You might recall that I'm using a very crude way of pulling video from the rover. The Android phone that controls the rover is running IP Webcam, and the client just pulls static JPEG images and displays them. This was very easy on the PC Java client, but there was a little bit of a challenge to overcome getting it to work on another Android phone.

The GUI components on Android run in a standard Activity. You can't do any time consuming work in the GUI thread or the OS will shut it down to enforce a reasonable user experience. My initial thought was to launch a new thread and repeatedly download the image in another thread and display it. I quickly found out that you can not update an ImageView from any thread except the main GUI thread, so I started looking at other approaches.

I settled on AsynchTask, which is made for precisely this sort of thing. If you want to do a time consuming background task that then interacts with the GUI, AsyncTask is a good place to start. It abstracts away the work of thread handling for you.

I started with an AsyncTask to download and display an image. I used a function that I found (reference given in the code sample below). This worked - it pulled a single image and displayed it. At that point I just needed to figure out how to wrap it in a loop, and I was good.

The trick to running AsyncTasks sequentially is to know that you can launch a function upon completion. I just had the AsyncTask call a launcher function to start another instance of itself as it completes.

If you try something like:

while (true)
     run_asynctask;

it won't work - it will try to launch them in parallel, which is not allowed. If you instead call the launcher function from the onPostExecute() of the AsyncTask, it runs sequentially. You can then add a conditional in your launcher to switch the feed on and off.

This code was tested and streams 320x240 JPEG frames from one phone to the other fairly smoothly, just like the PC client.

Code:

All this goes in the GUI thread: I start with a call from onCreate():

vidLoop();

Launcher function:

void vidLoop() 
    {
     if (connected == 1)
      {
      ImageDownloader id = new ImageDownloader();
      id.execute(vidURL); //vidURL is a String with the URL of the image you want
      }
    }



Async Task Code. This downloads an image from a URL and displays it in an ImageView called imageView1, then calls vidLoop() upon completion to do it again.



//this very useful chunk of code is from http://www.peachpit.com/articles/article.aspx?p=1823692&seqNum=3
    private class ImageDownloader 
    extends AsyncTask{
    protected void onPreExecute(){
            //Setup is done here
        }
        @Override
        protected Bitmap doInBackground(String... params) {
            //TODO Auto-generated method stub
            try{
                URL url = new URL(params[0]);
                HttpURLConnection httpCon = 
                (HttpURLConnection)url.openConnection();
                if(httpCon.getResponsepre() != 200)
                    throw new Exception("Failed to connect");
                InputStream is = httpCon.getInputStream();
                return BitmapFactory.depreStream(is);
            }catch(Exception e){
                Log.e("Image","Failed to load image",e);
            }
            return null;
        } 
        protected void onProgressUpdate(Integer... params){
            //Update a progress bar here, or ignore it, it's up to you
        }
        protected void onPostExecute(Bitmap img){
            ImageView iv = (ImageView)findViewById(R.id.imageView1);
            if(iv!=null && img !=null){
                iv.setImageBitmap(img);
                //start next image grab
                vidLoop();
            }
        }
            protected void onCancelled(){
            }
        }