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


    Многие разработчики, особенно начинающие, пользуются эмулятором 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 и выше. Для старых платформ выше описанный способ все еще актуален.
    Поделиться публикацией

    Комментарии 11

      +1
      Мой порог входа в iPhone был таким:
      1) 250$ iPod Touch 2G (на 2008 год)
      3) 800$ MacMini (но тут есть варианты и за $400 (не VmWare) )
      Итого: от $650 до $1050 если без iPad (c iPad +500$)

      +100$ в год на лицензию.
        +1
        Спасибо, очень интересно.

        Пользуясь случаем, хочу спросить.
        Меня все время интересовало, как сделать так, чтобы в эмуляторе Андроида ниже 2.2 не падала камера, когда она показывает вот эти самые квадраты.
          0
          По моим наблюдениям, камера падала в эмуляторе 2.2 (пробовал на четырёх или пяти машинах под Windows и GNU/Linux). А вот в эмуляторе 2.1 работала нормально.
          +2
          Исходники 3ей версии были закрыты, потому что гугл не успела допилить андроид для планшетов и сделала костыль в виде ханикомба, который выдавала вендорам под строгими договоренностями. С сэндвичем будет все в порядке и опенсурсно.
            +1
            Исходники ICS открыты (в том числе и исходники SDK), так что их можно допиливать как душе угодно.
            +1
            а разве в новом sdk работа с камерой не реализована?
            Я ставил недавно на ноутбук и оно подхватывало встроеную вебку.
              0
              О да, пишут что поддержка вебкамер в новом релизе есть, но мне не удалось ее включить. Возможно я что-то делал не так.
              developer.android.com/sdk/tools-notes.html
              В тоже время в официальной документации пишут, что ограничение для эмулятора — получение видеоданных с устройства.
              developer.android.com/guide/developing/devices/emulator.html (В самом низу).
              Потому не стал рыть дальше и использовал велосипед.
              0
              А в системе при этом появляется новое устройство Вебкамера? Можно в данном случае использовать для скайпа, например?
                0
                Нет, но похоже что этот метод уже не актуален.
                developer.android.com/sdk/tools-notes.html
                Пишут, что в новом SDK, начиная с 4.0 версии андроид вебкамера работает.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое