[camera] Switch to platform-interface-provided streaming (#5833)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index bd42ef4..72af38a 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.7+1
+
+* Moves streaming implementation to the platform interface package.
+
 ## 0.9.7
 
 * Returns all the available cameras on iOS.
diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart
index 5014795..6566e2a 100644
--- a/packages/camera/camera/lib/src/camera_controller.dart
+++ b/packages/camera/camera/lib/src/camera_controller.dart
@@ -12,8 +12,6 @@
 import 'package:flutter/services.dart';
 import 'package:quiver/core.dart';
 
-const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera');
-
 /// Signature for a callback receiving the a camera image.
 ///
 /// This is used by [CameraController.startImageStream].
@@ -257,7 +255,7 @@
   int _cameraId = kUninitializedCameraId;
 
   bool _isDisposed = false;
-  StreamSubscription<dynamic>? _imageStreamSubscription;
+  StreamSubscription<CameraImageData>? _imageStreamSubscription;
   FutureOr<bool>? _initCalled;
   StreamSubscription<DeviceOrientationChangedEvent>?
       _deviceOrientationSubscription;
@@ -438,27 +436,15 @@
     }
 
     try {
-      await _channel.invokeMethod<void>('startImageStream');
+      _imageStreamSubscription = CameraPlatform.instance
+          .onStreamedFrameAvailable(_cameraId)
+          .listen((CameraImageData imageData) {
+        onAvailable(CameraImage.fromPlatformInterface(imageData));
+      });
       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) {
-        if (defaultTargetPlatform == TargetPlatform.iOS) {
-          try {
-            _channel.invokeMethod<void>('receivedImageStreamData');
-          } on PlatformException catch (e) {
-            throw CameraException(e.code, e.message);
-          }
-        }
-        onAvailable(
-            CameraImage.fromPlatformData(imageData as Map<dynamic, dynamic>));
-      },
-    );
   }
 
   /// Stop streaming images from platform camera.
@@ -487,13 +473,11 @@
 
     try {
       value = value.copyWith(isStreamingImages: false);
-      await _channel.invokeMethod<void>('stopImageStream');
+      await _imageStreamSubscription?.cancel();
+      _imageStreamSubscription = null;
     } on PlatformException catch (e) {
       throw CameraException(e.code, e.message);
     }
-
-    await _imageStreamSubscription?.cancel();
-    _imageStreamSubscription = null;
   }
 
   /// Start a video recording.
diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart
index 0f2377e..cb3d306 100644
--- a/packages/camera/camera/lib/src/camera_image.dart
+++ b/packages/camera/camera/lib/src/camera_image.dart
@@ -7,11 +7,24 @@
 import 'package:camera_platform_interface/camera_platform_interface.dart';
 import 'package:flutter/foundation.dart';
 
+// TODO(stuartmorgan): Remove all of these classes in a breaking change, and
+// vend the platform interface versions directly. See
+// https://github.com/flutter/flutter/issues/104188
+
 /// A single color plane of image data.
 ///
 /// The number and meaning of the planes in an image are determined by the
 /// format of the Image.
 class Plane {
+  Plane._fromPlatformInterface(CameraImagePlane plane)
+      : bytes = plane.bytes,
+        bytesPerPixel = plane.bytesPerPixel,
+        bytesPerRow = plane.bytesPerRow,
+        height = plane.height,
+        width = plane.width;
+
+  // Only used by the deprecated codepath that's kept to avoid breaking changes.
+  // Never called by the plugin itself.
   Plane._fromPlatformData(Map<dynamic, dynamic> data)
       : bytes = data['bytes'] as Uint8List,
         bytesPerPixel = data['bytesPerPixel'] as int?,
@@ -43,6 +56,12 @@
 
 /// Describes how pixels are represented in an image.
 class ImageFormat {
+  ImageFormat._fromPlatformInterface(CameraImageFormat format)
+      : group = format.group,
+        raw = format.raw;
+
+  // Only used by the deprecated codepath that's kept to avoid breaking changes.
+  // Never called by the plugin itself.
   ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw);
 
   /// Describes the format group the raw image format falls into.
@@ -58,6 +77,8 @@
   final dynamic raw;
 }
 
