Tutorial: Making a spycam

donv edited this page Aug 24, 2012 · 19 revisions

Goal

Start a webserver that serves a fresh picture taken by the on board camera on each request. Concepts covered:

  • Extending stack size
  • Synchronization of common background job
  • Starting a webserver on the device
  • Accessing the device camera
  • Force the screen to stay on
  • Adjusting the system volume

Prerequisites

You should have completed the Getting started with Ruboto tutorial. This tutorial accesses the camera of the device, so it doesn't make much sense to develop on an emulator. Use a real device with a camera.

This tutorial has been tested with the following setups

Platform JDK ant Ruby ruboto RubotoCore Device API level Tester
OS X 10.8.0 1.6.0_33 1.8.2 MRI 1.8.7 0.8.1.dev 0.4.7 HTC Desire HD android-10 donv
OS X 10.8.0 1.6.0_33 1.8.2 MRI 1.8.7 0.8.0 0.4.7 HTC Desire HD android-10 donv
OS X 10.7.3 1.6.0_31 1.8.2 MRI 1.8.7 0.6.0.rc.3 0.4.7 HTC Desire android-8 donv
OS X 10.7.3 1.6.0_31 1.8.2 MRI 1.8.7 0.6.0.rc.3 0.4.7 HTC Desire HD android-10 donv
OS X 10.7.3 1.6.0_31 1.8.2 MRI 1.8.7 0.6.0.rc.3 0.4.7 ASUS TF101 android-15 donv

Create the app, install, and run it

Connect your device.

ruboto gen app --package org.ruboto.example.spycam
cd spycam
rake update_scripts:restart

You should see the standard "What hath Matz wrought?" screen.

A fresh app

Add a webserver

Add a new file src/spycam_server.rb:

require 'monitor'
require 'camera_helper'
require 'ruboto/util/stack'

class SpycamServer
  extend MonitorMixin

  PORT = 4567

  @@server = nil
  @@camera_data = nil

  def self.start(activity, camera)
    Thread.with_large_stack(512) do
      synchronize do
        if @@server.nil?
          activity.server_status = "Loading"
          require 'webrick'
          activity.server_status = "Loaded"
          @@server = WEBrick::HTTPServer.new(:Port => PORT, :DocumentRoot => "#{activity.files_dir.absolute_path}/")

          @@server.mount_proc('/') do |req, resp|
            case req.path
            when '/', 'index.html'
              CameraHelper.take_picture(self, camera, activity)
              resp.content_type = "text/html"
              resp.body = '<html>
                              <head>
                                <title>Spycam</title>
                              </head>
                              <body>
                                <a href="/"><img src="latest.jpg"></a>
                              </body>
                            </html>'
              raise WEBrick::HTTPStatus::OK
            when '/latest.jpg'
              resp.content_type = "image/jpg"
              resp.body = @@camera_data
              @@camera_data = nil
              raise WEBrick::HTTPStatus::OK
            else
              resp.body = "Unknown path: #{req.path.inspect}"
              raise WEBrick::HTTPStatus::NotFound
            end
          end
          server = @@server
          Thread.new{server.start}
        end
        activity.server_status = "WEBrick started on http://#{get_local_ip_address(activity)}:#{PORT}/"
      end
    end
  end

  def self.camera_data=(data)
    @@camera_data = data
  end

  def self.stop
    synchronize do
      if @@server
        @@server.shutdown
        sleep 0.1 until @@server.status == :Stop
        @@server = nil
      end
    end
  end

  private

  def self.get_local_ip_address(context)
    ip = context.get_system_service(context.class::WIFI_SERVICE).connection_info.ip_address
    return "localhost" if ip == 0
    [0, 8, 16, 24].map{|n| ((ip >> n) & 0xff).to_s}.join(".")
  end

end

Add permissions

Add the following lines to you AndroidManifest.xml file, just after the uses_sdk tag:

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-feature android:name="android.hardware.camera"/>
    <uses-feature android:name="android.hardware.camera.autofocus"/>

