[camera] Add web support (#4240)

* feat: add web to the example app
* docs: update README and point users to camera_web for more web-specific info.
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index 0e5385d..62b5f1f 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.4
+
+* Add web support by endorsing `package:camera_web`.
+
 ## 0.9.3+1
 
 * Remove iOS 9 availability check around ultra high capture sessions.
diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md
index aa34273..24566e7 100644
--- a/packages/camera/camera/README.md
+++ b/packages/camera/camera/README.md
@@ -2,7 +2,7 @@
 
 [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera)
 
-A Flutter plugin for iOS and Android allowing access to the device cameras.
+A Flutter plugin for iOS, Android and Web allowing access to the device cameras.
 
 *Note*: This plugin is still under development, and some APIs might not be available yet. We are working on a refactor which can be followed here: [issue](https://github.com/flutter/flutter/issues/31225)
 
@@ -47,6 +47,11 @@
 
 It's important to note that the `MediaRecorder` class is not working properly on emulators, as stated in the documentation: https://developer.android.com/reference/android/media/MediaRecorder. Specifically, when recording a video with sound enabled and trying to play it back, the duration won't be correct and you will only see the first frame.
 
+### Web integration
+
+For web integration details, see the
+[`camera_web` package](https://pub.dev/packages/camera_web).
+
 ### Handling Lifecycle states
 
 As of version [0.5.0](https://github.com/flutter/plugins/blob/master/packages/camera/CHANGELOG.md#050) of the camera plugin, lifecycle changes are no longer handled by the plugin. This means developers are now responsible to control camera resources when the lifecycle state is updated. Failure to do so might lead to unexpected behavior (for example as described in issue [#39109](https://github.com/flutter/flutter/issues/39109)). Handling lifecycle changes can be done by overriding the `didChangeAppLifecycleState` method like so:
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
index c0e90ee..a3a5d1d 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -8,6 +8,7 @@
 import 'dart:io';
 
 import 'package:camera/camera.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:video_player/video_player.dart';
 
@@ -231,7 +232,14 @@
                 ? Container()
                 : SizedBox(
                     child: (localVideoController == null)
-                        ? Image.file(File(imageFile!.path))
+                        ? (
+                            // The captured image on the web contains a network-accessible URL
+                            // pointing to a location within the browser. It may be displayed
+                            // either with Image.network or Image.memory after loading the image
+                            // bytes to memory.
+                            kIsWeb
+                                ? Image.network(imageFile!.path)
+                                : Image.file(File(imageFile!.path)))
                         : Container(
                             child: Center(
                               child: AspectRatio(
@@ -267,17 +275,24 @@
               color: Colors.blue,
               onPressed: controller != null ? onFlashModeButtonPressed : null,
             ),
-            IconButton(
-              icon: Icon(Icons.exposure),
-              color: Colors.blue,
-              onPressed:
-                  controller != null ? onExposureModeButtonPressed : null,
-            ),
-            IconButton(
-              icon: Icon(Icons.filter_center_focus),
-              color: Colors.blue,
-              onPressed: controller != null ? onFocusModeButtonPressed : null,
-            ),
+            // The exposure and focus mode are currently not supported on the web.
+            ...(!kIsWeb
+                ? [
+                    IconButton(
+                      icon: Icon(Icons.exposure),
+                      color: Colors.blue,
+                      onPressed: controller != null
+                          ? onExposureModeButtonPressed
+                          : null,
+                    ),
+                    IconButton(
+                      icon: Icon(Icons.filter_center_focus),
+                      color: Colors.blue,
+                      onPressed:
+                          controller != null ? onFocusModeButtonPressed : null,
+                    )
+                  ]
+                : []),
             IconButton(
               icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute),
               color: Colors.blue,
@@ -616,7 +631,7 @@
 
     final CameraController cameraController = CameraController(
       cameraDescription,
-      ResolutionPreset.medium,
+      kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
       enableAudio: enableAudio,
       imageFormatGroup: ImageFormatGroup.jpeg,
     );
@@ -635,12 +650,17 @@
     try {
       await cameraController.initialize();
       await Future.wait([
-        cameraController
-            .getMinExposureOffset()
-            .then((value) => _minAvailableExposureOffset = value),
-        cameraController
-            .getMaxExposureOffset()
-            .then((value) => _maxAvailableExposureOffset = value),
+        // The exposure mode is currently not supported on the web.
+        ...(!kIsWeb
+            ? [
+                cameraController
+                    .getMinExposureOffset()
+                    .then((value) => _minAvailableExposureOffset = value),
+                cameraController
+                    .getMaxExposureOffset()
+                    .then((value) => _maxAvailableExposureOffset = value)
+              ]
+            : []),
         cameraController
             .getMaxZoomLevel()
             .then((value) => _maxAvailableZoom = value),
@@ -708,16 +728,20 @@
   }
 
   void onCaptureOrientationLockButtonPressed() async {
-    if (controller != null) {
-      final CameraController cameraController = controller!;
-      if (cameraController.value.isCaptureOrientationLocked) {
-        await cameraController.unlockCaptureOrientation();
-        showInSnackBar('Capture orientation unlocked');
-      } else {
-        await cameraController.lockCaptureOrientation();
-        showInSnackBar(
-            'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
+    try {
+      if (controller != null) {
+        final CameraController cameraController = controller!;
+        if (cameraController.value.isCaptureOrientationLocked) {
+          await cameraController.unlockCaptureOrientation();
+          showInSnackBar('Capture orientation unlocked');
+        } else {
+          await cameraController.lockCaptureOrientation();
+          showInSnackBar(
+              'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
+        }
       }
+    } on CameraException catch (e) {
+      _showCameraException(e);
     }
   }
 
@@ -916,8 +940,10 @@
       return;
     }
 
-    final VideoPlayerController vController =
-        VideoPlayerController.file(File(videoFile!.path));
+    final VideoPlayerController vController = kIsWeb
+        ? VideoPlayerController.network(videoFile!.path)
+        : VideoPlayerController.file(File(videoFile!.path));
+
     videoPlayerListener = () {
       if (videoController != null && videoController!.value.size != null) {
         // Refreshing the state to update video player with the correct ratio.
diff --git a/packages/camera/camera/example/web/favicon.png b/packages/camera/camera/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/camera/camera/example/web/favicon.png
Binary files differ
diff --git a/packages/camera/camera/example/web/icons/Icon-192.png b/packages/camera/camera/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/camera/camera/example/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/camera/camera/example/web/icons/Icon-512.png b/packages/camera/camera/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/camera/camera/example/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/camera/camera/example/web/index.html b/packages/camera/camera/example/web/index.html
new file mode 100644
index 0000000..2a3117d
--- /dev/null
+++ b/packages/camera/camera/example/web/index.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html>
+
+<head>
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="An example of the camera on the web.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="example">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="shortcut icon" type="image/png" href="favicon.png" />
+
+  <title>Camera Web Example</title>
+  <link rel="manifest" href="manifest.json">
+</head>
+
+<body>
+  <!-- This script installs service_worker.js to provide PWA functionality to
+       application. For more information, see:
+       https://developers.google.com/web/fundamentals/primers/service-workers -->
+  <script>
+    if ('serviceWorker' in navigator) {
+      window.addEventListener('load', function () {
+        navigator.serviceWorker.register('flutter_service_worker.js');
+      });
+    }
+  </script>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/packages/camera/camera/example/web/manifest.json b/packages/camera/camera/example/web/manifest.json
new file mode 100644
index 0000000..5fe0e04
--- /dev/null
+++ b/packages/camera/camera/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+    "name": "camera example",
+    "short_name": "camera",
+    "start_url": ".",
+    "display": "standalone",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "An example of the camera on the web.",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "icons/Icon-192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ]
+}
diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart
index 6a15896..5faa69f 100644
--- a/packages/camera/camera/lib/src/camera_preview.dart
+++ b/packages/camera/camera/lib/src/camera_preview.dart
@@ -43,7 +43,7 @@
   }
 
   Widget _wrapInRotatedBox({required Widget child}) {
-    if (defaultTargetPlatform != TargetPlatform.android) {
+    if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
       return child;
     }
 
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 7efc793..b8894d5 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -1,10 +1,10 @@
 name: camera
 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,
+  camera on Android, iOS and Web. Supports previewing the camera feed, capturing images, capturing video,
   and streaming image buffers to dart.
 repository: https://github.com/flutter/plugins/tree/master/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.3+1
+version: 0.9.4
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
@@ -18,9 +18,12 @@
         pluginClass: CameraPlugin
       ios:
         pluginClass: CameraPlugin
+      web:
+        default_package: camera_web
 
 dependencies:
   camera_platform_interface: ^2.1.0
+  camera_web: ^0.2.1
   flutter:
     sdk: flutter
   pedantic: ^1.10.0
diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart
index 14afdda..32718f4 100644
--- a/packages/camera/camera/test/camera_preview_test.dart
+++ b/packages/camera/camera/test/camera_preview_test.dart
@@ -221,7 +221,7 @@
 
       debugDefaultTargetPlatformOverride = null;
     });
-  });
+  }, skip: kIsWeb);
 
   testWidgets('when not on Android there should not be a rotated box',
       (WidgetTester tester) async {