[camera] Add implementations for `camera_platform_interface` package. (#3302)

* First suggestion for camera platform interface

* Remove test coverage folder

* Renamed onLatestImageAvailableHandler definition

* Split CameraEvents into separate streams

* Implemented & tested first parts of method channel implementation

* Remove unused EventChannelMock class

* Add missing unit tests

* Added placeholders in default method channel implementation

* Updated platform interface

* Update packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart

Co-authored-by: Maurits van Beusekom <maurits@vnbskm.nl>

* Add unit test for availableCameras

* Expand availableCameras unit test. Added unit test for takePicture.

* Add unit test for startVideoRecording

* Add unit test for prepareForVideoRecording

* Add unit test for stopVideoRecording

* Add unit test for pauseVideoRecording

* Add unit test for buildView

* WIP: Dart and Android implementation

* Fix formatting

* Have resolution stream replay last value on subscription. Replace stream_transform with rxdart.

* Added reverse method channel to replace event channel. Updated initialise and takePicture implementations for android. WIP implementation for startVideoRecording

* Fixed example app for Android. Removed isRecordingVideo and isStreamingImages from buildView method.

* Added some first tests for camera/camera

* More tests and some feedback

* iOS implementation: Removed standard event channel. Added reverse method channel. Updated initialize method. Added resolution changed event. Updated error reporting to use new method channel.

* Started splitting initialize method

* Finish splitting up initialize for iOS

* Update unit tests

* Fix takePicture method on iOS

* Split initialize method on Android

* Fix video recording on iOS. Updated platform interface.

* Update unit tests

* Update error handling of video methods in iOS code. Make iOS code more consistent.

* Updated startVideoRecording documentation

* Make sure file is returned by stopVideoRecording

* Use correct event-type after initializing

* Fix DartMessenger unit-tests

* Change cast

* Fix formatting

* Fixed tests, formatting and analysis warnings

* Added missing license to Dart files

* Updated CHANGELOG and version

* Added additional unit-tests to platform_interface

* Added more tests

* Formatted code

* Re-added the CameraPreview widget

* Use import/export instead of part implementation

* fixed formatting

* Resolved additional feedback

* Update dependency to git repo

* Depend on pub.dev for camera_platform_interface

* Fix JAVA formatting

* Fix changelog

* Make sure camera package can be published

* Assert when stream methods are called from wrong platform

* Add dev_dependency on plugin_platform_interface package, required by tests.

* Remove pedantic requirement from initialize() method. Remove unnecessary completers.

* Remove dependency on dart:io

* Restrict exposed types from platform interface

* Moved test for image stream in separate file

* Fixed formatting issue

* Fix deprecation warning

* Apply feedback from bparrishMines

* Fix formatting issues

* Removed redundant podspec files

* Removed redundant ios files

* Handle SecurityException

Co-authored-by: Maurits van Beusekom <maurits@baseflow.com>
Co-authored-by: Maurits van Beusekom <maurits@vnbskm.nl>
Co-authored-by: daniel <daniel.roek@gmail.com>
Co-authored-by: David Iglesias Teixeira <ditman@gmail.com>
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index d1d11c4..cb8dfed 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,13 @@
+## 0.6.0
+
+As part of implementing federated architecture and making the interface compatible with the web this version contains the following **breaking changes**:
+
+Method changes in `CameraController`:
+- The `takePicture` method no longer accepts the `path` parameter, but instead returns the captured image as an instance of the `XFile` class;
+- The `startVideoRecording` method no longer accepts the `filePath`. Instead the recorded video is now returned as a `XFile` instance when the `stopVideoRecording` method completes; 
+- The `stopVideoRecording` method now returns the captured video when it completes;
+- Added the `buildPreview` method which is now used to implement the CameraPreview widget.
+
 ## 0.5.8+19
 
 * Update Flutter SDK constraint.
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
index 9cf111b..306dd44 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java
@@ -65,8 +65,10 @@
   private CaptureRequest.Builder captureRequestBuilder;
   private MediaRecorder mediaRecorder;
   private boolean recordingVideo;
+  private File videoRecordingFile;
   private CamcorderProfile recordingProfile;
   private int currentOrientation = ORIENTATION_UNKNOWN;
+  private Context applicationContext;
 
   // Mirrors camera.dart
   public enum ResolutionPreset {
@@ -94,6 +96,7 @@
     this.flutterTexture = flutterTexture;
     this.dartMessenger = dartMessenger;
     this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
+    this.applicationContext = activity.getApplicationContext();
     orientationEventListener =
         new OrientationEventListener(activity.getApplicationContext()) {
           @Override
@@ -135,7 +138,7 @@
   }
 
   @SuppressLint("MissingPermission")
-  public void open(@NonNull final Result result) throws CameraAccessException {
+  public void open() throws CameraAccessException {
     pictureImageReader =
         ImageReader.newInstance(
             captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2);
@@ -154,15 +157,13 @@
             try {
               startPreview();
             } catch (CameraAccessException e) {
-              result.error("CameraAccess", e.getMessage(), null);
+              dartMessenger.sendCameraErrorEvent(e.getMessage());
               close();
               return;
             }
-            Map<String, Object> reply = new HashMap<>();
-            reply.put("textureId", flutterTexture.id());
-            reply.put("previewWidth", previewSize.getWidth());
-            reply.put("previewHeight", previewSize.getHeight());
-            result.success(reply);
+
+            dartMessenger.sendCameraInitializedEvent(
+                previewSize.getWidth(), previewSize.getHeight());
           }
 
           @Override
@@ -174,7 +175,7 @@
           @Override
           public void onDisconnected(@NonNull CameraDevice cameraDevice) {
             close();
-            dartMessenger.send(DartMessenger.EventType.ERROR, "The camera was disconnected.");
+            dartMessenger.sendCameraErrorEvent("The camera was disconnected.");
           }
 
           @Override
@@ -200,7 +201,7 @@
               default:
                 errorDescription = "Unknown camera error";
             }
-            dartMessenger.send(DartMessenger.EventType.ERROR, errorDescription);
+            dartMessenger.sendCameraErrorEvent(errorDescription);
           }
         },
         null);
@@ -218,12 +219,13 @@
     return flutterTexture;
   }
 
-  public void takePicture(String filePath, @NonNull final Result result) {
-    final File file = new File(filePath);
-
-    if (file.exists()) {
-      result.error(
-          "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null);
+  public void takePicture(@NonNull final Result result) {
+    final File outputDir = applicationContext.getCacheDir();
+    final File file;
+    try {
+      file = File.createTempFile("CAP", ".jpg", outputDir);
+    } catch (IOException | SecurityException e) {
+      result.error("cannotCreateFile", e.getMessage(), null);
       return;
     }
 
@@ -232,7 +234,7 @@
           try (Image image = reader.acquireLatestImage()) {
             ByteBuffer buffer = image.getPlanes()[0].getBuffer();
             writeToFile(buffer, file);
-            result.success(null);
+            result.success(file.getAbsolutePath());
           } catch (IOException e) {
             result.error("IOError", "Failed saving image", null);
           }
@@ -308,8 +310,7 @@
           public void onConfigured(@NonNull CameraCaptureSession session) {
             try {
               if (cameraDevice == null) {
-                dartMessenger.send(
-                    DartMessenger.EventType.ERROR, "The camera was closed during configuration.");
+                dartMessenger.sendCameraErrorEvent("The camera was closed during configuration.");
                 return;
               }
               cameraCaptureSession = session;
@@ -320,14 +321,13 @@
                 onSuccessCallback.run();
               }
             } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) {
-              dartMessenger.send(DartMessenger.EventType.ERROR, e.getMessage());
+              dartMessenger.sendCameraErrorEvent(e.getMessage());
             }
           }
 
           @Override
           public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
-            dartMessenger.send(
-                DartMessenger.EventType.ERROR, "Failed to configure camera session.");
+            dartMessenger.sendCameraErrorEvent("Failed to configure camera session.");
           }
         };
 
@@ -369,18 +369,24 @@
     cameraDevice.createCaptureSession(surfaces, callback, null);
   }
 
-  public void startVideoRecording(String filePath, Result result) {
-    if (new File(filePath).exists()) {
-      result.error("fileExists", "File at path '" + filePath + "' already exists.", null);
+  public void startVideoRecording(Result result) {
+    final File outputDir = applicationContext.getCacheDir();
+    try {
+      videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir);
+    } catch (IOException | SecurityException e) {
+      result.error("cannotCreateFile", e.getMessage(), null);
       return;
     }
+
     try {
-      prepareMediaRecorder(filePath);
+      prepareMediaRecorder(videoRecordingFile.getAbsolutePath());
       recordingVideo = true;
       createCaptureSession(
           CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface());
       result.success(null);
     } catch (CameraAccessException | IOException e) {
+      recordingVideo = false;
+      videoRecordingFile = null;
       result.error("videoRecordingFailed", e.getMessage(), null);
     }
   }
@@ -396,7 +402,8 @@
       mediaRecorder.stop();
       mediaRecorder.reset();
       startPreview();
-      result.success(null);
+      result.success(videoRecordingFile.getAbsolutePath());
+      videoRecordingFile = null;
     } catch (CameraAccessException | IllegalStateException e) {
       result.error("videoRecordingFailed", e.getMessage(), null);
     }
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java
index fe385be..49f9d9a 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java
@@ -3,49 +3,56 @@
 import android.text.TextUtils;
 import androidx.annotation.Nullable;
 import io.flutter.plugin.common.BinaryMessenger;
-import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodChannel;
 import java.util.HashMap;
 import java.util.Map;
 
 class DartMessenger {
-  @Nullable private EventChannel.EventSink eventSink;
+  @Nullable private MethodChannel channel;
 
   enum EventType {
     ERROR,
     CAMERA_CLOSING,
+    INITIALIZED,
   }
 
-  DartMessenger(BinaryMessenger messenger, long eventChannelId) {
-    new EventChannel(messenger, "flutter.io/cameraPlugin/cameraEvents" + eventChannelId)
-        .setStreamHandler(
-            new EventChannel.StreamHandler() {
-              @Override
-              public void onListen(Object arguments, EventChannel.EventSink sink) {
-                eventSink = sink;
-              }
+  DartMessenger(BinaryMessenger messenger, long cameraId) {
+    channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId);
+  }
 
-              @Override
-              public void onCancel(Object arguments) {
-                eventSink = null;
-              }
-            });
+  void sendCameraInitializedEvent(Integer previewWidth, Integer previewHeight) {
+    this.send(
+        EventType.INITIALIZED,
+        new HashMap<String, Object>() {
+          {
+            if (previewWidth != null) put("previewWidth", previewWidth.doubleValue());
+            if (previewHeight != null) put("previewHeight", previewHeight.doubleValue());
+          }
+        });
   }
 
   void sendCameraClosingEvent() {
-    send(EventType.CAMERA_CLOSING, null);
+    send(EventType.CAMERA_CLOSING);
   }
 
-  void send(EventType eventType, @Nullable String description) {
-    if (eventSink == null) {
+  void sendCameraErrorEvent(@Nullable String description) {
+    this.send(
+        EventType.ERROR,
+        new HashMap<String, Object>() {
+          {
+            if (!TextUtils.isEmpty(description)) put("description", description);
+          }
+        });
+  }
+
+  void send(EventType eventType) {
+    send(eventType, new HashMap<>());
+  }
+
+  void send(EventType eventType, Map<String, Object> args) {
+    if (channel == null) {
       return;
     }
-
-    Map<String, String> event = new HashMap<>();
-    event.put("eventType", eventType.toString().toLowerCase());
-    // Only errors have a description.
-    if (eventType == EventType.ERROR && !TextUtils.isEmpty(description)) {
-      event.put("errorDescription", description);
-    }
-    eventSink.success(event);
+    channel.invokeMethod(eventType.toString().toLowerCase(), args);
   }
 }
diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
index 1320755..6c2e65e 100644
--- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
+++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java
@@ -11,6 +11,8 @@
 import io.flutter.plugin.common.MethodChannel.Result;
 import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
 import io.flutter.view.TextureRegistry;
+import java.util.HashMap;
+import java.util.Map;
 
 final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
   private final Activity activity;
@@ -49,11 +51,12 @@
           handleException(e, result);
         }
         break;
-      case "initialize":
+      case "create":
         {
           if (camera != null) {
             camera.close();
           }
+
           cameraPermissions.requestPermissions(
               activity,
               permissionsRegistry,
@@ -69,12 +72,28 @@
                   result.error(errCode, errDesc, null);
                 }
               });
