Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Add web support #4240

Merged
merged 25 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6925d10
feat: add web to the example app
bselwe Jul 23, 2021
f318101
docs: update camera README
bselwe Aug 13, 2021
52b8608
docs: update camera platform interface comments
bselwe Aug 13, 2021
85dab25
feat: update example to support web
bselwe Aug 13, 2021
2c58b59
build: update camera pubspec
bselwe Aug 13, 2021
f203e81
docs: add copyright to camera example index.html
bselwe Aug 13, 2021
410e853
[camera_web] Add support for pausing and resuming the camera preview …
bselwe Aug 13, 2021
17b7b2d
Unendorse camera_web for now, but use it in the example app.
ditman Aug 25, 2021
d03e585
Merge branch 'master' into feat/camera-web-example
ditman Aug 25, 2021
277407a
feat: move previous camera controller dispose to finally in case of a…
bselwe Aug 24, 2021
7c1be26
feat: do not rotate camera preview on the web
bselwe Aug 27, 2021
c374b00
Merge branch 'upstream/master' into feat/camera-web-example
bselwe Aug 27, 2021
69fa8d2
feat: use max camera resolution on the web
bselwe Aug 27, 2021
ad50574
docs: update resolution preset comments
bselwe Aug 27, 2021
5983616
Merge branch 'upstream/master' into feat/camera-web-example
bselwe Aug 30, 2021
0254106
revert: #4236
bselwe Aug 30, 2021
75330ed
Merge branch 'upstream/master' into feat/camera-web-example
bselwe Sep 14, 2021
52fca85
feat: use network VideoPlayerController for the camera recordings
bselwe Sep 14, 2021
6a051d9
docs: update readme
bselwe Sep 14, 2021
e08cab9
Merge branch 'flutter:master' into feat/camera-web-example
ditman Sep 17, 2021
4c169f4
Merge branch 'flutter:master' into feat/camera-web-example
ditman Sep 18, 2021
6a146af
Reenable video operations
ditman Sep 18, 2021
d8878c4
Use endorsed version of camera_web
ditman Sep 18, 2021
3f8a625
Endorse camera_web package.
ditman Sep 18, 2021
798443e
Skip android-only tests on web.
ditman Sep 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/camera/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -45,6 +45,11 @@ minSdkVersion 21

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:
Expand Down
147 changes: 88 additions & 59 deletions packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:async';
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';

Expand Down Expand Up @@ -231,7 +232,14 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
? 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(
Expand Down Expand Up @@ -267,17 +275,24 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
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,
Expand Down Expand Up @@ -499,38 +514,43 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
? onTakePictureButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.videocam),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
!cameraController.value.isRecordingVideo
? onVideoRecordButtonPressed
: null,
),
IconButton(
icon: cameraController != null &&
cameraController.value.isRecordingPaused
? Icon(Icons.play_arrow)
: Icon(Icons.pause),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
cameraController.value.isRecordingVideo
? (cameraController.value.isRecordingPaused)
? onResumeButtonPressed
: onPauseButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.stop),
color: Colors.red,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
cameraController.value.isRecordingVideo
? onStopButtonPressed
: null,
)
// Video recording is currently not supported on the web.
...(!kIsWeb
? [
IconButton(
icon: const Icon(Icons.videocam),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
!cameraController.value.isRecordingVideo
? onVideoRecordButtonPressed
: null,
),
IconButton(
icon: cameraController != null &&
cameraController.value.isRecordingPaused
? Icon(Icons.play_arrow)
: Icon(Icons.pause),
color: Colors.blue,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
cameraController.value.isRecordingVideo
? (cameraController.value.isRecordingPaused)
? onResumeButtonPressed
: onPauseButtonPressed
: null,
),
IconButton(
icon: const Icon(Icons.stop),
color: Colors.red,
onPressed: cameraController != null &&
cameraController.value.isInitialized &&
cameraController.value.isRecordingVideo
? onStopButtonPressed
: null,
)
]
: []),
],
);
}
Expand Down Expand Up @@ -621,12 +641,17 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
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),
Expand Down Expand Up @@ -696,16 +721,20 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
}

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);
}
}

Expand Down
Binary file added packages/camera/camera/example/web/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions packages/camera/camera/example/web/index.html
Original file line number Diff line number Diff line change
@@ -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>
23 changes: 23 additions & 0 deletions packages/camera/camera/example/web/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
5 changes: 4 additions & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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
Expand All @@ -18,9 +18,12 @@ flutter:
pluginClass: CameraPlugin
ios:
pluginClass: CameraPlugin
web:
default_package: camera_web

dependencies:
camera_platform_interface: ^2.0.0
camera_web: ^0.0.1
ditman marked this conversation as resolved.
Show resolved Hide resolved
flutter:
sdk: flutter
pedantic: ^1.10.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,21 @@ abstract class CameraPlatform extends PlatformInterface {
/// [imageFormatGroup] is used to specify the image formatting used.
/// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream.
/// On iOS this defaults to kCVPixelFormatType_32BGRA.
/// On Web this parameter is currently not supported.
Future<void> initializeCamera(
int cameraId, {
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
}) {
throw UnimplementedError('initializeCamera() is not implemented.');
}

/// The camera has been initialized
/// The camera has been initialized.
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
throw UnimplementedError('onCameraInitialized() is not implemented.');
}

/// The camera's resolution has changed
/// The camera's resolution has changed.
/// On Web this returns an empty stream.
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
throw UnimplementedError('onResolutionChanged() is not implemented.');
}
Expand All @@ -91,7 +93,7 @@ abstract class CameraPlatform extends PlatformInterface {
throw UnimplementedError('onCameraError() is not implemented.');
}

/// The camera finished recording a video
/// The camera finished recording a video.
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
throw UnimplementedError('onCameraTimeLimitReached() is not implemented.');
}
Expand Down Expand Up @@ -154,6 +156,7 @@ abstract class CameraPlatform extends PlatformInterface {
}

/// Sets the flash mode for the selected camera.
/// On Web [FlashMode.auto] corresponds to [FlashMode.always].
Future<void> setFlashMode(int cameraId, FlashMode mode) {
throw UnimplementedError('setFlashMode() is not implemented.');
}
Expand Down Expand Up @@ -228,8 +231,8 @@ abstract class CameraPlatform extends PlatformInterface {

/// Set the zoom level for the selected camera.
///
/// The supplied [zoom] value should be between 1.0 and the maximum supported
/// zoom level returned by the `getMaxZoomLevel`. Throws a `CameraException`
/// The supplied [zoom] value should be between the minimum and the maximum supported
/// zoom level returned by `getMinZoomLevel` and `getMaxZoomLevel`. Throws a `CameraException`
/// when an illegal zoom level is supplied.
Future<void> setZoomLevel(int cameraId, double zoom) {
throw UnimplementedError('setZoomLevel() is not implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
///
/// 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
/// 352x288 on iOS, 240p (320x240) on Android and Web
low,

/// 480p (640x480 on iOS, 720x480 on Android)
/// 480p (640x480 on iOS, 720x480 on Android and Web)
medium,

/// 720p (1280x720)
Expand Down