+// Only used by the deprecated codepath that's kept to avoid breaking changes.
+// Never called by the plugin itself.
 ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) {
   if (defaultTargetPlatform == TargetPlatform.android) {
     switch (rawFormat) {
@@ -94,7 +115,19 @@
 /// Although not all image formats are planar on iOS, we treat 1-dimensional
 /// images as single planar images.
 class CameraImage {
-  /// CameraImage Constructor
+  /// Creates a [CameraImage] from the platform interface version.
+  CameraImage.fromPlatformInterface(CameraImageData data)
+      : format = ImageFormat._fromPlatformInterface(data.format),
+        height = data.height,
+        width = data.width,
+        planes = List<Plane>.unmodifiable(data.planes.map<Plane>(
+            (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))),
+        lensAperture = data.lensAperture,
+        sensorExposureTime = data.sensorExposureTime,
+        sensorSensitivity = data.sensorSensitivity;
+
+  /// Creates a [CameraImage] from method channel data.
+  @Deprecated('Use fromPlatformInterface instead')
   CameraImage.fromPlatformData(Map<dynamic, dynamic> data)
       : format = ImageFormat._fromPlatformData(data['format']),
         height = data['height'] as int,
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index ea9f2e0..d1f70d9 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
   Dart.
 repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
-version: 0.9.7
+version: 0.9.7+1
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
@@ -22,7 +22,7 @@
         default_package: camera_web
 
 dependencies:
-  camera_platform_interface: ^2.1.0
+  camera_platform_interface: ^2.2.0
   camera_web: ^0.2.1
   flutter:
     sdk: flutter
diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart
index 7055b22..a9320e4 100644
--- a/packages/camera/camera/test/camera_image_stream_test.dart
+++ b/packages/camera/camera/test/camera_image_stream_test.dart
@@ -2,18 +2,21 @@
 // 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_test/flutter_test.dart';
 
 import 'camera_test.dart';
-import 'utils/method_channel_mock.dart';
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
+  late MockStreamingCameraPlatform mockPlatform;
 
   setUp(() {
-    CameraPlatform.instance = MockCameraPlatform();
+    mockPlatform = MockStreamingCameraPlatform();
+    CameraPlatform.instance = mockPlatform;
   });
 
   test('startImageStream() throws $CameraException when uninitialized', () {
@@ -87,13 +90,6 @@
   });
 
   test('startImageStream() calls CameraPlatform', () async {
-    final MethodChannelMock cameraChannelMock = MethodChannelMock(
-        channelName: 'plugins.flutter.io/camera',
-        methods: <String, dynamic>{'startImageStream': <String, dynamic>{}});
-    final MethodChannelMock streamChannelMock = MethodChannelMock(
-        channelName: 'plugins.flutter.io/camera/imageStream',
-        methods: <String, dynamic>{'listen': <String, dynamic>{}});
-
     final CameraController cameraController = CameraController(
         const CameraDescription(
             name: 'cam',
@@ -104,10 +100,8 @@
 
     await cameraController.startImageStream((CameraImage image) => null);
 
-    expect(cameraChannelMock.log,
-        <Matcher>[isMethodCall('startImageStream', arguments: null)]);
-    expect(streamChannelMock.log,
-        <Matcher>[isMethodCall('listen', arguments: null)]);
+    expect(mockPlatform.streamCallLog,
+        <String>['onStreamedFrameAvailable', 'listen']);
   });
 
   test('stopImageStream() throws $CameraException when uninitialized', () {
@@ -178,19 +172,6 @@
   });
 
   test('stopImageStream() intended behaviour', () async {
-    final MethodChannelMock cameraChannelMock = MethodChannelMock(
-        channelName: 'plugins.flutter.io/camera',
-        methods: <String, dynamic>{
-          'startImageStream': <String, dynamic>{},
-          'stopImageStream': <String, dynamic>{}
-        });
-    final MethodChannelMock streamChannelMock = MethodChannelMock(
-        channelName: 'plugins.flutter.io/camera/imageStream',
-        methods: <String, dynamic>{
-          'listen': <String, dynamic>{},
-          'cancel': <String, dynamic>{}
-        });
-
     final CameraController cameraController = CameraController(
         const CameraDescription(
             name: 'cam',
@@ -201,14 +182,33 @@
     await cameraController.startImageStream((CameraImage 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)
-    ]);
+    expect(mockPlatform.streamCallLog,
+        <String>['onStreamedFrameAvailable', 'listen', 'cancel']);
   });
 }
+
+class MockStreamingCameraPlatform extends MockCameraPlatform {
+  List<String> streamCallLog = <String>[];
+
+  StreamController<CameraImageData>? _streamController;
+
+  @override
+  Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
+      {CameraImageStreamOptions? options}) {
+    streamCallLog.add('onStreamedFrameAvailable');
+    _streamController = StreamController<CameraImageData>(
+      onListen: _onFrameStreamListen,
+      onCancel: _onFrameStreamCancel,
+    );
+    return _streamController!.stream;
+  }
+
+  void _onFrameStreamListen() {
+    streamCallLog.add('listen');
+  }
+
+  FutureOr<void> _onFrameStreamCancel() async {
+    streamCallLog.add('cancel');
+    _streamController = null;
+  }
+}
diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart
index 55bf4a2..c964e7a 100644
--- a/packages/camera/camera/test/camera_image_test.dart
+++ b/packages/camera/camera/test/camera_image_test.dart
@@ -5,11 +5,64 @@
 import 'dart:typed_data';
 
 import 'package:camera/camera.dart';
+import 'package:camera_platform_interface/camera_platform_interface.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter_test/flutter_test.dart';
 
 void main() {
-  group('$CameraImage tests', () {
+  test('translates correctly from platform interface classes', () {
+    final CameraImageData originalImage = CameraImageData(
+      format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 1234),
+      planes: <CameraImagePlane>[
+        CameraImagePlane(
+          bytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
+          bytesPerRow: 20,
+          bytesPerPixel: 3,
+          width: 200,
+          height: 100,
+        ),
+        CameraImagePlane(
+          bytes: Uint8List.fromList(<int>[5, 6, 7, 8]),
+          bytesPerRow: 18,
+          bytesPerPixel: 4,
+          width: 220,
+          height: 110,
+        ),
+      ],
+      width: 640,
+      height: 480,
+      lensAperture: 2.5,
+      sensorExposureTime: 5,
+      sensorSensitivity: 1.3,
+    );
+
+    final CameraImage image = CameraImage.fromPlatformInterface(originalImage);
+    // Simple values.
+    expect(image.width, 640);
+    expect(image.height, 480);
+    expect(image.lensAperture, 2.5);
+    expect(image.sensorExposureTime, 5);
+    expect(image.sensorSensitivity, 1.3);
+    // Format.
+    expect(image.format.group, ImageFormatGroup.jpeg);
+    expect(image.format.raw, 1234);
+    // Planes.
+    expect(image.planes.length, originalImage.planes.length);
+    for (int i = 0; i < image.planes.length; i++) {
+      expect(
+          image.planes[i].bytes.length, originalImage.planes[i].bytes.length);
+      for (int j = 0; j < image.planes[i].bytes.length; j++) {
+        expect(image.planes[i].bytes[j], originalImage.planes[i].bytes[j]);
+      }
+      expect(
+          image.planes[i].bytesPerPixel, originalImage.planes[i].bytesPerPixel);
+      expect(image.planes[i].bytesPerRow, originalImage.planes[i].bytesPerRow);
+      expect(image.planes[i].width, originalImage.planes[i].width);
+      expect(image.planes[i].height, originalImage.planes[i].height);
+    }
+  });
+
+  group('legacy constructors', () {
     test('$CameraImage can be created', () {
       debugDefaultTargetPlatformOverride = TargetPlatform.android;
       final CameraImage cameraImage =
diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart
deleted file mode 100644
index 7c8b4ca..0000000
--- a/packages/camera/camera/test/utils/method_channel_mock.dart
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2013 The Flutter 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';
-import 'package:flutter_test/flutter_test.dart';
-
-class MethodChannelMock {
-  MethodChannelMock({
-    required String channelName,
-    this.delay,
-    required this.methods,
-  }) : methodChannel = MethodChannel(channelName) {
-    methodChannel.setMockMethodCallHandler(_handler);
-  }
-
-  final Duration? delay;
-  final MethodChannel methodChannel;
-  final Map<String, dynamic> methods;
-  final List<MethodCall> log = <MethodCall>[];
-
-  Future<dynamic> _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<dynamic>.delayed(delay ?? Duration.zero, () {
-      final Object? result = methods[methodCall.method];
-      if (result is Exception) {
-        throw result;
-      }
-
-      return Future<dynamic>.value(result);
-    });
-  }
-}