-
+          break;
+        }
+      case "initialize":
+        {
+          if (camera != null) {
+            try {
+              camera.open();
+              result.success(null);
+            } catch (Exception e) {
+              handleException(e, result);
+            }
+          } else {
+            result.error(
+                "cameraNotFound",
+                "Camera not found. Please call the 'create' method before calling 'initialize'.",
+                null);
+          }
           break;
         }
       case "takePicture":
         {
-          camera.takePicture(call.argument("path"), result);
+          camera.takePicture(result);
           break;
         }
       case "prepareForVideoRecording":
@@ -85,7 +104,7 @@
         }
       case "startVideoRecording":
         {
-          camera.startVideoRecording(call.argument("filePath"), result);
+          camera.startVideoRecording(result);
           break;
         }
       case "stopVideoRecording":
@@ -157,7 +176,9 @@
             resolutionPreset,
             enableAudio);
 
-    camera.open(result);
+    Map<String, Object> reply = new HashMap<>();
+    reply.put("cameraId", flutterSurfaceTexture.id());
+    result.success(reply);
   }
 
   // We move catching CameraAccessException out of onMethodCall because it causes a crash
diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java
index 5a53582..a689f2b 100644
--- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java
+++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java
@@ -2,42 +2,34 @@
 
 import static junit.framework.TestCase.assertNull;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 
+import androidx.annotation.NonNull;
 import io.flutter.plugin.common.BinaryMessenger;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.StandardMethodCodec;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 
 public class DartMessengerTest {
   /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */
   private static class FakeBinaryMessenger implements BinaryMessenger {
-    private BinaryMessageHandler handler;
     private final List<ByteBuffer> sentMessages = new ArrayList<>();
 
     @Override
-    public void send(String channel, ByteBuffer message) {
+    public void send(@NonNull String channel, ByteBuffer message) {
       sentMessages.add(message);
     }
 
     @Override
-    public void send(String channel, ByteBuffer message, BinaryReply callback) {
+    public void send(@NonNull String channel, ByteBuffer message, BinaryReply callback) {
       send(channel, message);
     }
 
     @Override
-    public void setMessageHandler(String channel, BinaryMessageHandler handler) {
-      this.handler = handler;
-    }
-
-    BinaryMessageHandler getMessageHandler() {
-      return handler;
-    }
+    public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {}
 
     List<ByteBuffer> getMessages() {
       return new ArrayList<>(sentMessages);
@@ -54,55 +46,42 @@
   }
 
   @Test
-  public void setsStreamHandler() {
-    assertNotNull(fakeBinaryMessenger.getMessageHandler());
-  }
-
-  @Test
-  public void send_handlesNullEventSinks() {
-    dartMessenger.send(DartMessenger.EventType.ERROR, "error description");
-
-    List<ByteBuffer> sentMessages = fakeBinaryMessenger.getMessages();
-    assertEquals(0, sentMessages.size());
-  }
-
-  @Test
-  public void send_includesErrorDescriptions() {
-    initializeEventSink();
-
-    dartMessenger.send(DartMessenger.EventType.ERROR, "error description");
+  public void sendCameraErrorEvent_includesErrorDescriptions() {
+    dartMessenger.sendCameraErrorEvent("error description");
 
     List<ByteBuffer> sentMessages = fakeBinaryMessenger.getMessages();
     assertEquals(1, sentMessages.size());
-    Map<String, String> event = decodeSentMessage(sentMessages.get(0));
-    assertEquals(DartMessenger.EventType.ERROR.toString().toLowerCase(), event.get("eventType"));
-    assertEquals("error description", event.get("errorDescription"));
+    MethodCall call = decodeSentMessage(sentMessages.get(0));
+    assertEquals("error", call.method);
+    assertEquals("error description", call.argument("description"));
+  }
+
+  @Test
+  public void sendCameraInitializedEvent_includesPreviewSize() {
+    dartMessenger.sendCameraInitializedEvent(0, 0);
+
+    List<ByteBuffer> sentMessages = fakeBinaryMessenger.getMessages();
+    assertEquals(1, sentMessages.size());
+    MethodCall call = decodeSentMessage(sentMessages.get(0));
+    assertEquals("initialized", call.method);
+    assertEquals(0, (double) call.argument("previewWidth"), 0);
+    assertEquals(0, (double) call.argument("previewHeight"), 0);
   }
 
   @Test
   public void sendCameraClosingEvent() {
-    initializeEventSink();
-
     dartMessenger.sendCameraClosingEvent();
 
     List<ByteBuffer> sentMessages = fakeBinaryMessenger.getMessages();
     assertEquals(1, sentMessages.size());
-    Map<String, String> event = decodeSentMessage(sentMessages.get(0));
-    assertEquals(
-        DartMessenger.EventType.CAMERA_CLOSING.toString().toLowerCase(), event.get("eventType"));
-    assertNull(event.get("errorDescription"));
+    MethodCall call = decodeSentMessage(sentMessages.get(0));
+    assertEquals("camera_closing", call.method);
+    assertNull(call.argument("description"));
   }
 
-  @SuppressWarnings("unchecked")
-  private Map<String, String> decodeSentMessage(ByteBuffer sentMessage) {
+  private MethodCall decodeSentMessage(ByteBuffer sentMessage) {
     sentMessage.position(0);
-    return (Map<String, String>) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage);
-  }
 
-  private void initializeEventSink() {
-    MethodCall call = new MethodCall("listen", null);
-    ByteBuffer encodedCall = StandardMethodCodec.INSTANCE.encodeMethodCall(call);
-    encodedCall.position(0);
-    fakeBinaryMessenger.getMessageHandler().onMessage(encodedCall, reply -> {});
+    return StandardMethodCodec.INSTANCE.decodeMethodCall(sentMessage);
   }
 }
diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart
index ef4646f..c2e73e0 100644
--- a/packages/camera/camera/example/integration_test/camera_test.dart
+++ b/packages/camera/camera/example/integration_test/camera_test.dart
@@ -2,9 +2,9 @@
 import 'dart:io';
 import 'dart:ui';
 
+import 'package:camera/camera.dart';
 import 'package:flutter/painting.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:camera/camera.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:video_player/video_player.dart';
 import 'package:integration_test/integration_test.dart';
@@ -55,12 +55,10 @@
         'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
 
     // Take Picture
-    final String filePath =
-        '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg';
-    await controller.takePicture(filePath);
+    final file = await controller.takePicture();
 
     // Load picture
-    final File fileImage = File(filePath);
+    final File fileImage = File(file.path);
     final Image image = await decodeImageFromList(fileImage.readAsBytesSync());
 
     // Verify image dimensions are as expected
@@ -102,14 +100,12 @@
         'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}');
 
     // Take Video
-    final String filePath =
-        '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4';
-    await controller.startVideoRecording(filePath);
+    await controller.startVideoRecording();
     sleep(const Duration(milliseconds: 300));
-    await controller.stopVideoRecording();
+    final file = await controller.stopVideoRecording();
 
     // Load video metadata
-    final File videoFile = File(filePath);
+    final File videoFile = File(file.path);
     final VideoPlayerController videoController =
         VideoPlayerController.file(videoFile);
     await videoController.initialize();
@@ -160,13 +156,10 @@
     await controller.initialize();
     await controller.prepareForVideoRecording();
 
-    final String filePath =
-        '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4';
-
     int startPause;
     int timePaused = 0;
 
-    await controller.startVideoRecording(filePath);
+    await controller.startVideoRecording();
     final int recordingStart = DateTime.now().millisecondsSinceEpoch;
     sleep(const Duration(milliseconds: 500));
 
@@ -186,11 +179,11 @@
 
     sleep(const Duration(milliseconds: 500));
 
-    await controller.stopVideoRecording();
+    final file = await controller.stopVideoRecording();
     final int recordingTime =
         DateTime.now().millisecondsSinceEpoch - recordingStart;
 
-    final File videoFile = File(filePath);
+    final File videoFile = File(file.path);
     final VideoPlayerController videoController = VideoPlayerController.file(
       videoFile,
     );
diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
index 862ee64..d51240a 100644
--- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj
@@ -9,11 +9,7 @@
 /* Begin PBXBuildFile section */
 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
-		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
-		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */; };
-		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
-		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
 		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -28,8 +24,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
-				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -40,7 +34,6 @@
 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
-		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
 		483D985F075B951ADBAD218E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
 		620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -48,7 +41,6 @@
 		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
-		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
 		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
 		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@@ -63,8 +55,6 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
-				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
 				75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -83,9 +73,7 @@
 		9740EEB11CF90186004384FC /* Flutter */ = {
 			isa = PBXGroup;
 			children = (
-				3B80C3931E831B6300D905FE /* App.framework */,
 				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
-				9740EEBA1CF902C7004384FC /* Flutter.framework */,
 				9740EEB21CF90195004384FC /* Debug.xcconfig */,
 				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
 				9740EEB31CF90195004384FC /* Generated.xcconfig */,
@@ -181,6 +169,7 @@
 				TargetAttributes = {
 					97C146ED1CF9000F007C117D = {
 						CreatedOnToolsVersion = 7.3.1;
+						DevelopmentTeam = 7624MWN53C;
 					};
 				};
 			};
@@ -229,7 +218,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
 		};
 		3E30118C54AB12C3EB9EDF27 /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
@@ -269,9 +258,12 @@
 			files = (
 			);
 			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+				"${PODS_ROOT}/../Flutter/Flutter.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -315,7 +307,6 @@
 /* Begin XCBuildConfiguration section */
 		97C147031CF9000F007C117D /* Debug */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -372,7 +363,6 @@
 		};
 		97C147041CF9000F007C117D /* Release */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -426,6 +416,7 @@
 			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = 7624MWN53C;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
@@ -447,6 +438,7 @@
 			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = 7624MWN53C;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
index 3ec6604..e1edc1b 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -9,7 +9,6 @@
 
 import 'package:camera/camera.dart';
 import 'package:flutter/material.dart';
-import 'package:path_provider/path_provider.dart';
 import 'package:video_player/video_player.dart';
 
 class CameraExampleHome extends StatefulWidget {
@@ -38,8 +37,8 @@
 class _CameraExampleHomeState extends State<CameraExampleHome>
     with WidgetsBindingObserver {
   CameraController controller;
-  String imagePath;
-  String videoPath;
+  XFile imageFile;
+  XFile videoFile;
   VideoPlayerController videoController;
   VoidCallback videoPlayerListener;
   bool enableAudio = true;
@@ -166,11 +165,11 @@
         child: Row(
           mainAxisSize: MainAxisSize.min,
           children: <Widget>[
-            videoController == null && imagePath == null
+            videoController == null && imageFile == null
                 ? Container()
                 : SizedBox(
                     child: (videoController == null)
-                        ? Image.file(File(imagePath))
+                        ? Image.file(File(imageFile.path))
                         : Container(
                             child: Center(
                               child: AspectRatio(
@@ -306,29 +305,32 @@
   }
 
   void onTakePictureButtonPressed() {
-    takePicture().then((String filePath) {
+    takePicture().then((XFile file) {
       if (mounted) {
         setState(() {
-          imagePath = filePath;
+          imageFile = file;
           videoController?.dispose();
           videoController = null;
         });
-        if (filePath != null) showInSnackBar('Picture saved to $filePath');
+        if (file != null) showInSnackBar('Picture saved to ${file.path}');
       }
     });
   }
 
   void onVideoRecordButtonPressed() {
-    startVideoRecording().then((String filePath) {
+    startVideoRecording().then((_) {
       if (mounted) setState(() {});
-      if (filePath != null) showInSnackBar('Saving video to $filePath');
     });
   }
 
   void onStopButtonPressed() {
-    stopVideoRecording().then((_) {
+    stopVideoRecording().then((file) {
       if (mounted) setState(() {});
-      showInSnackBar('Video recorded to: $videoPath');
+      if (file != null) {
+        showInSnackBar('Video recorded to ${file.path}');
+        videoFile = file;
+        _startVideoPlayer();
+      }
     });
   }
 
@@ -346,45 +348,36 @@
     });
   }
 
-  Future<String> startVideoRecording() async {
+  Future<void> startVideoRecording() async {
     if (!controller.value.isInitialized) {
       showInSnackBar('Error: select a camera first.');
-      return null;
+      return;
     }
 
-    final Directory extDir = await getApplicationDocumentsDirectory();
-    final String dirPath = '${extDir.path}/Movies/flutter_test';
-    await Directory(dirPath).create(recursive: true);
-    final String filePath = '$dirPath/${timestamp()}.mp4';
-
     if (controller.value.isRecordingVideo) {
       // A recording is already started, do nothing.
-      return null;
+      return;
     }
 
     try {
-      videoPath = filePath;
-      await controller.startVideoRecording(filePath);
+      await controller.startVideoRecording();
     } on CameraException catch (e) {
       _showCameraException(e);
-      return null;
+      return;
     }
-    return filePath;
   }
 
-  Future<void> stopVideoRecording() async {
+  Future<XFile> stopVideoRecording() async {
     if (!controller.value.isRecordingVideo) {
       return null;
     }
 
     try {
-      await controller.stopVideoRecording();
+      return controller.stopVideoRecording();
     } on CameraException catch (e) {
       _showCameraException(e);
       return null;
     }
-
-    await _startVideoPlayer();
   }
 
   Future<void> pauseVideoRecording() async {
@@ -414,8 +407,8 @@
   }
 
   Future<void> _startVideoPlayer() async {
-    final VideoPlayerController vcontroller =
-        VideoPlayerController.file(File(videoPath));
+    final VideoPlayerController vController =
+        VideoPlayerController.file(File(videoFile.path));
     videoPlayerListener = () {
       if (videoController != null && videoController.value.size != null) {
         // Refreshing the state to update video player with the correct ratio.
@@ -423,28 +416,24 @@
         videoController.removeListener(videoPlayerListener);
       }
     };
-    vcontroller.addListener(videoPlayerListener);
-    await vcontroller.setLooping(true);
-    await vcontroller.initialize();
+    vController.addListener(videoPlayerListener);
+    await vController.setLooping(true);
+    await vController.initialize();
     await videoController?.dispose();
     if (mounted) {
       setState(() {
-        imagePath = null;
-        videoController = vcontroller;
+        imageFile = null;
+        videoController = vController;
       });
     }
-    await vcontroller.play();
+    await vController.play();
   }
 
-  Future<String> takePicture() async {
+  Future<XFile> takePicture() async {
     if (!controller.value.isInitialized) {
       showInSnackBar('Error: select a camera first.');
       return null;
     }
-    final Directory extDir = await getApplicationDocumentsDirectory();
-    final String dirPath = '${extDir.path}/Pictures/flutter_test';
-    await Directory(dirPath).create(recursive: true);
-    final String filePath = '$dirPath/${timestamp()}.jpg';
 
     if (controller.value.isTakingPicture) {
       // A capture is already pending, do nothing.
@@ -452,12 +441,12 @@
     }
 
     try {
-      await controller.takePicture(filePath);
+      XFile file = await controller.takePicture();
+      return file;
     } on CameraException catch (e) {
       _showCameraException(e);
       return null;
     }
-    return filePath;
   }
 
   void _showCameraException(CameraException e) {
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index 525c128..9455375 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -7,6 +7,7 @@
 #import <Accelerate/Accelerate.h>
 #import <CoreMotion/CoreMotion.h>
 #import <libkern/OSAtomic.h>
+#import <uuid/uuid.h>
 
 static FlutterError *getFlutterError(NSError *error) {
   return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code]
@@ -51,10 +52,10 @@
   self = [super init];
   NSAssert(self, @"super init cannot be nil");
   _path = path;
-  _result = result;
   _motionManager = motionManager;
   _cameraPosition = cameraPosition;
   selfReference = self;
+  _result = result;
   return self;
 }
 
@@ -81,7 +82,7 @@
     _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]);
     return;
   }
-  _result(nil);
+  _result(_path);
 }
 
 - (UIImageOrientation)getImageRotation {
@@ -152,14 +153,12 @@
 
 @interface FLTCam : NSObject <FlutterTexture,
                               AVCaptureVideoDataOutputSampleBufferDelegate,
-                              AVCaptureAudioDataOutputSampleBufferDelegate,
-                              FlutterStreamHandler>
+                              AVCaptureAudioDataOutputSampleBufferDelegate>
 @property(readonly, nonatomic) int64_t textureId;
 @property(nonatomic, copy) void (^onFrameAvailable)(void);
 @property BOOL enableAudio;
-@property(nonatomic) FlutterEventChannel *eventChannel;
 @property(nonatomic) FLTImageStreamHandler *imageStreamHandler;
-@property(nonatomic) FlutterEventSink eventSink;
+@property(nonatomic) FlutterMethodChannel *methodChannel;
 @property(readonly, nonatomic) AVCaptureSession *captureSession;
 @property(readonly, nonatomic) AVCaptureDevice *captureDevice;
 @property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10));
@@ -174,6 +173,7 @@
 @property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor;
 @property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
 @property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
+@property(strong, nonatomic) NSString *videoRecordingPath;
 @property(assign, nonatomic) BOOL isRecording;
 @property(assign, nonatomic) BOOL isRecordingPaused;
 @property(assign, nonatomic) BOOL videoIsDisconnected;
@@ -194,6 +194,7 @@
 }
 // Format used for video and image streaming.
 FourCharCode const videoFormat = kCVPixelFormatType_32BGRA;
+NSString *const errorMethod = @"error";
 
 - (instancetype)initWithCameraName:(NSString *)cameraName
                   resolutionPreset:(NSString *)resolutionPreset
@@ -257,11 +258,20 @@
   [_captureSession stopRunning];
 }
 
-- (void)captureToFile:(NSString *)path result:(FlutterResult)result API_AVAILABLE(ios(10)) {
+- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) {
   AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
   if (_resolutionPreset == max) {
     [settings setHighResolutionPhotoEnabled:YES];
   }
+  NSError *error;
+  NSString *path = [self getTemporaryFilePathWithExtension:@"jpg"
+                                                 subfolder:@"pictures"
+                                                    prefix:@"CAP_"
+                                                     error:error];
+  if (error) {
+    result(getFlutterError(error));
+    return;
+  }
   [_capturePhotoOutput
       capturePhotoWithSettings:settings
                       delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
@@ -270,6 +280,32 @@
                                                            cameraPosition:_captureDevice.position]];
 }
 
+- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension
+                                      subfolder:(NSString *)subfolder
+                                         prefix:(NSString *)prefix
+                                          error:(NSError *)error {
+  NSString *docDir =
+      NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
+  NSString *fileDir =
+      [[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder];
+  NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
+  NSString *file =
+      [[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension];
+
+  NSFileManager *fm = [NSFileManager defaultManager];
+  if (![fm fileExistsAtPath:fileDir]) {
+    [[NSFileManager defaultManager] createDirectoryAtPath:fileDir
+                              withIntermediateDirectories:true
+                                               attributes:nil
+                                                    error:&error];
+    if (error) {
+      return nil;
+    }
+  }
+
+  return file;
+}
+
 - (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset {
   switch (resolutionPreset) {
     case max:
@@ -347,10 +383,8 @@
     }
   }
   if (!CMSampleBufferDataIsReady(sampleBuffer)) {
-    _eventSink(@{
-      @"event" : @"error",
-      @"errorDescription" : @"sample buffer is not ready. Skipping sample"
-    });
+    [_methodChannel invokeMethod:errorMethod
+                       arguments:@"sample buffer is not ready. Skipping sample"];
     return;
   }
   if (_isStreamingImages) {
@@ -414,10 +448,8 @@
   }
   if (_isRecording && !_isRecordingPaused) {
     if (_videoWriter.status == AVAssetWriterStatusFailed) {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" : [NSString stringWithFormat:@"%@", _videoWriter.error]
-      });
+      [_methodChannel invokeMethod:errorMethod
+                         arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
       return;
     }
 
@@ -500,20 +532,16 @@
 - (void)newVideoSample:(CMSampleBufferRef)sampleBuffer {
   if (_videoWriter.status != AVAssetWriterStatusWriting) {
     if (_videoWriter.status == AVAssetWriterStatusFailed) {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" : [NSString stringWithFormat:@"%@", _videoWriter.error]
-      });
+      [_methodChannel invokeMethod:errorMethod
+                         arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
     }
     return;
   }
   if (_videoWriterInput.readyForMoreMediaData) {
     if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" :
-            [NSString stringWithFormat:@"%@", @"Unable to write to video input"]
-      });
+      [_methodChannel
+          invokeMethod:errorMethod
+             arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]];
     }
   }
 }
@@ -521,20 +549,16 @@
 - (void)newAudioSample:(CMSampleBufferRef)sampleBuffer {
   if (_videoWriter.status != AVAssetWriterStatusWriting) {
     if (_videoWriter.status == AVAssetWriterStatusFailed) {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" : [NSString stringWithFormat:@"%@", _videoWriter.error]
-      });
+      [_methodChannel invokeMethod:errorMethod
+                         arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
     }
     return;
   }
   if (_audioWriterInput.readyForMoreMediaData) {
     if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" :
-            [NSString stringWithFormat:@"%@", @"Unable to write to audio input"]
-      });
+      [_methodChannel
+          invokeMethod:errorMethod
+             arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]];
     }
   }
 }
