-Add a mojo service to get video frames from the camera through an android service -Add an example mojo app that displays this video in its view. -Some re-organization around the camera mojo definition. R=alhaad@google.com, qsr@chromium.org Review URL: https://codereview.chromium.org/1375733004 .
diff --git a/examples/dart/camera_roll/lib/main.dart b/examples/dart/camera_roll/lib/main.dart index d0a1bd7..d0ab8a7 100644 --- a/examples/dart/camera_roll/lib/main.dart +++ b/examples/dart/camera_roll/lib/main.dart
@@ -89,7 +89,7 @@ } void main() { - embedder.connectToService("mojo:camera_roll", cameraRoll); + embedder.connectToService("mojo:camera", cameraRoll); view.setFrameCallback(beginFrame); view.setEventCallback(handleEvent); getPhoto();
diff --git a/examples/dart/camera_video/lib/main.dart b/examples/dart/camera_video/lib/main.dart new file mode 100644 index 0000000..4637a21 --- /dev/null +++ b/examples/dart/camera_video/lib/main.dart
@@ -0,0 +1,79 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This example makes use of mojo:camera which is available only when +// running on Android. It repeatedly captures camera video frame images +// and displays it in a mojo view. +// +// Example usage: +// pub get +// pub run sky_tools build +// pub run sky_tools run_mojo --mojo-path=../../.. --android + +import 'dart:sky'; +import 'dart:typed_data'; + +import 'package:mojo_services/mojo/camera.mojom.dart'; +import 'package:sky/services.dart'; + +Image image = null; +final CameraServiceProxy camera = new CameraServiceProxy.unbound(); + +Picture paint(Rect paintBounds) { + PictureRecorder recorder = new PictureRecorder(); + if (image != null) { + Canvas canvas = new Canvas(recorder, paintBounds); + canvas.translate(paintBounds.width / 2.0, paintBounds.height / 2.0); + canvas.scale(0.3, 0.3); + Paint paint = new Paint()..color = const Color.fromARGB(255, 0, 255, 0); + canvas.drawImage(image, new Point(-image.width / 2.0, -image.height / 2.0), paint); + } + return recorder.endRecording(); +} + +Scene composite(Picture picture, Rect paintBounds) { + final double devicePixelRatio = view.devicePixelRatio; + Rect sceneBounds = new Rect.fromLTWH( + 0.0, 0.0, view.width * devicePixelRatio, view.height * devicePixelRatio); + Float32List deviceTransform = new Float32List(16) + ..[0] = devicePixelRatio + ..[5] = devicePixelRatio + ..[10] = 1.0 + ..[15] = 1.0; + SceneBuilder sceneBuilder = new SceneBuilder(sceneBounds) + ..pushTransform(deviceTransform) + ..addPicture(Offset.zero, picture, paintBounds) + ..pop(); + return sceneBuilder.build(); +} + +void beginFrame(double timeStamp) { + Rect paintBounds = new Rect.fromLTWH(0.0, 0.0, view.width, view.height); + Picture picture = paint(paintBounds); + Scene scene = composite(picture, paintBounds); + view.scene = scene; +} + +void drawNextPhoto() { + var future = camera.ptr.getLatestFrame(); + future.then((response) { + if (response.content == null) { + drawNextPhoto(); + return; + } + new ImageDecoder(response.content.handle.h, (frame) { + if (frame != null) { + image = frame; + view.scheduleFrame(); + drawNextPhoto(); + } + }); + }); +} + +void main() { + view.setFrameCallback(beginFrame); + embedder.connectToService("mojo:camera", camera); + drawNextPhoto(); +} \ No newline at end of file
diff --git a/examples/dart/camera_video/pubspec.lock b/examples/dart/camera_video/pubspec.lock new file mode 100644 index 0000000..532f213 --- /dev/null +++ b/examples/dart/camera_video/pubspec.lock
@@ -0,0 +1,219 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.26.1+7" + archive: + description: archive + source: hosted + version: "1.0.20" + args: + description: args + source: hosted + version: "0.13.2" + async: + description: async + source: hosted + version: "1.3.0" + barback: + description: barback + source: hosted + version: "0.15.2+7" + cassowary: + description: cassowary + source: hosted + version: "0.1.7" + charcode: + description: charcode + source: hosted + version: "1.1.0" + collection: + description: collection + source: hosted + version: "1.1.3" + concepts: + description: concepts + source: hosted + version: "0.2.0" + crypto: + description: crypto + source: hosted + version: "0.9.1" + csslib: + description: csslib + source: hosted + version: "0.12.1" + either: + description: either + source: hosted + version: "0.1.8" + glob: + description: glob + source: hosted + version: "1.0.5" + html: + description: html + source: hosted + version: "0.12.2" + http_multi_server: + description: http_multi_server + source: hosted + version: "1.3.2" + http_parser: + description: http_parser + source: hosted + version: "1.0.0" + intl: + description: intl + source: hosted + version: "0.12.4+2" + logging: + description: logging + source: hosted + version: "0.11.1+1" + matcher: + description: matcher + source: hosted + version: "0.12.0+1" + material_design_icons: + description: material_design_icons + source: hosted + version: "0.0.3" + mime: + description: mime + source: hosted + version: "0.9.3" + mojo: + description: mojo + source: hosted + version: "0.1.0" + mojo_services: + description: mojo_services + source: hosted + version: "0.1.0" + mojom: + description: mojom + source: hosted + version: "0.1.0" + mustache4dart: + description: mustache4dart + source: hosted + version: "1.0.10" + newton: + description: newton + source: hosted + version: "0.1.4" + option: + description: option + source: hosted + version: "1.1.0" + package_config: + description: package_config + source: hosted + version: "0.1.3" + path: + description: path + source: hosted + version: "1.3.6" + petitparser: + description: petitparser + source: hosted + version: "1.4.3" + plugin: + description: plugin + source: hosted + version: "0.1.0" + pool: + description: pool + source: hosted + version: "1.1.0" + pub_semver: + description: pub_semver + source: hosted + version: "1.2.2" + quiver: + description: quiver + source: hosted + version: "0.21.4" + shelf: + description: shelf + source: hosted + version: "0.6.3" + shelf_path: + description: shelf_path + source: hosted + version: "0.1.7" + shelf_route: + description: shelf_route + source: hosted + version: "0.13.5" + shelf_static: + description: shelf_static + source: hosted + version: "0.2.3+1" + shelf_web_socket: + description: shelf_web_socket + source: hosted + version: "0.0.1+4" + sky: + description: sky + source: hosted + version: "0.0.51" + sky_engine: + description: sky_engine + source: hosted + version: "0.0.29" + sky_services: + description: sky_services + source: hosted + version: "0.0.29" + sky_tools: + description: sky_tools + source: hosted + version: "0.0.15" + source_map_stack_trace: + description: source_map_stack_trace + source: hosted + version: "1.0.4" + source_maps: + description: source_maps + source: hosted + version: "0.10.1" + source_span: + description: source_span + source: hosted + version: "1.2.1" + stack_trace: + description: stack_trace + source: hosted + version: "1.4.2" + string_scanner: + description: string_scanner + source: hosted + version: "0.1.4" + test: + description: test + source: hosted + version: "0.12.4+9" + uri: + description: uri + source: hosted + version: "0.11.0" + utf: + description: utf + source: hosted + version: "0.9.0+2" + vector_math: + description: vector_math + source: hosted + version: "1.4.3" + watcher: + description: watcher + source: hosted + version: "0.9.7" + yaml: + description: yaml + source: hosted + version: "2.1.6"
diff --git a/examples/dart/camera_video/pubspec.yaml b/examples/dart/camera_video/pubspec.yaml new file mode 100644 index 0000000..0e1e628 --- /dev/null +++ b/examples/dart/camera_video/pubspec.yaml
@@ -0,0 +1,5 @@ +name: camera +dependencies: + mojo_services: any + sky: any + sky_tools: any
diff --git a/mojo/services/camera_roll/public/interfaces/BUILD.gn b/mojo/services/camera/public/interfaces/BUILD.gn similarity index 94% rename from mojo/services/camera_roll/public/interfaces/BUILD.gn rename to mojo/services/camera/public/interfaces/BUILD.gn index 53657bf..c001bf5 100644 --- a/mojo/services/camera_roll/public/interfaces/BUILD.gn +++ b/mojo/services/camera/public/interfaces/BUILD.gn
@@ -7,7 +7,7 @@ mojom("interfaces") { sources = [ - "camera_roll.mojom", + "camera.mojom", ] import_dirs = [ get_path_info("../../../", "abspath") ]
diff --git a/mojo/services/camera_roll/public/interfaces/camera_roll.mojom b/mojo/services/camera/public/interfaces/camera.mojom similarity index 82% rename from mojo/services/camera_roll/public/interfaces/camera_roll.mojom rename to mojo/services/camera/public/interfaces/camera.mojom index 850da78..b7cc5cb 100644 --- a/mojo/services/camera_roll/public/interfaces/camera_roll.mojom +++ b/mojo/services/camera/public/interfaces/camera.mojom
@@ -28,3 +28,10 @@ // if such an index is out-of-bounds. GetPhoto(uint32 index) => (Photo? photo); }; + +// |CameraService| provides access to the device's camera video stream. +interface CameraService { + // Returns the most recent frame captured by the device's camera + // in preview mode. + GetLatestFrame() => (handle<data_pipe_consumer>? content); +};
diff --git a/mojo/services/mojo_services.gni b/mojo/services/mojo_services.gni index 2f6b0a1..3edd025 100644 --- a/mojo/services/mojo_services.gni +++ b/mojo/services/mojo_services.gni
@@ -12,7 +12,7 @@ "//mojo/services/asset_bundle/public/interfaces", "//mojo/services/authenticating_url_loader_interceptor/public/interfaces", "//mojo/services/authentication/public/interfaces", - "//mojo/services/camera_roll/public/interfaces", + "//mojo/services/camera/public/interfaces", "//mojo/services/clipboard/public/interfaces", "//mojo/services/contacts/public/interfaces", "//mojo/services/content_handler/public/interfaces",
diff --git a/services/BUILD.gn b/services/BUILD.gn index 788ed2a..f9a568e 100644 --- a/services/BUILD.gn +++ b/services/BUILD.gn
@@ -28,7 +28,7 @@ if (is_android) { deps += [ "//services/android:java_handler", - "//services/camera_roll", + "//services/camera", "//services/contacts", "//services/location", "//services/notifications",
diff --git a/services/camera/BUILD.gn b/services/camera/BUILD.gn new file mode 100644 index 0000000..6a07fcf --- /dev/null +++ b/services/camera/BUILD.gn
@@ -0,0 +1,36 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//mojo/public/tools/bindings/mojom.gni") +import("//mojo/android/rules.gni") + +mojo_android_java_application("camera") { + sources = [ + "src/org/chromium/services/camera/CameraApp.java", + "src/org/chromium/services/camera/CameraServiceImpl.java", + ] + + mojo_main = "org.chromium.services.camera.CameraApp" + + deps = [ + "//base:base_java", + "//mojo/public/interfaces/application:application_java", + "//mojo/public/java:application", + "//mojo/services/camera/public/interfaces:interfaces_java", + ] +} + +mojo_android_java_application("camera_roll") { + sources = [ + "src/org/chromium/services/camera/CameraRollApp.java", + ] + + mojo_main = "org.chromium.services.camera.CameraRollApp" + + deps = [ + "//mojo/public/interfaces/application:application_java", + "//mojo/public/java:application", + "//mojo/services/camera/public/interfaces:interfaces_java", + ] +}
diff --git a/services/camera/src/org/chromium/services/camera/CameraApp.java b/services/camera/src/org/chromium/services/camera/CameraApp.java new file mode 100644 index 0000000..76fabd5 --- /dev/null +++ b/services/camera/src/org/chromium/services/camera/CameraApp.java
@@ -0,0 +1,61 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.services.camera; + +import android.content.Context; + +import org.chromium.mojo.application.ApplicationConnection; +import org.chromium.mojo.application.ApplicationDelegate; +import org.chromium.mojo.application.ApplicationRunner; +import org.chromium.mojo.application.ServiceFactoryBinder; +import org.chromium.mojo.bindings.InterfaceRequest; +import org.chromium.mojo.system.Core; +import org.chromium.mojo.system.MessagePipeHandle; +import org.chromium.mojom.mojo.CameraService; +import org.chromium.mojom.mojo.Shell; + +class CameraApp implements ApplicationDelegate { + private CameraServiceImpl mCameraServiceImpl; + + public CameraApp(Context context, Core core) { + mCameraServiceImpl = new CameraServiceImpl(context, core); + } + + public void initialize(Shell shell, String[] args, String url) { + } + + @Override + public boolean configureIncomingConnection(final ApplicationConnection connection) { + connection.addService(new ServiceFactoryBinder<CameraService>() { + @Override + public void bind(InterfaceRequest<CameraService> request) { + if (mCameraServiceImpl.cameraInUse()) { + /* another application is using the camera */ + /* TODO: support multiplexing the camera stream to multiple applications */ + request.close(); + return; + } + mCameraServiceImpl.openCamera(); + CameraService.MANAGER.bind(mCameraServiceImpl, request); + } + + @Override + public String getInterfaceName() { + return CameraService.MANAGER.getName(); + } + }); + return true; + } + + @Override + public void quit() { + mCameraServiceImpl.cleanup(); + } + + public static void mojoMain(Context context, Core core, + MessagePipeHandle applicationRequestHandle) { + ApplicationRunner.run(new CameraApp(context, core), core, applicationRequestHandle); + } +}
diff --git a/services/camera_roll/src/org/chromium/services/camera_roll/CameraRollApp.java b/services/camera/src/org/chromium/services/camera/CameraRollApp.java similarity index 100% rename from services/camera_roll/src/org/chromium/services/camera_roll/CameraRollApp.java rename to services/camera/src/org/chromium/services/camera/CameraRollApp.java
diff --git a/services/camera/src/org/chromium/services/camera/CameraServiceImpl.java b/services/camera/src/org/chromium/services/camera/CameraServiceImpl.java new file mode 100644 index 0000000..568a97c --- /dev/null +++ b/services/camera/src/org/chromium/services/camera/CameraServiceImpl.java
@@ -0,0 +1,246 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.services.camera; + +import android.content.Context; +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.util.Size; + +import org.chromium.mojo.system.Core; +import org.chromium.mojo.system.DataPipe; +import org.chromium.mojo.system.MojoException; +import org.chromium.mojo.system.Pair; +import org.chromium.mojom.mojo.CameraService; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.Semaphore; + +/** + * Implementation of AuthenticationService from services/camera/camera.mojom + */ +public class CameraServiceImpl implements CameraService { + private final Core mCore; + private final Context mContext; + private static final String TAG = "CameraServiceImpl"; + + private Semaphore mCameraOpenCloseLock = new Semaphore(1); + private CameraDevice mCameraDevice; + private HandlerThread mBackgroundThread; + private Handler mHandler; + + private ImageReader mImageReader; + private DataPipe.ProducerHandle mProducerHandle; + + public CameraServiceImpl(Context context, Core core) { + mContext = context; + mCore = core; + startBackgroundThread(); + } + + private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = + new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + DataPipe.ProducerHandle handle; + synchronized (this) { + if (mProducerHandle == null) { + return; + } + handle = mProducerHandle; + mProducerHandle = null; + } + try (Image img = reader.acquireLatestImage()) { + // TODO: Dont write the image data as a single block. + ByteBuffer buffer = img.getPlanes()[0].getBuffer(); + handle.writeData(buffer, DataPipe.WriteFlags.none()); + } catch (MojoException e) { + Log.e(TAG, "Failed to write to producer", e); + } finally { + handle.close(); + } + } + }; + + public void openCamera() { + CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + try { + for (String cameraId : manager.getCameraIdList()) { + CameraCharacteristics characteristics = + manager.getCameraCharacteristics(cameraId); + StreamConfigurationMap map = characteristics + .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + Size size = chooseVideoSize(map.getOutputSizes(ImageFormat.JPEG)); + // We don't use a front facing camera in this sample. + Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) { + continue; + } + mImageReader = ImageReader + .newInstance(size.getWidth(), size.getHeight(), + ImageFormat.JPEG, 100/*fps*/); + mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler); + mCameraOpenCloseLock.acquire(); + try { + manager.openCamera(cameraId, mCameraStateCallback, mHandler); + } catch (Exception e) { + mCameraOpenCloseLock.release(); + Log.e(TAG, "Failed to openCamera", e); + } + break; + } + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to access camera characteristics", e); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera opening."); + } + } + + private void closeCamera() { + try { + mCameraOpenCloseLock.acquire(); + try { + if (mImageReader != null) { + mImageReader.close(); + mImageReader = null; + } + if (mCameraDevice != null) { + mCameraDevice.close(); + mCameraDevice = null; + } + } finally { + mCameraOpenCloseLock.release(); + } + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera opening."); + } + } + + private void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraServiceImplThread"); + mBackgroundThread.start(); + mHandler = new Handler(mBackgroundThread.getLooper()); + } + + private void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mHandler = null; + } catch (InterruptedException e) { + Log.e(TAG, "Failed to stop background thread", e); + } + } + + private Size chooseVideoSize(Size[] choices) { + for (Size size : choices) { + // 1080p resolution + if (size.getWidth() == size.getHeight() * 16 / 9 && size.getHeight() <= 1080) { + return size; + } + } + return choices[choices.length - 1]; + } + + private final CameraDevice.StateCallback mCameraStateCallback = + new CameraDevice.StateCallback() { + @Override + public void onOpened(CameraDevice device) { + mCameraDevice = device; + startPreview(); + mCameraOpenCloseLock.release(); + } + + @Override + public void onDisconnected(CameraDevice cameraDevice) { + mCameraOpenCloseLock.release(); + } + + @Override + public void onError(CameraDevice cameraDevice, int error) { + mCameraOpenCloseLock.release(); + Log.e(TAG, "Failed to connect to camera:" + error); + } + }; + + private void startPreview() { + try { + final CaptureRequest.Builder request = + mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + request.addTarget(mImageReader.getSurface()); + mCameraDevice.createCaptureSession( + Arrays.asList(mImageReader.getSurface()), + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + request.set(CaptureRequest.CONTROL_MODE, + CameraMetadata.CONTROL_MODE_AUTO); + try { + session.setRepeatingRequest(request.build(), null, mHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to set repeating request", e); + } + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + Log.e(TAG, "Could not configure session capture"); + } + }, mHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Failed to start preview for camera", e); + } + } + + @Override + public void getLatestFrame(CameraService.GetLatestFrameResponse callback) { + Pair<DataPipe.ProducerHandle, DataPipe.ConsumerHandle> handles = mCore.createDataPipe(null); + callback.call(handles.second); + synchronized (this) { + if (mProducerHandle != null) { + mProducerHandle.close(); + } + mProducerHandle = handles.first; + } + } + + @Override + public void onConnectionError(MojoException e) { + } + + @Override + public void close() { + closeCamera(); + synchronized (this) { + if (mProducerHandle != null) { + mProducerHandle.close(); + mProducerHandle = null; + } + } + } + + public boolean cameraInUse() { + return mImageReader != null; + } + + public void cleanup() { + stopBackgroundThread(); + close(); + } +}
diff --git a/services/camera_roll/BUILD.gn b/services/camera_roll/BUILD.gn deleted file mode 100644 index c6839c4..0000000 --- a/services/camera_roll/BUILD.gn +++ /dev/null
@@ -1,19 +0,0 @@ -# Copyright 2015 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -import("//mojo/android/rules.gni") - -mojo_android_java_application("camera_roll") { - sources = [ - "src/org/chromium/services/camera_roll/CameraRollApp.java", - ] - - mojo_main = "org.chromium.services.camera_roll.CameraRollApp" - - deps = [ - "//mojo/public/interfaces/application:application_java", - "//mojo/public/java:application", - "//mojo/services/camera_roll/public/interfaces:interfaces_java", - ] -}
diff --git a/shell/android/apk/AndroidManifest.xml.jinja2 b/shell/android/apk/AndroidManifest.xml.jinja2 index 45640bc..0229050 100644 --- a/shell/android/apk/AndroidManifest.xml.jinja2 +++ b/shell/android/apk/AndroidManifest.xml.jinja2
@@ -19,6 +19,7 @@ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.NFC"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> + <uses-permission android:name="android.permission.CAMERA" /> <application android:icon="@mipmap/ic_launcher" android:name="org.chromium.mojo.shell.MojoShellApplication"