Activate the web server

Edit src/spycam_activity.rb to this:

require 'ruboto/activity'
require 'ruboto/widget'
require 'spycam_server'

import android.util.Log
import android.view.Surface
import android.view.WindowManager

ruboto_import_widgets :Button, :LinearLayout, :ScrollView, :TextView
ruboto_import_widget :SurfaceView, "android.view"

class SpycamActivity
  def on_create(bundle)
    super
    rotation = {
        Surface::ROTATION_0 => 0,Surface::ROTATION_90 => 90,Surface::ROTATION_180 => 180,Surface::ROTATION_270 => 270
    }[window_manager.default_display.rotation]
    self.title = "Spycam #{rotation}"
    # self.setRequestedOrientation(android.content.pm.ActivityInfo::SCREEN_ORIENTATION_PORTRAIT)
    window.add_flags(WindowManager::LayoutParams::FLAG_KEEP_SCREEN_ON)

    setContentView(linear_layout(:orientation => :vertical) do
      linear_layout do
        text_view :text => "Server: "
        @server_status_view = text_view
      end
      linear_layout do
        text_view :text => "Picture: "
        @camera_status_view = text_view
      end

      sv = surface_view
      @surface_holder_callback = RubotoSurfaceHolderCallback.new(self, rotation)
      sv.holder.add_callback @surface_holder_callback
      # Deprecated, but still required for older API version
      sv.holder.set_type android.view.SurfaceHolder::SURFACE_TYPE_PUSH_BUFFERS
    end)
  end
  
  def camera_status=(value)
    run_on_ui_thread { @camera_status_view.text = value }
  end

  def server_status=(value)
    run_on_ui_thread { @server_status_view.text = value }
  end

end

class RubotoSurfaceHolderCallback
  def initialize(activity, roation)
    @activity = activity
    @rotation = roation
  end

  def surfaceCreated(holder)
    puts 'RubotoSurfaceHolderCallback#surfaceCreated'
    @camera = android.hardware.Camera.open
    parameters = @camera.parameters
    parameters.picture_format = android.graphics.PixelFormat::JPEG
    parameters.rotation = (360 + (90 - @rotation)) % 360
    parameters.set_picture_size(640, 480)
    @camera.parameters = parameters
    @camera.preview_display = holder
    @camera.display_orientation = (360 + (90 - @rotation)) % 360
    @camera.start_preview
    SpycamServer.start(@activity, @camera)
  end

  def surfaceChanged(holder, format, width, height)
  end

  def surfaceDestroyed(holder)
    SpycamServer.stop
    @camera.stop_preview
    @camera.release
    @camera = nil
  end
end

Take a picture

Add a new file src/camera_helper.rb:

class CameraHelper
  def self.take_picture(server, camera, activity)
    activity.camera_status = "Set volume..."
    am = activity.getSystemService(android.content.Context::AUDIO_SERVICE)
    old_volume = am.get_stream_volume(android.media.AudioManager::STREAM_SYSTEM)
    am.set_stream_volume(android.media.AudioManager::STREAM_SYSTEM, 0, 0)

    activity.camera_status = "Taking picture..."
    picture_taken = false
    camera.take_picture(nil, nil) do |data, camera|
      server.camera_data = String.from_java_bytes(data)
      activity.camera_status = "Gotcha!"

      camera.start_preview
      am.set_stream_volume(android.media.AudioManager::STREAM_SYSTEM, old_volume, 0)
      picture_taken = true
    end
    sleep 0.1 until picture_taken
  end
end

Redeploy the application with new scripts

rake update_scripts:restart

The app should restart and show two lines of information:

Server: WEBrick started on http://xxx.xxx.xxx.xxx:4567/
Picture: 

Point a web browser to the given URL. You should get a web page with a picture from the camera. Click the camera to update the picture.

Enjoy!

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.