@@ -565,23 +589,19 @@
   return pixelBuffer;
 }
 
-- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments {
-  _eventSink = nil;
-  // need to unregister stream handler when disposing the camera
-  [_eventChannel setStreamHandler:nil];
-  return nil;
-}
-
-- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
-                                       eventSink:(nonnull FlutterEventSink)events {
-  _eventSink = events;
-  return nil;
-}
-
-- (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result {
+- (void)startVideoRecordingWithResult:(FlutterResult)result {
   if (!_isRecording) {
-    if (![self setupWriterForPath:path]) {
-      _eventSink(@{@"event" : @"error", @"errorDescription" : @"Setup Writer Failed"});
+    NSError *error;
+    _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4"
+                                                        subfolder:@"videos"
+                                                           prefix:@"CAP_"
+                                                            error:error];
+    if (error) {
+      result(getFlutterError(error));
+      return;
+    }
+    if (![self setupWriterForPath:_videoRecordingPath]) {
+      result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]);
       return;
     }
     _isRecording = YES;
@@ -592,7 +612,7 @@
     _audioIsDisconnected = NO;
     result(nil);
   } else {
-    _eventSink(@{@"event" : @"error", @"errorDescription" : @"Video is already recording!"});
+    result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]);
   }
 }
 
@@ -602,12 +622,12 @@
     if (_videoWriter.status != AVAssetWriterStatusUnknown) {
       [_videoWriter finishWritingWithCompletionHandler:^{
         if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
-          result(nil);
+          result(self->_videoRecordingPath);
+          self->_videoRecordingPath = nil;
         } else {
-          self->_eventSink(@{
-            @"event" : @"error",
-            @"errorDescription" : @"AVAssetWriter could not finish writing!"
-          });
+          result([FlutterError errorWithCode:@"IOError"
+                                     message:@"AVAssetWriter could not finish writing!"
+                                     details:nil]);
         }
       }];
     }
@@ -620,14 +640,16 @@
   }
 }
 
-- (void)pauseVideoRecording {
+- (void)pauseVideoRecordingWithResult:(FlutterResult)result {
   _isRecordingPaused = YES;
   _videoIsDisconnected = YES;
   _audioIsDisconnected = YES;
+  result(nil);
 }
 
-- (void)resumeVideoRecording {
+- (void)resumeVideoRecordingWithResult:(FlutterResult)result {
   _isRecordingPaused = NO;
+  result(nil);
 }
 
 - (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger {
@@ -641,8 +663,8 @@
 
     _isStreamingImages = YES;
   } else {
-    _eventSink(
-        @{@"event" : @"error", @"errorDescription" : @"Images from camera are already streaming!"});
+    [_methodChannel invokeMethod:errorMethod
+                       arguments:@"Images from camera are already streaming!"];
   }
 }
 
@@ -651,8 +673,7 @@
     _isStreamingImages = NO;
     _imageStreamHandler = nil;
   } else {
-    _eventSink(
-        @{@"event" : @"error", @"errorDescription" : @"Images from camera are not streaming!"});
+    [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"];
   }
 }
 
@@ -672,7 +693,7 @@
                                               error:&error];
   NSParameterAssert(_videoWriter);
   if (error) {
-    _eventSink(@{@"event" : @"error", @"errorDescription" : error.description});
+    [_methodChannel invokeMethod:errorMethod arguments:error.description];
     return NO;
   }
   NSDictionary *videoSettings = [NSDictionary
@@ -726,7 +747,7 @@
   AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice
                                                                            error:&error];
   if (error) {
-    _eventSink(@{@"event" : @"error", @"errorDescription" : error.description});
+    [_methodChannel invokeMethod:errorMethod arguments:error.description];
   }
   // Setup the audio output.
   _audioOutput = [[AVCaptureAudioDataOutput alloc] init];
