Pull to refresh

Как воспользоваться вебкамерой в эмуляторе Android

Reading time 8 min
Views 28K

Многие разработчики, особенно начинающие, пользуются эмулятором Android для создания своих приложений. Это позволяет делать очень многое без подключения телефона. Почти всё. Вот именно это «почти» и относится, например, к вебкамере. Для большинства приложений может быть и достаточно будет такого вида, ведь можно передать «нужное» изображение на обработчик, а окончательно протестировать уже на реальном устройстве. Для приложений которые работают с дополненой реальностью так работать будет совсем не удобно. Хотелось бы иметь видеопоток. В случае если телефона с андроидом под рукой нет — это проблема.
В прошлой заметке я писал о том, как работают методы распознавания маркера дополненой реальности. Данная статья будет посвящена тому, как воспользоваться вебкамерой в эмуляторе Android.

Суть проблемы


Если мы используем эмулятор и камеру в нашем приложении, то на выходе получаем приблизительно такую картинку:

Хотелось бы, чтобы вместо этого были изображения с вебкамеры компьютера.

Подходы к решению


1) Можно доработать эмулятор Андроид, который в opensource.
2) Передать поток с вебкамеры по сети, и использовать его в приложении.
3) Использовать Android x86.

Исходники эмулятора для honeycomb недоступны с марта 2011, есть доступные deprecated версии, с ними работать весело, но непродуктивно. А жаль, это был бы наиболее приемлимый вариант. Тем более опыт допиливания приложений, которые работают с видеопотоком и v4l2 под Linux есть.
Androidx86 — похоже выход, это может помочь многим, но… У меня поднять веб-камеру не получилось.
Первый и третий подход нам недоступен, тогда будем бороться за второй подход.
Идея давно уже предложена и даже реализована для старых версий эмулятора и API. Для новых версий API предложено решение, но исключительно для JMF. Отличное решение, но мою камеру JMF не распознал. Заставить камеру работать с JMF не получилось ни в Linux, ни в Windows(возможно я что-то делал не так, по идее это решение должно запускаться в Windows). Все дальнейшие действия я проводил уже исключительно в Linux. Модифицируем решение этой проблемы на базе уже готового кода.
В исходном коде реализована классическая система клиент-сервер. Сервер на компьютере вещает в сеть картинки с камеры, а клиент в эмуляторе (в приложении) принимает эти картинки.

Что установлено


JDK
Android SDK
Eclipse+google ADT
v4l4j
/dev/video0 — вебкамера.

Сервер


Для создания сервера я решил двигаться по пути наименьшего сопротивления и воспользоваться библиотекой v4l4j — которая прекрасно поддерживает мою камеру.
Необходимо доработать исходный код таким образом, чтобы вместо JMF он использовал v4l4j. Вот что получилось.

измененный WebBroadcaster(привожу код полностью, чтобы была понятна логика работы. Автор Tom Gibara, я лишь адаптировал под v4l4j):

package com.webcambroadcaster;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import au.edu.jcu.v4l4j.DeviceInfo;
import au.edu.jcu.v4l4j.FrameGrabber;
import au.edu.jcu.v4l4j.V4L4JConstants;
import au.edu.jcu.v4l4j.VideoDevice;
import au.edu.jcu.v4l4j.VideoFrame;


/**
 * A disposable class that uses JMF to serve a still sequence captured from a
 * webcam over a socket connection. It doesn't use TCP, it just blindly
 * captures a still, JPEG compresses it, and pumps it out over any incoming
 * socket connection.
 * 
 * @author Tom Gibara
 *
 */


public class WebcamBroadcaster {

 public static boolean RAW = false; 
 
 public static void main(String[] args) {
  int[] values = new int[args.length];
  for (int i = 0; i < values.length; i++) {
   values[i] = Integer.parseInt(args[i]);
  }
  //Parse inputs
  WebcamBroadcaster wb;
  if (values.length == 0) {
   wb = new WebcamBroadcaster();
  } else if (values.length == 1) {
   wb = new WebcamBroadcaster(values[0]);
  } else if (values.length == 2) {
   wb = new WebcamBroadcaster(values[0], values[1]);
  } else {
   wb = new WebcamBroadcaster(values[0], values[1], values[2]);
  }
  //Start the grabbing procedure
  wb.start();
 }
 
 public static final int DEFAULT_PORT = 9889;
 public static final int DEFAULT_WIDTH = 320;
 public static final int DEFAULT_HEIGHT = 240;
 
 private final Object lock = new Object();
 
 private final int width;
 private final int height;
 private final int port;
 