@@ -738,10 +759,8 @@
       [_captureSession addOutput:_audioOutput];
       _isAudioSetup = YES;
     } else {
-      _eventSink(@{
-        @"event" : @"error",
-        @"errorDescription" : @"Unable to add Audio input/output to session capture"
-      });
+      [_methodChannel invokeMethod:errorMethod
+                         arguments:@"Unable to add Audio input/output to session capture"];
       _isAudioSetup = NO;
     }
   }
@@ -819,7 +838,7 @@
     } else {
       result(FlutterMethodNotImplemented);
     }
-  } else if ([@"initialize" isEqualToString:call.method]) {
+  } else if ([@"create" isEqualToString:call.method]) {
     NSString *cameraName = call.arguments[@"cameraName"];
     NSString *resolutionPreset = call.arguments[@"resolutionPreset"];
     NSNumber *enableAudio = call.arguments[@"enableAudio"];
@@ -829,6 +848,7 @@
                                          enableAudio:[enableAudio boolValue]
                                        dispatchQueue:_dispatchQueue
                                                error:&error];
+
     if (error) {
       result(getFlutterError(error));
     } else {
@@ -837,25 +857,10 @@
       }
       int64_t textureId = [_registry registerTexture:cam];
       _camera = cam;
-      __weak CameraPlugin *weakSelf = self;
-      cam.onFrameAvailable = ^{
-        [weakSelf.registry textureFrameAvailable:textureId];
-      };
-      FlutterEventChannel *eventChannel = [FlutterEventChannel
-          eventChannelWithName:[NSString
-                                   stringWithFormat:@"flutter.io/cameraPlugin/cameraEvents%lld",
-                                                    textureId]
-               binaryMessenger:_messenger];
-      [eventChannel setStreamHandler:cam];
-      cam.eventChannel = eventChannel;
+
       result(@{
-        @"textureId" : @(textureId),
-        @"previewWidth" : @(cam.previewSize.width),
-        @"previewHeight" : @(cam.previewSize.height),
-        @"captureWidth" : @(cam.captureSize.width),
-        @"captureHeight" : @(cam.captureSize.height),
+        @"cameraId" : @(textureId),
       });
-      [cam start];
     }
   } else if ([@"startImageStream" isEqualToString:call.method]) {
     [_camera startImageStreamWithMessenger:_messenger];
@@ -863,23 +868,34 @@
   } else if ([@"stopImageStream" isEqualToString:call.method]) {
     [_camera stopImageStream];
     result(nil);
-  } else if ([@"pauseVideoRecording" isEqualToString:call.method]) {
-    [_camera pauseVideoRecording];
-    result(nil);
-  } else if ([@"resumeVideoRecording" isEqualToString:call.method]) {
-    [_camera resumeVideoRecording];
-    result(nil);
   } else {
     NSDictionary *argsMap = call.arguments;
-    NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue;
-    if ([@"takePicture" isEqualToString:call.method]) {
+    NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue;
+    if ([@"initialize" isEqualToString:call.method]) {
+      __weak CameraPlugin *weakSelf = self;
+      _camera.onFrameAvailable = ^{
+        [weakSelf.registry textureFrameAvailable:cameraId];
+      };
+      FlutterMethodChannel *methodChannel = [FlutterMethodChannel
+          methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu",
+                                                           (unsigned long)cameraId]
+                binaryMessenger:_messenger];
+      _camera.methodChannel = methodChannel;
+      [methodChannel invokeMethod:@"initialized"
+                        arguments:@{
+                          @"previewWidth" : @(_camera.previewSize.width),
+                          @"previewHeight" : @(_camera.previewSize.height)
+                        }];
+      [_camera start];
+      result(nil);
+    } else if ([@"takePicture" isEqualToString:call.method]) {
       if (@available(iOS 10.0, *)) {
-        [_camera captureToFile:call.arguments[@"path"] result:result];
+        [_camera captureToFile:result];
       } else {
         result(FlutterMethodNotImplemented);
       }
     } else if ([@"dispose" isEqualToString:call.method]) {
-      [_registry unregisterTexture:textureId];
+      [_registry unregisterTexture:cameraId];
       [_camera close];
       _dispatchQueue = nil;
       result(nil);
@@ -887,9 +903,13 @@
       [_camera setUpCaptureSessionForAudio];
       result(nil);
     } else if ([@"startVideoRecording" isEqualToString:call.method]) {
-      [_camera startVideoRecordingAtPath:call.arguments[@"filePath"] result:result];
+      [_camera startVideoRecordingWithResult:result];
     } else if ([@"stopVideoRecording" isEqualToString:call.method]) {
       [_camera stopVideoRecordingWithResult:result];
+    } else if ([@"pauseVideoRecording" isEqualToString:call.method]) {
+      [_camera pauseVideoRecordingWithResult:result];
+    } else if ([@"resumeVideoRecording" isEqualToString:call.method]) {
+      [_camera resumeVideoRecordingWithResult:result];
     } else {
       result(FlutterMethodNotImplemented);
     }
diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart
index 3b2cd77..6c6d90b 100644
--- a/packages/camera/camera/lib/camera.dart
+++ b/packages/camera/camera/lib/camera.dart
@@ -2,643 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-import 'dart:typed_data';
+export 'src/camera_controller.dart';
+export 'src/camera_image.dart';
+export 'src/camera_preview.dart';
 
-import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
-
-part 'camera_image.dart';
-
-final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera');
-
-/// The direction the camera is facing.
-enum CameraLensDirection {
-  /// Front facing camera (a user looking at the screen is seen by the camera).
-  front,
-
-  /// Back facing camera (a user looking at the screen is not seen by the camera).
-  back,
-
-  /// External camera which may not be mounted to the device.
-  external,
-}
-
-/// Affect the quality of video recording and image capture:
-///
-/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically.
-enum ResolutionPreset {
-  /// 352x288 on iOS, 240p (320x240) on Android
-  low,
-
-  /// 480p (640x480 on iOS, 720x480 on Android)
-  medium,
-
-  /// 720p (1280x720)
-  high,
-
-  /// 1080p (1920x1080)
-  veryHigh,
-
-  /// 2160p (3840x2160)
-  ultraHigh,
-
-  /// The highest resolution available.
-  max,
-}
-
-/// Signature for a callback receiving the a camera image.
-///
-/// This is used by [CameraController.startImageStream].
-// ignore: inference_failure_on_function_return_type
-typedef onLatestImageAvailable = Function(CameraImage image);
-
-/// Returns the resolution preset as a String.
-String serializeResolutionPreset(ResolutionPreset resolutionPreset) {
-  switch (resolutionPreset) {
-    case ResolutionPreset.max:
-      return 'max';
-    case ResolutionPreset.ultraHigh:
-      return 'ultraHigh';
-    case ResolutionPreset.veryHigh:
-      return 'veryHigh';
-    case ResolutionPreset.high:
-      return 'high';
-    case ResolutionPreset.medium:
-      return 'medium';
-    case ResolutionPreset.low:
-      return 'low';
-  }
-  throw ArgumentError('Unknown ResolutionPreset value');
-}
-
-CameraLensDirection _parseCameraLensDirection(String string) {
-  switch (string) {
-    case 'front':
-      return CameraLensDirection.front;
-    case 'back':
-      return CameraLensDirection.back;
-    case 'external':
-      return CameraLensDirection.external;
-  }
-  throw ArgumentError('Unknown CameraLensDirection value');
-}
-
-/// Completes with a list of available cameras.
-///
-/// May throw a [CameraException].
-Future<List<CameraDescription>> availableCameras() async {
-  try {
-    final List<Map<dynamic, dynamic>> cameras = await _channel
-        .invokeListMethod<Map<dynamic, dynamic>>('availableCameras');
-    return cameras.map((Map<dynamic, dynamic> camera) {
-      return CameraDescription(
-        name: camera['name'],
-        lensDirection: _parseCameraLensDirection(camera['lensFacing']),
-        sensorOrientation: camera['sensorOrientation'],
-      );
-    }).toList();
-  } on PlatformException catch (e) {
-    throw CameraException(e.code, e.message);
-  }
-}
-
-/// Properties of a camera device.
-class CameraDescription {
-  /// Creates a new camera description with the given properties.
-  CameraDescription({this.name, this.lensDirection, this.sensorOrientation});
-
-  /// The name of the camera device.
-  final String name;
-
-  /// The direction the camera is facing.
-  final CameraLensDirection lensDirection;
-
-  /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation.
-  ///
-  /// **Range of valid values:**
-  /// 0, 90, 180, 270
-  ///
-  /// On Android, also defines the direction of rolling shutter readout, which
-  /// is from top to bottom in the sensor's coordinate system.
-  final int sensorOrientation;
-
-  @override
-  bool operator ==(Object o) {
-    return o is CameraDescription &&
-        o.name == name &&
-        o.lensDirection == lensDirection;
-  }
-
-  @override
-  int get hashCode {
-    return hashValues(name, lensDirection);
-  }
-
-  @override
-  String toString() {
-    return '$runtimeType($name, $lensDirection, $sensorOrientation)';
-  }
-}
-
-/// This is thrown when the plugin reports an error.
-class CameraException implements Exception {
-  /// Creates a new camera exception with the given error code and description.
-  CameraException(this.code, this.description);
-
-  /// Error code.
-  // TODO(bparrishMines): Document possible error codes.
-  // https://github.com/flutter/flutter/issues/69298
-  String code;
-
-  /// Textual description of the error.
-  String description;
-
-  @override
-  String toString() => '$runtimeType($code, $description)';
-}
-
-/// A widget showing a live camera preview.
-class CameraPreview extends StatelessWidget {
-  /// Creates a preview widget for the given camera controller.
-  const CameraPreview(this.controller);
-
-  /// The controller for the camera that the preview is shown for.
-  final CameraController controller;
-
-  @override
-  Widget build(BuildContext context) {
-    return controller.value.isInitialized
-        ? Texture(textureId: controller._textureId)
-        : Container();
-  }
-}
-
-/// The state of a [CameraController].
-class CameraValue {
-  /// Creates a new camera controller state.
-  const CameraValue({
-    this.isInitialized,
-    this.errorDescription,
-    this.previewSize,
-    this.isRecordingVideo,
-    this.isTakingPicture,
-    this.isStreamingImages,
-    bool isRecordingPaused,
-  }) : _isRecordingPaused = isRecordingPaused;
-
-  /// Creates a new camera controller state for an uninitialzed controller.
-  const CameraValue.uninitialized()
-      : this(
-          isInitialized: false,
-          isRecordingVideo: false,
-          isTakingPicture: false,
-          isStreamingImages: false,
-          isRecordingPaused: false,
-        );
-
-  /// True after [CameraController.initialize] has completed successfully.
-  final bool isInitialized;
-
-  /// True when a picture capture request has been sent but as not yet returned.
-  final bool isTakingPicture;
-
-  /// True when the camera is recording (not the same as previewing).
-  final bool isRecordingVideo;
-
-  /// True when images from the camera are being streamed.
-  final bool isStreamingImages;
-
-  final bool _isRecordingPaused;
-
-  /// True when camera [isRecordingVideo] and recording is paused.
-  bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused;
-
-  /// Description of an error state.
-  ///
-  /// This is null while the controller is not in an error state.
-  /// When [hasError] is true this contains the error description.
-  final String errorDescription;
-
-  /// The size of the preview in pixels.
-  ///
-  /// Is `null` until  [isInitialized] is `true`.
-  final Size previewSize;
-
-  /// Convenience getter for `previewSize.height / previewSize.width`.
-  ///
-  /// Can only be called when [initialize] is done.
-  double get aspectRatio => previewSize.height / previewSize.width;
-
-  /// Whether the controller is in an error state.
-  ///
-  /// When true [errorDescription] describes the error.
-  bool get hasError => errorDescription != null;
-
-  /// Creates a modified copy of the object.
-  ///
-  /// Explicitly specified fields get the specified value, all other fields get
-  /// the same value of the current object.
-  CameraValue copyWith({
-    bool isInitialized,
-    bool isRecordingVideo,
-    bool isTakingPicture,
-    bool isStreamingImages,
-    String errorDescription,
-    Size previewSize,
-    bool isRecordingPaused,
-  }) {
-    return CameraValue(
-      isInitialized: isInitialized ?? this.isInitialized,
-      errorDescription: errorDescription,
-      previewSize: previewSize ?? this.previewSize,
-      isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo,
-      isTakingPicture: isTakingPicture ?? this.isTakingPicture,
-      isStreamingImages: isStreamingImages ?? this.isStreamingImages,
-      isRecordingPaused: isRecordingPaused ?? _isRecordingPaused,
-    );
-  }
-
-  @override
-  String toString() {
-    return '$runtimeType('
-        'isRecordingVideo: $isRecordingVideo, '
-        'isRecordingVideo: $isRecordingVideo, '
-        'isInitialized: $isInitialized, '
-        'errorDescription: $errorDescription, '
-        'previewSize: $previewSize, '
-        'isStreamingImages: $isStreamingImages)';
-  }
-}
-
-/// Controls a device camera.
-///
-/// Use [availableCameras] to get a list of available cameras.
-///
-/// Before using a [CameraController] a call to [initialize] must complete.
-///
-/// To show the camera preview on the screen use a [CameraPreview] widget.
-class CameraController extends ValueNotifier<CameraValue> {
-  /// Creates a new camera controller in an uninitialized state.
-  CameraController(
-    this.description,
-    this.resolutionPreset, {
-    this.enableAudio = true,
-  }) : super(const CameraValue.uninitialized());
-
-  /// The properties of the camera device controlled by this controller.
-  final CameraDescription description;
-
-  /// The resolution this controller is targeting.
-  ///
-  /// This resolution preset is not guaranteed to be available on the device,
-  /// if unavailable a lower resolution will be used.
-  ///
-  /// See also: [ResolutionPreset].
-  final ResolutionPreset resolutionPreset;
-
-  /// Whether to include audio when recording a video.
-  final bool enableAudio;
-
-  int _textureId;
-  bool _isDisposed = false;
-  StreamSubscription<dynamic> _eventSubscription;
-  StreamSubscription<dynamic> _imageStreamSubscription;
-  Completer<void> _creatingCompleter;
-
-  /// Checks whether [CameraController.dispose] has completed successfully.
-  ///
-  /// This is a no-op when asserts are disabled.
-  void debugCheckIsDisposed() {
-    assert(_isDisposed);
-  }
-
-  /// Initializes the camera on the device.
-  ///
-  /// Throws a [CameraException] if the initialization fails.
-  Future<void> initialize() async {
-    if (_isDisposed) {
-      return Future<void>.value();
-    }
-    try {
-      _creatingCompleter = Completer<void>();
-      final Map<String, dynamic> reply =
-          await _channel.invokeMapMethod<String, dynamic>(
-        'initialize',
-        <String, dynamic>{
-          'cameraName': description.name,
-          'resolutionPreset': serializeResolutionPreset(resolutionPreset),
-          'enableAudio': enableAudio,
-        },
-      );
-      _textureId = reply['textureId'];
-      value = value.copyWith(
-        isInitialized: true,
-        previewSize: Size(
-          reply['previewWidth'].toDouble(),
-          reply['previewHeight'].toDouble(),
-        ),
-      );
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-    _eventSubscription =
-        EventChannel('flutter.io/cameraPlugin/cameraEvents$_textureId')
-            .receiveBroadcastStream()
-            .listen(_listener);
-    _creatingCompleter.complete();
-    return _creatingCompleter.future;
-  }
-
-  /// Prepare the capture session for video recording.
-  ///
-  /// Use of this method is optional, but it may be called for performance
-  /// reasons on iOS.
-  ///
-  /// Preparing audio can cause a minor delay in the CameraPreview view on iOS.
-  /// If video recording is intended, calling this early eliminates this delay
-  /// that would otherwise be experienced when video recording is started.
-  /// This operation is a no-op on Android.
-  ///
-  /// Throws a [CameraException] if the prepare fails.
-  Future<void> prepareForVideoRecording() async {
-    await _channel.invokeMethod<void>('prepareForVideoRecording');
-  }
-
-  /// Listen to events from the native plugins.
-  ///
-  /// A "cameraClosing" event is sent when the camera is closed automatically by the system (for example when the app go to background). The plugin will try to reopen the camera automatically but any ongoing recording will end.
-  void _listener(dynamic event) {
-    final Map<dynamic, dynamic> map = event;
-    if (_isDisposed) {
-      return;
-    }
-
-    switch (map['eventType']) {
-      case 'error':
-        value = value.copyWith(errorDescription: event['errorDescription']);
-        break;
-      case 'cameraClosing':
-        value = value.copyWith(isRecordingVideo: false);
-        break;
-    }
-  }
-
-  /// Captures an image and saves it to [path].
-  ///
-  /// A path can for example be obtained using
-  /// [path_provider](https://pub.dartlang.org/packages/path_provider).
-  ///
-  /// If a file already exists at the provided path an error will be thrown.
-  /// The file can be read as this function returns.
-  ///
-  /// Throws a [CameraException] if the capture fails.
-  Future<void> takePicture(String path) async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController.',
-        'takePicture was called on uninitialized CameraController',
-      );
-    }
-    if (value.isTakingPicture) {
-      throw CameraException(
-        'Previous capture has not returned yet.',
-        'takePicture was called before the previous capture returned.',
-      );
-    }
-    try {
-      value = value.copyWith(isTakingPicture: true);
-      await _channel.invokeMethod<void>(
-        'takePicture',
-        <String, dynamic>{'textureId': _textureId, 'path': path},
-      );
-      value = value.copyWith(isTakingPicture: false);
-    } on PlatformException catch (e) {
-      value = value.copyWith(isTakingPicture: false);
-      throw CameraException(e.code, e.message);
-    }
-  }
-
-  /// Start streaming images from platform camera.
-  ///
-  /// Settings for capturing images on iOS and Android is set to always use the
-  /// latest image available from the camera and will drop all other images.
-  ///
-  /// When running continuously with [CameraPreview] widget, this function runs
-  /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can
-  /// have significant frame rate drops for [CameraPreview] on lower end
-  /// devices.
-  ///
-  /// Throws a [CameraException] if image streaming or video recording has
-  /// already started.
-  // TODO(bmparr): Add settings for resolution and fps.
-  Future<void> startImageStream(onLatestImageAvailable onAvailable) async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'startImageStream was called on uninitialized CameraController.',
-      );
-    }
-    if (value.isRecordingVideo) {
-      throw CameraException(
-        'A video recording is already started.',
-        'startImageStream was called while a video is being recorded.',
-      );
-    }
-    if (value.isStreamingImages) {
-      throw CameraException(
-        'A camera has started streaming images.',
-        'startImageStream was called while a camera was streaming images.',
-      );
-    }
-
-    try {
-      await _channel.invokeMethod<void>('startImageStream');
-      value = value.copyWith(isStreamingImages: true);
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-    const EventChannel cameraEventChannel =
-        EventChannel('plugins.flutter.io/camera/imageStream');
-    _imageStreamSubscription =
-        cameraEventChannel.receiveBroadcastStream().listen(
-      (dynamic imageData) {
-        onAvailable(CameraImage._fromPlatformData(imageData));
-      },
-    );
-  }
-
-  /// Stop streaming images from platform camera.
-  ///
-  /// Throws a [CameraException] if image streaming was not started or video
-  /// recording was started.
-  Future<void> stopImageStream() async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'stopImageStream was called on uninitialized CameraController.',
-      );
-    }
-    if (value.isRecordingVideo) {
-      throw CameraException(
-        'A video recording is already started.',
-        'stopImageStream was called while a video is being recorded.',
-      );
-    }
-    if (!value.isStreamingImages) {
-      throw CameraException(
-        'No camera is streaming images',
-        'stopImageStream was called when no camera is streaming images.',
-      );
-    }
-
-    try {
-      value = value.copyWith(isStreamingImages: false);
-      await _channel.invokeMethod<void>('stopImageStream');
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-
-    await _imageStreamSubscription.cancel();
-    _imageStreamSubscription = null;
-  }
-
-  /// Start a video recording and save the file to [path].
-  ///
-  /// A path can for example be obtained using
-  /// [path_provider](https://pub.dartlang.org/packages/path_provider).
-  ///
-  /// The file is written on the flight as the video is being recorded.
-  /// If a file already exists at the provided path an error will be thrown.
-  /// The file can be read as soon as [stopVideoRecording] returns.
-  ///
-  /// Throws a [CameraException] if the capture fails.
-  Future<void> startVideoRecording(String filePath) async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'startVideoRecording was called on uninitialized CameraController',
-      );
-    }
-    if (value.isRecordingVideo) {
-      throw CameraException(
-        'A video recording is already started.',
-        'startVideoRecording was called when a recording is already started.',
-      );
-    }
-    if (value.isStreamingImages) {
-      throw CameraException(
-        'A camera has started streaming images.',
-        'startVideoRecording was called while a camera was streaming images.',
-      );
-    }
-
-    try {
-      await _channel.invokeMethod<void>(
-        'startVideoRecording',
-        <String, dynamic>{'textureId': _textureId, 'filePath': filePath},
-      );
-      value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false);
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-  }
-
-  /// Stop recording.
-  Future<void> stopVideoRecording() async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'stopVideoRecording was called on uninitialized CameraController',
-      );
-    }
-    if (!value.isRecordingVideo) {
-      throw CameraException(
-        'No video is recording',
-        'stopVideoRecording was called when no video is recording.',
-      );
-    }
-    try {
-      value = value.copyWith(isRecordingVideo: false);
-      await _channel.invokeMethod<void>(
-        'stopVideoRecording',
-        <String, dynamic>{'textureId': _textureId},
-      );
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-  }
-
-  /// Pause video recording.
-  ///
-  /// This feature is only available on iOS and Android sdk 24+.
-  Future<void> pauseVideoRecording() async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'pauseVideoRecording was called on uninitialized CameraController',
-      );
-    }
-    if (!value.isRecordingVideo) {
-      throw CameraException(
-        'No video is recording',
-        'pauseVideoRecording was called when no video is recording.',
-      );
-    }
-    try {
-      value = value.copyWith(isRecordingPaused: true);
-      await _channel.invokeMethod<void>(
-        'pauseVideoRecording',
-        <String, dynamic>{'textureId': _textureId},
-      );
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-  }
-
-  /// Resume video recording after pausing.
-  ///
-  /// This feature is only available on iOS and Android sdk 24+.
-  Future<void> resumeVideoRecording() async {
-    if (!value.isInitialized || _isDisposed) {
-      throw CameraException(
-        'Uninitialized CameraController',
-        'resumeVideoRecording was called on uninitialized CameraController',
-      );
-    }
-    if (!value.isRecordingVideo) {
-      throw CameraException(
-        'No video is recording',
-        'resumeVideoRecording was called when no video is recording.',
-      );
-    }
-    try {
-      value = value.copyWith(isRecordingPaused: false);
-      await _channel.invokeMethod<void>(
-        'resumeVideoRecording',
-        <String, dynamic>{'textureId': _textureId},
-      );
-    } on PlatformException catch (e) {
-      throw CameraException(e.code, e.message);
-    }
-  }
-
-  /// Releases the resources of this camera.
-  @override
-  Future<void> dispose() async {
-    if (_isDisposed) {
-      return;
-    }
-    _isDisposed = true;
-    super.dispose();
-    if (_creatingCompleter != null) {
-      await _creatingCompleter.future;
-      await _channel.invokeMethod<void>(
-        'dispose',
-        <String, dynamic>{'textureId': _textureId},
-      );
-      await _eventSubscription?.cancel();
-    }
-  }
-}
+export 'package:camera_platform_interface/camera_platform_interface.dart'
+    show
+        CameraDescription,
+        CameraException,
+        CameraLensDirection,
+        ResolutionPreset,
+        XFile;
diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart
new file mode 100644
index 0000000..fcf0024
--- /dev/null
+++ b/packages/camera/camera/lib/src/camera_controller.dart
@@ -0,0 +1,484 @@
+// Copyright 2018 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 'dart:async';
+
+import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera');
+
+/// Signature for a callback receiving the a camera image.
+///
+/// This is used by [CameraController.startImageStream].
+// ignore: inference_failure_on_function_return_type
+typedef onLatestImageAvailable = Function(CameraImage image);
+
+/// Completes with a list of available cameras.
+///
+/// May throw a [CameraException].
+Future<List<CameraDescription>> availableCameras() async {
+  return CameraPlatform.instance.availableCameras();
+}
+
+/// The state of a [CameraController].
+class CameraValue {
+  /// Creates a new camera controller state.
+  const CameraValue({
+    this.isInitialized,
+    this.errorDescription,
+    this.previewSize,
+    this.isRecordingVideo,
+    this.isTakingPicture,
+    this.isStreamingImages,
+    bool isRecordingPaused,
+  }) : _isRecordingPaused = isRecordingPaused;
+
+  /// Creates a new camera controller state for an uninitialized controller.
+  const CameraValue.uninitialized()
+      : this(
+          isInitialized: false,
+          isRecordingVideo: false,
+          isTakingPicture: false,
+          isStreamingImages: false,
+          isRecordingPaused: false,
+        );
+
+  /// True after [CameraController.initialize] has completed successfully.
+  final bool isInitialized;
+
+  /// True when a picture capture request has been sent but as not yet returned.
+  final bool isTakingPicture;
+
+  /// True when the camera is recording (not the same as previewing).
+  final bool isRecordingVideo;
+
+  /// True when images from the camera are being streamed.
+  final bool isStreamingImages;
+
+  final bool _isRecordingPaused;
+
+  /// True when camera [isRecordingVideo] and recording is paused.
+  bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused;
+
+  /// Description of an error state.
+  ///
+  /// This is null while the controller is not in an error state.
+  /// When [hasError] is true this contains the error description.
+  final String errorDescription;
+
+  /// The size of the preview in pixels.
+  ///
+  /// Is `null` until  [isInitialized] is `true`.
+  final Size previewSize;
+
+  /// Convenience getter for `previewSize.height / previewSize.width`.
+  ///
+  /// Can only be called when [initialize] is done.
+  double get aspectRatio => previewSize.height / previewSize.width;
+
+  /// Whether the controller is in an error state.
+  ///
+  /// When true [errorDescription] describes the error.
+  bool get hasError => errorDescription != null;
+
+  /// Creates a modified copy of the object.
+  ///
+  /// Explicitly specified fields get the specified value, all other fields get
+  /// the same value of the current object.
+  CameraValue copyWith({
+    bool isInitialized,
+    bool isRecordingVideo,
+    bool isTakingPicture,
+    bool isStreamingImages,
+    String errorDescription,
+    Size previewSize,
+    bool isRecordingPaused,
+  }) {
+    return CameraValue(
+      isInitialized: isInitialized ?? this.isInitialized,
+      errorDescription: errorDescription,
+      previewSize: previewSize ?? this.previewSize,
+      isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo,
+      isTakingPicture: isTakingPicture ?? this.isTakingPicture,
+      isStreamingImages: isStreamingImages ?? this.isStreamingImages,
+      isRecordingPaused: isRecordingPaused ?? _isRecordingPaused,
+    );
+  }
+
+  @override
+  String toString() {
+    return '$runtimeType('
+        'isRecordingVideo: $isRecordingVideo, '
+        'isInitialized: $isInitialized, '
+        'errorDescription: $errorDescription, '
+        'previewSize: $previewSize, '
+        'isStreamingImages: $isStreamingImages)';
+  }
+}
+
+/// Controls a device camera.
+///
+/// Use [availableCameras] to get a list of available cameras.
+///
+/// Before using a [CameraController] a call to [initialize] must complete.
+///
+/// To show the camera preview on the screen use a [CameraPreview] widget.
+class CameraController extends ValueNotifier<CameraValue> {
+  /// Creates a new camera controller in an uninitialized state.
+  CameraController(
+    this.description,
+    this.resolutionPreset, {
+    this.enableAudio = true,
+  }) : super(const CameraValue.uninitialized());
+
+  /// The properties of the camera device controlled by this controller.
+  final CameraDescription description;
+
+  /// The resolution this controller is targeting.
+  ///
+  /// This resolution preset is not guaranteed to be available on the device,
+  /// if unavailable a lower resolution will be used.
+  ///
+  /// See also: [ResolutionPreset].
+  final ResolutionPreset resolutionPreset;
+
+  /// Whether to include audio when recording a video.
+  final bool enableAudio;
+
+  int _cameraId;
+  bool _isDisposed = false;
+  StreamSubscription<dynamic> _imageStreamSubscription;
+  FutureOr<bool> _initCalled;
+
+  /// Checks whether [CameraController.dispose] has completed successfully.
+  ///
+  /// This is a no-op when asserts are disabled.
+  void debugCheckIsDisposed() {
+    assert(_isDisposed);
+  }
+
+  /// The camera identifier with which the controller is associated.
+  int get cameraId => _cameraId;
+
+  /// Initializes the camera on the device.
+  ///
+  /// Throws a [CameraException] if the initialization fails.
+  Future<void> initialize() async {
+    if (_isDisposed) {
+      throw CameraException(
+        'Disposed CameraController',
+        'initialize was called on a disposed CameraController',
+      );
+    }
+    try {
+      _cameraId = await CameraPlatform.instance.createCamera(
+        description,
+        resolutionPreset,
+        enableAudio: enableAudio,
+      );
+
+      final previewSize =
+          CameraPlatform.instance.onCameraInitialized(_cameraId).map((event) {
+        return Size(
+          event.previewWidth,
+          event.previewHeight,
+        );
+      }).first;
+
+      await CameraPlatform.instance.initializeCamera(_cameraId);
+
+      value = value.copyWith(
+        isInitialized: true,
+        previewSize: await previewSize,
+      );
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+
+    _initCalled = true;
+  }
+
+  /// Prepare the capture session for video recording.
+  ///
+  /// Use of this method is optional, but it may be called for performance
+  /// reasons on iOS.
+  ///
+  /// Preparing audio can cause a minor delay in the CameraPreview view on iOS.
+  /// If video recording is intended, calling this early eliminates this delay
+  /// that would otherwise be experienced when video recording is started.
+  /// This operation is a no-op on Android.
+  ///
+  /// Throws a [CameraException] if the prepare fails.
+  Future<void> prepareForVideoRecording() async {
+    await CameraPlatform.instance.prepareForVideoRecording();
+  }
+
+  /// Captures an image and saves it to [path].
+  ///
+  /// A path can for example be obtained using
+  /// [path_provider](https://pub.dartlang.org/packages/path_provider).
+  ///
+  /// If a file already exists at the provided path an error will be thrown.
+  /// The file can be read as this function returns.
+  ///
+  /// Throws a [CameraException] if the capture fails.
+  Future<XFile> takePicture() async {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController.',
+        'takePicture was called on uninitialized CameraController',
+      );
+    }
+    if (value.isTakingPicture) {
+      throw CameraException(
+        'Previous capture has not returned yet.',
+        'takePicture was called before the previous capture returned.',
+      );
+    }
+    try {
+      value = value.copyWith(isTakingPicture: true);
+      XFile file = await CameraPlatform.instance.takePicture(_cameraId);
+      value = value.copyWith(isTakingPicture: false);
+      return file;
+    } on PlatformException catch (e) {
+      value = value.copyWith(isTakingPicture: false);
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Start streaming images from platform camera.
+  ///
+  /// Settings for capturing images on iOS and Android is set to always use the
+  /// latest image available from the camera and will drop all other images.
+  ///
+  /// When running continuously with [CameraPreview] widget, this function runs
+  /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can
+  /// have significant frame rate drops for [CameraPreview] on lower end
+  /// devices.
+  ///
+  /// Throws a [CameraException] if image streaming or video recording has
+  /// already started.
+  ///
+  /// The `startImageStream` method is only available on Android and iOS (other
+  /// platforms won't be supported in current setup).
+  ///
+  // TODO(bmparr): Add settings for resolution and fps.
+  Future<void> startImageStream(onLatestImageAvailable onAvailable) async {
+    assert(defaultTargetPlatform == TargetPlatform.android ||
+        defaultTargetPlatform == TargetPlatform.iOS);
+
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'startImageStream was called on uninitialized CameraController.',
+      );
+    }
+    if (value.isRecordingVideo) {
+      throw CameraException(
+        'A video recording is already started.',
+        'startImageStream was called while a video is being recorded.',
+      );
+    }
+    if (value.isStreamingImages) {
+      throw CameraException(
+        'A camera has started streaming images.',
+        'startImageStream was called while a camera was streaming images.',
+      );
+    }
+
+    try {
+      await _channel.invokeMethod<void>('startImageStream');
+      value = value.copyWith(isStreamingImages: true);
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+    const EventChannel cameraEventChannel =
+        EventChannel('plugins.flutter.io/camera/imageStream');
+    _imageStreamSubscription =
+        cameraEventChannel.receiveBroadcastStream().listen(
+      (dynamic imageData) {
+        onAvailable(CameraImage.fromPlatformData(imageData));
+      },
+    );
+  }
+
+  /// Stop streaming images from platform camera.
+  ///
+  /// Throws a [CameraException] if image streaming was not started or video
+  /// recording was started.
+  ///
+  /// The `stopImageStream` method is only available on Android and iOS (other
+  /// platforms won't be supported in current setup).
+  Future<void> stopImageStream() async {
+    assert(defaultTargetPlatform == TargetPlatform.android ||
+        defaultTargetPlatform == TargetPlatform.iOS);
+
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'stopImageStream was called on uninitialized CameraController.',
+      );
+    }
+    if (value.isRecordingVideo) {
+      throw CameraException(
+        'A video recording is already started.',
+        'stopImageStream was called while a video is being recorded.',
+      );
+    }
+    if (!value.isStreamingImages) {
+      throw CameraException(
+        'No camera is streaming images',
+        'stopImageStream was called when no camera is streaming images.',
+      );
+    }
+
+    try {
+      value = value.copyWith(isStreamingImages: false);
+      await _channel.invokeMethod<void>('stopImageStream');
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+
+    await _imageStreamSubscription.cancel();
+    _imageStreamSubscription = null;
+  }
+
+  /// Start a video recording.
+  ///
+  /// The video is returned as a [XFile] after calling [stopVideoRecording].
+  /// Throws a [CameraException] if the capture fails.
+  Future<void> startVideoRecording() async {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'startVideoRecording was called on uninitialized CameraController',
+      );
+    }
+    if (value.isRecordingVideo) {
+      throw CameraException(
+        'A video recording is already started.',
+        'startVideoRecording was called when a recording is already started.',
+      );
+    }
+    if (value.isStreamingImages) {
+      throw CameraException(
+        'A camera has started streaming images.',
+        'startVideoRecording was called while a camera was streaming images.',
+      );
+    }
+
+    try {
+      await CameraPlatform.instance.startVideoRecording(_cameraId);
+      value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false);
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Stops the video recording and returns the file where it was saved.
+  ///
+  /// Throws a [CameraException] if the capture failed.
+  Future<XFile> stopVideoRecording() async {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'stopVideoRecording was called on uninitialized CameraController',
+      );
+    }
+    if (!value.isRecordingVideo) {
+      throw CameraException(
+        'No video is recording',
+        'stopVideoRecording was called when no video is recording.',
+      );
+    }
+    try {
+      XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId);
+      value = value.copyWith(isRecordingVideo: false);
+      return file;
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Pause video recording.
+  ///
+  /// This feature is only available on iOS and Android sdk 24+.
+  Future<void> pauseVideoRecording() async {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'pauseVideoRecording was called on uninitialized CameraController',
+      );
+    }
+    if (!value.isRecordingVideo) {
+      throw CameraException(
+        'No video is recording',
+        'pauseVideoRecording was called when no video is recording.',
+      );
+    }
+    try {
+      await CameraPlatform.instance.pauseVideoRecording(_cameraId);
+      value = value.copyWith(isRecordingPaused: true);
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Resume video recording after pausing.
+  ///
+  /// This feature is only available on iOS and Android sdk 24+.
+  Future<void> resumeVideoRecording() async {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'resumeVideoRecording was called on uninitialized CameraController',
+      );
+    }
+    if (!value.isRecordingVideo) {
+      throw CameraException(
+        'No video is recording',
+        'resumeVideoRecording was called when no video is recording.',
+      );
+    }
+    try {
+      await CameraPlatform.instance.resumeVideoRecording(_cameraId);
+      value = value.copyWith(isRecordingPaused: false);
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Returns a widget showing a live camera preview.
+  Widget buildPreview() {
+    if (!value.isInitialized || _isDisposed) {
+      throw CameraException(
+        'Uninitialized CameraController',
+        'buildView() was called on uninitialized CameraController.',
+      );
+    }
+    try {
+      return CameraPlatform.instance.buildPreview(_cameraId);
+    } on PlatformException catch (e) {
+      throw CameraException(e.code, e.message);
+    }
+  }
+
+  /// Releases the resources of this camera.
+  @override
+  Future<void> dispose() async {
+    if (_isDisposed) {
+      return;
+    }
+    _isDisposed = true;
+    super.dispose();
+    if (_initCalled != null) {
+      await _initCalled;
+      await CameraPlatform.instance.dispose(_cameraId);
+    }
+  }
+}
diff --git a/packages/camera/camera/lib/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart
similarity index 95%
rename from packages/camera/camera/lib/camera_image.dart
rename to packages/camera/camera/lib/src/camera_image.dart
index cebc148..ca8115e 100644
--- a/packages/camera/camera/lib/camera_image.dart
+++ b/packages/camera/camera/lib/src/camera_image.dart
@@ -2,7 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-part of 'camera.dart';
+import 'dart:typed_data';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
 
 /// A single color plane of image data.
 ///