 private boolean running;
 
 private boolean stopping;
 private Worker worker;
 private VideoDevice vd=null;
 private FrameGrabber fg=null;
 
 public WebcamBroadcaster(int width, int height, int port) {
  this.width = width;
  this.height = height;
  this.port = port;
 }

 public WebcamBroadcaster(int width, int height) {
  this(width, height, DEFAULT_PORT);
 }

 public WebcamBroadcaster(int port) {
  this(DEFAULT_WIDTH, DEFAULT_HEIGHT, port);
 }

 public WebcamBroadcaster() {
  this(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_PORT);
 }
 

 
 public void start() {
  synchronized (lock) {
   if (running) return;
   //Starting capture
   startCapture();

   worker = new Worker();
   worker.start();
   System.out.println("Start capture");
   running = true;
  }
 }

 public void releaseCapture(){
     fg.stopCapture();
     vd.releaseFrameGrabber();
     vd.release();
 }
 
 public void startCapture(){
	   try{
	       String dev = "/dev/video0";
	       vd = new VideoDevice(dev);	      	      
	       fg = vd.getJPEGFrameGrabber(width, height, 0, 0, 80);
	       fg.startCapture();       
	   }catch(Exception e){
		   e.printStackTrace();
	   }
 }
 
 
 public void stop() throws InterruptedException {
  synchronized (lock) {
   if (!running) return;
   //
   //Stop capture at this place
   releaseCapture();
   //
   stopping = true;
   running = false;
   worker = null;
  }
  try {
   worker.join();
  } finally {
   stopping = false;
  }
 }

 private class Worker extends Thread {
  
  private final int[] data = new int[width*height];
  public byte[] b=null;
  @Override
  public void run() {
   ServerSocket ss; 
   VideoFrame frm;
   try {
    ss = new ServerSocket(port);
    
   } catch (IOException e) {
    e.printStackTrace();
    return;
   }
   
   while(true) {
    synchronized (lock) {

     if (stopping) break;
    }
    Socket socket = null;
    
    try {
     socket = ss.accept();
     //Grab image here
     try{
     	//
     	frm = fg.getVideoFrame();
        System.out.println("Datagrabbed");  
        OutputStream out = socket.getOutputStream();
        DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
        dout.write(frm.getBytes(), 0, frm.getFrameLength());
        dout.close();
        System.out.println("Datasent");
        frm.recycle();
         //
     }catch(Exception e){
    	 e.printStackTrace();
    	 return;
     }         
     socket.close();
     socket = null;
    } catch (IOException e) {
     e.printStackTrace();
    } finally {
     if (socket != null)
      try {
       socket.close();
      } catch (IOException e) {
       /* ignore */
      }
    }
    
   }   
   try {
    ss.close();
   } catch (IOException e) {
    /* ignore */
   }
  }

 }
 
}



Какая логика работы:
При запуске включаем камеру и подготавливаемся к получению изображений:
 public void startCapture(){
	   try{
	       String dev = "/dev/video0";
	       vd = new VideoDevice(dev);	      	      
	       fg = vd.getJPEGFrameGrabber(width, height, 0, 0, 80);
	       fg.startCapture();       
	   }catch(Exception e){
		   e.printStackTrace();
	   }
 }

Потом, когда клиент соединяется — высылаем ему изображение в поток:
  try{
     	//
     	frm = fg.getVideoFrame();
        System.out.println("Datagrabbed");  
        OutputStream out = socket.getOutputStream();
        DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(out));
        dout.write(frm.getBytes(), 0, frm.getFrameLength());
        dout.close();
        System.out.println("Datasent");
        frm.recycle();
         //
     }catch(Exception e){
    	 e.printStackTrace();
    	 return;
     }         


Из доработок на будущее— код использует устаревшее getVideoFrame(), который надо-бы заменить на вызов callback функции при появлении нового фрэйма на камере, но пришлось бы вносить изменения также и в логику работы всей связки, потому оставил все как есть, Возможно перепишу лучше позже, когда будет время. Ведь это вспомогательная функция на этапе разработки… В идеале необходимо сделать чтобы программа читала поток в формате MJPEG, т. е. парсила multipart/x-mixed ответ от HTTP сервера и рисовала картинки по мере поступления.

Клиент


Cсылка на классический пример использования обычной камеры. Мы его немного сократим, упростим(в целях обучения и тестирования) и получим вот такой пример для обычной камеры.

Важные строки: Класс который будет отвечать за отображение.

preview = new Preview(this);
((FrameLayout) findViewById(R.id.preview)).addView(preview);	


И сам класс:

package com.example;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.FrameLayout;

public class CameraDemo extends Activity {
	private static final String TAG = "CameraDemo";
	Preview preview;
	Button buttonClick;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		preview = new Preview(this);
		((FrameLayout) findViewById(R.id.preview)).addView(preview);		

		Log.d(TAG, "It were created");
	}

}

А внутри этого класса Preview, перерисовываем каждый раз поверхность на который выводится предпросмотр.
package com.example;

import java.io.IOException;
import android.content.Context;
import android.hardware.Camera;
import android.view.SurfaceHolder;
import android.view.SurfaceView;


class Preview extends SurfaceView implements SurfaceHolder.Callback {

    SurfaceHolder mHolder;
        public Camera camera;
    
    
    Preview(Context context) {
        super(context);
        
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        	    mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, acquire the camera and tell it where
        // to draw.
    	camera = Camera.open();
        try {
			camera.setPreviewDisplay(holder);
			
        } catch (IOException e) {
			e.printStackTrace();
		}
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
        // Because the CameraDevice object is not a shared resource, it's very
        // important to release it when the activity is paused.
        camera.stopPreview();
        camera = null;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(w, h);
        camera.setParameters(parameters);
        camera.startPreview();
    }

 
}

Если этот код запустить в эмуляторе, то получим квадратик как на скриншоте выше.
Теперь, модифицируем класс таким образом чтобы он показывал картинки с нашего сервера.
1) Возьмем исходник класса SocketCamera отсюда. Добавим в наш проект.
2) Изменим исходный код класса Preview таким образом:
package com.example;


import java.io.IOException;

import android.content.Context;

import android.hardware.Camera;

import android.view.SurfaceHolder;
import android.view.SurfaceView;


class Preview extends SurfaceView implements SurfaceHolder.Callback {

    SurfaceHolder mHolder;
    //public Camera camera;
    public SocketCamera camera;
    
    Preview(Context context) {
        super(context);
        
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = getHolder();
        mHolder.addCallback(this);
        //mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_NORMAL);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // The Surface has been created, acquire the camera and tell it where
        // to draw.
        //camera = Camera.open();
    	camera = SocketCamera.open();
        try {
			camera.setPreviewDisplay(holder);
			
        } catch (IOException e) {
			e.printStackTrace();
		}
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return, so stop the preview.
        // Because the CameraDevice object is not a shared resource, it's very
        // important to release it when the activity is paused.
        camera.stopPreview();
        camera = null;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // Now that the size is known, set up the camera parameters and begin
        // the preview.
        Camera.Parameters parameters = camera.getParameters();
        parameters.setPreviewSize(w, h);
        camera.setParameters(parameters);
        camera.startPreview();
    }

 
}


Результат


Теперь запустим сервер:
java -Djava.library.path=/opt/Android/v4l4j-0.8.10 -cp "/opt/Android/v4l4j-0.8.10/v4l4j.jar:./" com/webcambroadcaster/WebcamBroadcaster

где -Djava.library.path=/opt/Android/v4l4j-0.8.10 путь к вашей библиотеке v4l4j

Запустим приложение CameraDemo на эмуляторе. В результате получим видеопоток в с вебкамеры в эмуляторе. Поток немного дергает, идет с запозданием, но это лучше чем ничего.



Исходники клиента: CameraDemo.zip
Исходники сервера: WebBroadcaster.zip

Послесловие


Задумался о существующем пороге входа в технологию. Вроде бы и небольшой порог, но покупка устройства очень часто недоступна начинающему разработчику. Эмулятор снимает этот порог лишь частично.
А какой же тогда порог входа в разработку приложений под iPhone?

Столкнувшись с тем, что участник сообщества открытого кода закрывет его после некоторого времени, возникают вопросы:
А в прибыли ли только дело?
Может быть работа сообщества не оправдала ожиданий Google?
Неужели теперь есть что скрывать от сообщества?
А что теряет гигант, закрываясь от изучения и дополнения кода сторонними разработчиками?

UPD: Это все возможно уже и не актуально. skl1f подсказывает, что камера поддерживается в SDK.
developer.android.com/sdk/tools-notes.html — вроде документация говорит да, а официальный мануал: developer.android.com/guide/developing/devices/emulator.html — нет. Надо пробовать.

UPD2: Проверил. Камера в эмуляторе работает и доступна для SDK tools rev. 14 и выше и только для Android 4.0 и выше. Для старых платформ выше описанный способ все еще актуален.
Tags:
Hubs:
+32
Comments 11
Comments Comments 11

Articles