@@ -113,7 +116,8 @@
 /// Although not all image formats are planar on iOS, we treat 1-dimensional
 /// images as single planar images.
 class CameraImage {
-  CameraImage._fromPlatformData(Map<dynamic, dynamic> data)
+  /// CameraImage Constructor
+  CameraImage.fromPlatformData(Map<dynamic, dynamic> data)
       : format = ImageFormat._fromPlatformData(data['format']),
         height = data['height'],
         width = data['width'],
diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart
new file mode 100644
index 0000000..bf7862e
--- /dev/null
+++ b/packages/camera/camera/lib/src/camera_preview.dart
@@ -0,0 +1,23 @@
+// Copyright 2018 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 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// A widget showing a live camera preview.
+class CameraPreview extends StatelessWidget {
+  /// Creates a preview widget for the given camera controller.
+  const CameraPreview(this.controller);
+
+  /// The controller for the camera that the preview is shown for.
+  final CameraController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return controller.value.isInitialized
+        ? CameraPlatform.instance.buildPreview(controller.cameraId)
+        : Container();
+  }
+}
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 146219d..64d5bba 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -2,12 +2,13 @@
 description: A Flutter plugin for getting information about and controlling the
   camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video,
   and streaming image buffers to dart.
-version: 0.5.8+19
+version: 0.6.0
 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera
 
 dependencies:
   flutter:
     sdk: flutter
+  camera_platform_interface: ^1.0.0
 
 dev_dependencies:
   path_provider: ^0.5.0
@@ -17,6 +18,8 @@
   flutter_driver:
     sdk: flutter
   pedantic: ^1.8.0
+  mockito: ^4.1.3
+  plugin_platform_interface: ^1.0.3
 
 flutter:
   plugin:
diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart
new file mode 100644
index 0000000..be7047f
--- /dev/null
+++ b/packages/camera/camera/test/camera_image_stream_test.dart
@@ -0,0 +1,187 @@
+import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'camera_test.dart';
+import 'utils/method_channel_mock.dart';
+
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  setUp(() {
+    CameraPlatform.instance = MockCameraPlatform();
+  });
+
+  test('startImageStream() throws $CameraException when uninitialized', () {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+
+    expect(
+        () => cameraController.startImageStream((image) => null),
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'Uninitialized CameraController.',
+          'startImageStream was called on uninitialized CameraController.',
+        )));
+  });
+
+  test('startImageStream() throws $CameraException when recording videos',
+      () async {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+
+    await cameraController.initialize();
+
+    cameraController.value =
+        cameraController.value.copyWith(isRecordingVideo: true);
+
+    expect(
+        () => cameraController.startImageStream((image) => null),
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'A video recording is already started.',
+          'startImageStream was called while a video is being recorded.',
+        )));
+  });
+  test(
+      'startImageStream() throws $CameraException when already streaming images',
+      () async {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+    await cameraController.initialize();
+
+    cameraController.value =
+        cameraController.value.copyWith(isStreamingImages: true);
+    expect(
+        () => cameraController.startImageStream((image) => null),
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'A camera has started streaming images.',
+          'startImageStream was called while a camera was streaming images.',
+        )));
+  });
+
+  test('startImageStream() calls CameraPlatform', () async {
+    MethodChannelMock cameraChannelMock = MethodChannelMock(
+        channelName: 'plugins.flutter.io/camera',
+        methods: {'startImageStream': {}});
+    MethodChannelMock streamChannelMock = MethodChannelMock(
+        channelName: 'plugins.flutter.io/camera/imageStream',
+        methods: {'listen': {}});
+
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+    await cameraController.initialize();
+
+    await cameraController.startImageStream((image) => null);
+
+    expect(cameraChannelMock.log,
+        <Matcher>[isMethodCall('startImageStream', arguments: null)]);
+    expect(streamChannelMock.log,
+        <Matcher>[isMethodCall('listen', arguments: null)]);
+  });
+
+  test('stopImageStream() throws $CameraException when uninitialized', () {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+
+    expect(
+        cameraController.stopImageStream,
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'Uninitialized CameraController.',
+          'stopImageStream was called on uninitialized CameraController.',
+        )));
+  });
+
+  test('stopImageStream() throws $CameraException when recording videos',
+      () async {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+    await cameraController.initialize();
+
+    await cameraController.startImageStream((image) => null);
+    cameraController.value =
+        cameraController.value.copyWith(isRecordingVideo: true);
+    expect(
+        cameraController.stopImageStream,
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'A video recording is already started.',
+          'stopImageStream was called while a video is being recorded.',
+        )));
+  });
+
+  test('stopImageStream() throws $CameraException when not streaming images',
+      () async {
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+    await cameraController.initialize();
+
+    expect(
+        cameraController.stopImageStream,
+        throwsA(isA<CameraException>().having(
+          (error) => error.description,
+          'No camera is streaming images',
+          'stopImageStream was called when no camera is streaming images.',
+        )));
+  });
+
+  test('stopImageStream() intended behaviour', () async {
+    MethodChannelMock cameraChannelMock = MethodChannelMock(
+        channelName: 'plugins.flutter.io/camera',
+        methods: {'startImageStream': {}, 'stopImageStream': {}});
+    MethodChannelMock streamChannelMock = MethodChannelMock(
+        channelName: 'plugins.flutter.io/camera/imageStream',
+        methods: {'listen': {}, 'cancel': {}});
+
+    CameraController cameraController = CameraController(
+        CameraDescription(
+            name: 'cam',
+            lensDirection: CameraLensDirection.back,
+            sensorOrientation: 90),
+        ResolutionPreset.max);
+    await cameraController.initialize();
+    await cameraController.startImageStream((image) => null);
+    await cameraController.stopImageStream();
+
+    expect(cameraChannelMock.log, <Matcher>[
+      isMethodCall('startImageStream', arguments: null),
+      isMethodCall('stopImageStream', arguments: null)
+    ]);
+
+    expect(streamChannelMock.log, <Matcher>[
+      isMethodCall('listen', arguments: null),
+      isMethodCall('cancel', arguments: null)
+    ]);
+  });
+}
diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart
new file mode 100644
index 0000000..c8f808f
--- /dev/null
+++ b/packages/camera/camera/test/camera_image_test.dart
@@ -0,0 +1,113 @@
+// Copyright 2019 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 'dart:typed_data';
+
+import 'package:camera/camera.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('$CameraImage tests', () {
+    test('$CameraImage can be created', () {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+      CameraImage cameraImage = CameraImage.fromPlatformData(<dynamic, dynamic>{
+        'format': 35,
+        'height': 1,
+        'width': 4,
+        'planes': [
+          {
+            'bytes': Uint8List.fromList([1, 2, 3, 4]),
+            'bytesPerPixel': 1,
+            'bytesPerRow': 4,
+            'height': 1,
+            'width': 4
+          }
+        ]
+      });
+      expect(cameraImage.height, 1);
+      expect(cameraImage.width, 4);
+      expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+      expect(cameraImage.planes.length, 1);
+    });
+
+    test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () {
+      debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+
+      CameraImage cameraImage = CameraImage.fromPlatformData(<dynamic, dynamic>{
+        'format': 875704438,
+        'height': 1,
+        'width': 4,
+        'planes': [
+          {
+            'bytes': Uint8List.fromList([1, 2, 3, 4]),
+            'bytesPerPixel': 1,
+            'bytesPerRow': 4,
+            'height': 1,
+            'width': 4
+          }
+        ]
+      });
+      expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+    });
+
+    test('$CameraImage has ImageFormatGroup.yuv420 for Android', () {
+      debugDefaultTargetPlatformOverride = TargetPlatform.android;
+
+      CameraImage cameraImage = CameraImage.fromPlatformData(<dynamic, dynamic>{
+        'format': 35,
+        'height': 1,
+        'width': 4,
+        'planes': [
+          {
+            'bytes': Uint8List.fromList([1, 2, 3, 4]),
+            'bytesPerPixel': 1,
+            'bytesPerRow': 4,
+            'height': 1,
+            'width': 4
+          }
+        ]
+      });
+      expect(cameraImage.format.group, ImageFormatGroup.yuv420);
+    });
+
+    test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () {
+      debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+
+      CameraImage cameraImage = CameraImage.fromPlatformData(<dynamic, dynamic>{
+        'format': 1111970369,
+        'height': 1,
+        'width': 4,
+        'planes': [
+          {
+            'bytes': Uint8List.fromList([1, 2, 3, 4]),
+            'bytesPerPixel': 1,
+            'bytesPerRow': 4,
+            'height': 1,
+            'width': 4
+          }
+        ]
+      });
+      expect(cameraImage.format.group, ImageFormatGroup.bgra8888);
+    });
+    test('$CameraImage has ImageFormatGroup.unknown', () {
+      CameraImage cameraImage = CameraImage.fromPlatformData(<dynamic, dynamic>{
+        'format': null,
+        'height': 1,
+        'width': 4,
+        'planes': [
+          {
+            'bytes': Uint8List.fromList([1, 2, 3, 4]),
+            'bytesPerPixel': 1,
+            'bytesPerRow': 4,
+            'height': 1,
+            'width': 4
+          }
+        ]
+      });
+      expect(cameraImage.format.group, ImageFormatGroup.unknown);
+    });
+  });
+}
diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart
index cc33b36..b129849 100644
--- a/packages/camera/camera/test/camera_test.dart
+++ b/packages/camera/camera/test/camera_test.dart
@@ -1,10 +1,46 @@
 // Copyright 2017 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 'dart:async';
+import 'dart:ui';
+
 import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+get mockAvailableCameras => [
+      CameraDescription(
+          name: 'camBack',
+          lensDirection: CameraLensDirection.back,
+          sensorOrientation: 90),
+      CameraDescription(
+          name: 'camFront',
+          lensDirection: CameraLensDirection.front,
+          sensorOrientation: 180),
+    ];
+
+get mockInitializeCamera => 13;
+
+get mockOnCameraInitializedEvent => CameraInitializedEvent(13, 75, 75);
+
+get mockOnCameraClosingEvent => null;
+
+get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing');
+
+XFile mockTakePicture = XFile('foo/bar.png');
+
+get mockVideoRecordingXFile => null;
+
+bool mockPlatformException = false;
 
 void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
   group('camera', () {
     test('debugCheckIsDisposed should not throw assertion error when disposed',
         () {
@@ -32,7 +68,288 @@
         throwsAssertionError,
       );
     });
+
+    test('availableCameras() has camera', () async {
+      CameraPlatform.instance = MockCameraPlatform();
+
+      var camList = await availableCameras();
+
+      expect(camList, equals(mockAvailableCameras));
+    });
   });
+
+  group('$CameraController', () {
+    setUpAll(() {
+      CameraPlatform.instance = MockCameraPlatform();
+    });
+
+    test('Can be initialized', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      expect(cameraController.value.aspectRatio, 1);
+      expect(cameraController.value.previewSize, Size(75, 75));
+      expect(cameraController.value.isInitialized, isTrue);
+    });
+
+    test('can be disposed', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      expect(cameraController.value.aspectRatio, 1);
+      expect(cameraController.value.previewSize, Size(75, 75));
+      expect(cameraController.value.isInitialized, isTrue);
+
+      await cameraController.dispose();
+
+      verify(CameraPlatform.instance.dispose(13)).called(1);
+    });
+
+    test('initialize() throws CameraException when disposed', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      expect(cameraController.value.aspectRatio, 1);
+      expect(cameraController.value.previewSize, Size(75, 75));
+      expect(cameraController.value.isInitialized, isTrue);
+
+      await cameraController.dispose();
+
+      verify(CameraPlatform.instance.dispose(13)).called(1);
+
+      expect(
+          cameraController.initialize,
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'Error description',
+            'initialize was called on a disposed CameraController',
+          )));
+    });
+
+    test('initialize() throws $CameraException on $PlatformException ',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+
+      mockPlatformException = true;
+
+      expect(
+          cameraController.initialize,
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'foo',
+            'bar',
+          )));
+      mockPlatformException = false;
+    });
+
+    test('prepareForVideoRecording() calls $CameraPlatform ', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      await cameraController.prepareForVideoRecording();
+
+      verify(CameraPlatform.instance.prepareForVideoRecording()).called(1);
+    });
+
+    test('takePicture() throws $CameraException when uninitialized ', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      expect(
+          cameraController.takePicture(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'Uninitialized CameraController.',
+            'takePicture was called on uninitialized CameraController',
+          )));
+    });
+
+    test('takePicture() throws $CameraException when takePicture is true',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      cameraController.value =
+          cameraController.value.copyWith(isTakingPicture: true);
+      expect(
+          cameraController.takePicture(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'Previous capture has not returned yet.',
+            'takePicture was called before the previous capture returned.',
+          )));
+    });
+
+    test('takePicture() returns $XFile', () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+      XFile xFile = await cameraController.takePicture();
+
+      expect(xFile.path, mockTakePicture.path);
+    });
+
+    test('takePicture() throws $CameraException on $PlatformException',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+      await cameraController.initialize();
+
+      mockPlatformException = true;
+      expect(
+          cameraController.takePicture(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'foo',
+            'bar',
+          )));
+      mockPlatformException = false;
+    });
+
+    test('startVideoRecording() throws $CameraException when uninitialized',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+
+      expect(
+          cameraController.startVideoRecording(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'Uninitialized CameraController',
+            'startVideoRecording was called on uninitialized CameraController',
+          )));
+    });
+    test('startVideoRecording() throws $CameraException when recording videos',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+
+      await cameraController.initialize();
+
+      cameraController.value =
+          cameraController.value.copyWith(isRecordingVideo: true);
+
+      expect(
+          cameraController.startVideoRecording(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'A video recording is already started.',
+            'startVideoRecording was called when a recording is already started.',
+          )));
+    });
+
+    test(
+        'startVideoRecording() throws $CameraException when already streaming images',
+        () async {
+      CameraController cameraController = CameraController(
+          CameraDescription(
+              name: 'cam',
+              lensDirection: CameraLensDirection.back,
+              sensorOrientation: 90),
+          ResolutionPreset.max);
+
+      await cameraController.initialize();
+
+      cameraController.value =
+          cameraController.value.copyWith(isStreamingImages: true);
+
+      expect(
+          cameraController.startVideoRecording(),
+          throwsA(isA<CameraException>().having(
+            (error) => error.description,
+            'A camera has started streaming images.',
+            'startVideoRecording was called while a camera was streaming images.',
+          )));
+    });
+  });
+}
+
+class MockCameraPlatform extends Mock
+    with MockPlatformInterfaceMixin
+    implements CameraPlatform {
+  @override
+  Future<List<CameraDescription>> availableCameras() =>
+      Future.value(mockAvailableCameras);
+
+  @override
+  Future<int> createCamera(
+    CameraDescription description,
+    ResolutionPreset resolutionPreset, {
+    bool enableAudio,
+  }) =>
+      mockPlatformException
+          ? throw PlatformException(code: 'foo', message: 'bar')
+          : Future.value(mockInitializeCamera);
+
+  @override
+  Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
+      Stream.value(mockOnCameraInitializedEvent);
+
+  @override
+  Stream<CameraClosingEvent> onCameraClosing(int cameraId) =>
+      Stream.value(mockOnCameraClosingEvent);
+
+  @override
+  Stream<CameraErrorEvent> onCameraError(int cameraId) =>
+      Stream.value(mockOnCameraErrorEvent);
+
+  @override
+  Future<XFile> takePicture(int cameraId) => mockPlatformException
+      ? throw PlatformException(code: 'foo', message: 'bar')
+      : Future.value(mockTakePicture);
+
+  @override
+  Future<XFile> startVideoRecording(int cameraId) =>
+      Future.value(mockVideoRecordingXFile);
 }
 
 class MockCameraDescription extends CameraDescription {
diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart
new file mode 100644
index 0000000..28255eb
--- /dev/null
+++ b/packages/camera/camera/test/camera_value_test.dart
@@ -0,0 +1,102 @@
+// Copyright 2019 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 'dart:ui';
+
+import 'package:camera/camera.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  group('camera_value', () {
+    test('Can be created', () {
+      var cameraValue = const CameraValue(
+        isInitialized: false,
+        errorDescription: null,
+        previewSize: Size(10, 10),
+        isRecordingPaused: false,
+        isRecordingVideo: false,
+        isTakingPicture: false,
+        isStreamingImages: false,
+      );
+
+      expect(cameraValue, isA<CameraValue>());
+      expect(cameraValue.isInitialized, isFalse);
+      expect(cameraValue.errorDescription, null);
+      expect(cameraValue.previewSize, Size(10, 10));
+      expect(cameraValue.isRecordingPaused, isFalse);
+      expect(cameraValue.isRecordingVideo, isFalse);
+      expect(cameraValue.isTakingPicture, isFalse);
+      expect(cameraValue.isStreamingImages, isFalse);
+    });
+
+    test('Can be created as uninitialized', () {
+      var cameraValue = const CameraValue.uninitialized();
+
+      expect(cameraValue, isA<CameraValue>());
+      expect(cameraValue.isInitialized, isFalse);
+      expect(cameraValue.errorDescription, null);
+      expect(cameraValue.previewSize, null);
+      expect(cameraValue.isRecordingPaused, isFalse);
+      expect(cameraValue.isRecordingVideo, isFalse);
+      expect(cameraValue.isTakingPicture, isFalse);
+      expect(cameraValue.isStreamingImages, isFalse);
+    });
+
+    test('Can be copied with isInitialized', () {
+      var cv = const CameraValue.uninitialized();
+      var cameraValue = cv.copyWith(isInitialized: true);
+
+      expect(cameraValue, isA<CameraValue>());
+      expect(cameraValue.isInitialized, isTrue);
+      expect(cameraValue.errorDescription, null);
+      expect(cameraValue.previewSize, null);
+      expect(cameraValue.isRecordingPaused, isFalse);
+      expect(cameraValue.isRecordingVideo, isFalse);
+      expect(cameraValue.isTakingPicture, isFalse);
+      expect(cameraValue.isStreamingImages, isFalse);
+    });
+
+    test('Has aspectRatio after setting size', () {
+      var cv = const CameraValue.uninitialized();
+      var cameraValue =
+          cv.copyWith(isInitialized: true, previewSize: Size(20, 10));
+
+      expect(cameraValue.aspectRatio, 0.5);
+    });
+
+    test('hasError is true after setting errorDescription', () {
+      var cv = const CameraValue.uninitialized();
+      var cameraValue = cv.copyWith(errorDescription: 'error');
+
+      expect(cameraValue.hasError, isTrue);
+      expect(cameraValue.errorDescription, 'error');
+    });
+
+    test('Recording paused is false when not recording', () {
+      var cv = const CameraValue.uninitialized();
+      var cameraValue = cv.copyWith(
+          isInitialized: true,
+          isRecordingVideo: false,
+          isRecordingPaused: true);
+
+      expect(cameraValue.isRecordingPaused, isFalse);
+    });
+
+    test('toString() works as expected', () {
+      var cameraValue = const CameraValue(
+        isInitialized: false,
+        errorDescription: null,
+        previewSize: Size(10, 10),
+        isRecordingPaused: false,
+        isRecordingVideo: false,
+        isTakingPicture: false,
+        isStreamingImages: false,
+      );
+
+      expect(cameraValue.toString(),
+          'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false)');
+    });
+  });
+}
diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart
new file mode 100644
index 0000000..cdf393f
--- /dev/null
+++ b/packages/camera/camera/test/utils/method_channel_mock.dart
@@ -0,0 +1,38 @@
+// Copyright 2019 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 'package:flutter/services.dart';
+
+class MethodChannelMock {
+  final Duration delay;
+  final MethodChannel methodChannel;
+  final Map<String, dynamic> methods;
+  final log = <MethodCall>[];
+
+  MethodChannelMock({
+    String channelName,
+    this.delay,
+    this.methods,
+  }) : methodChannel = MethodChannel(channelName) {
+    methodChannel.setMockMethodCallHandler(_handler);
+  }
+
+  Future _handler(MethodCall methodCall) async {
+    log.add(methodCall);
+
+    if (!methods.containsKey(methodCall.method)) {
+      throw MissingPluginException('No implementation found for method '
+          '${methodCall.method} on channel ${methodChannel.name}');
+    }
+
+    return Future.delayed(delay ?? Duration.zero, () {
+      final result = methods[methodCall.method];
+      if (result is Exception) {
+        throw result;
+      }
+
+      return Future.value(result);
+    });
+  }
+}
diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
index f6a2d6e..75392ae 100644
--- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj
@@ -9,11 +9,7 @@
 /* Begin PBXBuildFile section */
 		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
 		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
-		3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
-		3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; };
-		9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
-		9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
 		97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
 		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
@@ -28,8 +24,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
-				9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -40,14 +34,12 @@
 		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
 		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
 		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
-		3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
 		7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
 		7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
 		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
-		9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
 		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
 		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
@@ -63,8 +55,6 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
-				3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
 				4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -83,9 +73,7 @@
 		9740EEB11CF90186004384FC /* Flutter */ = {
 			isa = PBXGroup;
 			children = (
-				3B80C3931E831B6300D905FE /* App.framework */,
 				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
-				9740EEBA1CF902C7004384FC /* Flutter.framework */,
 				9740EEB21CF90195004384FC /* Debug.xcconfig */,
 				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
 				9740EEB31CF90195004384FC /* Generated.xcconfig */,
@@ -230,7 +218,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
 		};
 		74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = {
 			isa = PBXShellScriptBuildPhase;
@@ -270,9 +258,12 @@
 			files = (
 			);
 			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
+				"${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle",
 			);
 			name = "[CP] Copy Pods Resources";
 			outputPaths = (
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -285,9 +276,12 @@
 			files = (
 			);
 			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+				"${PODS_ROOT}/../Flutter/Flutter.framework",
 			);
 			name = "[CP] Embed Pods Frameworks";
 			outputPaths = (
+				"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
@@ -331,7 +325,6 @@
 /* Begin XCBuildConfiguration section */
 		97C147031CF9000F007C117D /* Debug */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -388,7 +381,6 @@
 		};
 		97C147041CF9000F007C117D /* Release */ = {
 			isa = XCBuildConfiguration;
-			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;