Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ flutter:
dependencies:
flutter:
sdk: flutter
camera_platform_interface:
path: ../camera_platform_interface
camera_web:
path: ../camera_web

Expand Down
310 changes: 134 additions & 176 deletions packages/camera/camera_web/lib/src/camera_web.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import 'dart:async';
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'dart:math';

import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_web/camera_web.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';

String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId';

/// The web implementation of [CameraPlatform].
///
/// This class implements the `package:camera` functionality for the web.
Expand All @@ -18,213 +16,173 @@ class CameraPlugin extends CameraPlatform {
CameraPlatform.instance = CameraPlugin();
}

final _cameras = <int, Camera>{};
var _textureCounter = 1;

@visibleForTesting
html.Window? window;

@override
Future<void> init() async {
_disposeAllCameras();
Future<List<CameraDescription>> availableCameras() {
throw UnimplementedError('availableCameras() is not implemented.');
}

@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) {
throw UnimplementedError('createCamera() is not implemented.');
}

@override
Future<void> dispose(int textureId) async {
_cameras[textureId]!.dispose();
_cameras.remove(textureId);
Future<void> initializeCamera(
int cameraId, {
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
}) {
throw UnimplementedError('initializeCamera() is not implemented.');
}

@override
Future<int> create(CameraOptions options) async {
final textureId = _textureCounter;
_textureCounter++;
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
throw UnimplementedError('onCameraInitialized() is not implemented.');
}

@override
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
throw UnimplementedError('onCameraResolutionChanged() is not implemented.');
}

final camera = Camera(
options: options,
textureId: textureId,
window: window,
@override
Stream<CameraClosingEvent> onCameraClosing(int cameraId) {
throw UnimplementedError('onCameraClosing() is not implemented.');
}

@override
Stream<CameraErrorEvent> onCameraError(int cameraId) {
throw UnimplementedError('onCameraError() is not implemented.');
}

@override
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
throw UnimplementedError('onVideoRecordedEvent() is not implemented.');
}

@override
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
throw UnimplementedError(
'onDeviceOrientationChanged() is not implemented.',
);
await camera.initialize();
}

_cameras[textureId] = camera;
return textureId;
@override
Future<void> lockCaptureOrientation(
int cameraId,
DeviceOrientation orientation,
) {
throw UnimplementedError('lockCaptureOrientation() is not implemented.');
}

@override
Widget buildView(int textureId) {
return HtmlElementView(viewType: _getViewType(textureId));
Future<void> unlockCaptureOrientation(int cameraId) {
throw UnimplementedError('unlockCaptureOrientation() is not implemented.');
}

@override
Future<void> play(int textureId) => _cameras[textureId]!.play();
Future<XFile> takePicture(int cameraId) {
throw UnimplementedError('takePicture() is not implemented.');
}

@override
Future<void> stop(int textureId) async => _cameras[textureId]!.stop();
Future<void> prepareForVideoRecording() {
throw UnimplementedError('prepareForVideoRecording() is not implemented.');
}

@override
Future<CameraImage> takePicture(int textureId) {
return _cameras[textureId]!.takePicture();
Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) {
throw UnimplementedError('startVideoRecording() is not implemented.');
}

void _disposeAllCameras() {
for (final camera in _cameras.values) {
camera.dispose();
}
_cameras.clear();
@override
Future<XFile> stopVideoRecording(int cameraId) {
throw UnimplementedError('stopVideoRecording() is not implemented.');
}

@override
Future<List<MediaDeviceInfo>> getMediaDevices() async {
final videoDevices = <MediaDeviceInfo>[];
if (html.window.navigator.mediaDevices != null) {
final devices =
await html.window.navigator.mediaDevices?.enumerateDevices() ?? [];
for (var deviceIndex = 0; deviceIndex < devices.length; deviceIndex++) {
dynamic device = devices[deviceIndex];
if (device is html.MediaDeviceInfo && device.kind == 'videoinput') {
videoDevices.add(
MediaDeviceInfo(deviceId: device.deviceId, label: device.label));
}
}
}
return videoDevices;
Future<void> pauseVideoRecording(int cameraId) {
throw UnimplementedError('pauseVideoRecording() is not implemented.');
}

@override
Future<String?> getDefaultDeviceId() async {
String? defaultId = VideoConstraints.defaultDeviceId;
if (browserEngine != BrowserEngine.blink) {
/// For browsers other than Chrome, enumerate video devices and return
/// first one since it is the default selected for the browser.
final devices = await getMediaDevices();
if (devices.isNotEmpty) {
defaultId = devices[0].deviceId;
}
}
return defaultId;
Future<void> resumeVideoRecording(int cameraId) {
throw UnimplementedError('resumeVideoRecording() is not implemented.');
}
}

class Camera {
Camera({
required this.textureId,
this.options = const CameraOptions(),
html.Window? window,
}) : window = window ?? html.window;

late html.VideoElement videoElement;
late html.DivElement divElement;
final CameraOptions options;
final int textureId;
final html.Window window;

Future<void> initialize() async {
final isSupported = window.navigator.mediaDevices?.getUserMedia != null;
if (!isSupported) {
throw const CameraNotSupportedException();
}

videoElement = html.VideoElement()..applyDefaultStyles();
divElement = html.DivElement()
..style.setProperty('object-fit', 'cover')
..append(videoElement);
// ignore: avoid_dynamic_calls
ui.platformViewRegistry.registerViewFactory(
_getViewType(textureId),
(_) => divElement,
);
@override
Future<void> setFlashMode(int cameraId, FlashMode mode) {
throw UnimplementedError('setFlashMode() is not implemented.');
}

final stream = await _getMediaStream();
videoElement
..autoplay = false
..muted = !options.audio.enabled
..srcObject = stream
..setAttribute('playsinline', '');
}

Future<html.MediaStream> _getMediaStream() async {
try {
final constraints = await options.toJson();
return await window.navigator.mediaDevices!.getUserMedia(
constraints,
);
} on html.DomException catch (e) {
switch (e.name) {
case 'NotFoundError':
case 'DevicesNotFoundError':
throw const CameraNotFoundException();
case 'NotReadableError':
case 'TrackStartError':
throw const CameraNotReadableException();
case 'OverconstrainedError':
case 'ConstraintNotSatisfiedError':
throw const CameraOverconstrainedException();
case 'NotAllowedError':
case 'PermissionDeniedError':
throw const CameraNotAllowedException();
case 'TypeError':
throw const CameraTypeException();
default:
throw const CameraUnknownException();
}
} catch (_) {
throw const CameraUnknownException();
}
}

Future<void> play() async {
if (videoElement.srcObject == null) {
final stream = await _getMediaStream();
videoElement.srcObject = stream;
}
await videoElement.play();
}

void stop() {
final tracks = videoElement.srcObject?.getVideoTracks();
if (tracks != null) {
for (final track in tracks) {
track.stop();
}
}
videoElement.srcObject = null;
}

void dispose() {
stop();
videoElement
..srcObject = null
..load();
}

Future<CameraImage> takePicture() async {
final videoWidth = videoElement.videoWidth;
final videoHeight = videoElement.videoHeight;
final canvas = html.CanvasElement(width: videoWidth, height: videoHeight);
canvas.context2D
..translate(videoWidth, 0)
..scale(-1, 1)
..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight);
final blob = await canvas.toBlob();
return CameraImage(
data: html.Url.createObjectUrl(blob),
width: videoWidth,
height: videoHeight,
);
@override
Future<void> setExposureMode(int cameraId, ExposureMode mode) {
throw UnimplementedError('setExposureMode() is not implemented.');
}
}

extension on html.VideoElement {
void applyDefaultStyles() {
style
..removeProperty('transform-origin')
..setProperty('pointer-events', 'none')
..setProperty('width', '100%')
..setProperty('height', '100%')
..setProperty('transform', 'scaleX(-1)')
..setProperty('object-fit', 'cover')
..setProperty('-webkit-transform', 'scaleX(-1)')
..setProperty('-moz-transform', 'scaleX(-1)');
@override
Future<void> setExposurePoint(int cameraId, Point<double>? point) {
throw UnimplementedError('setExposurePoint() is not implemented.');
}

@override
Future<double> getMinExposureOffset(int cameraId) {
throw UnimplementedError('getMinExposureOffset() is not implemented.');
}

@override
Future<double> getMaxExposureOffset(int cameraId) {
throw UnimplementedError('getMaxExposureOffset() is not implemented.');
}

@override
Future<double> getExposureOffsetStepSize(int cameraId) {
throw UnimplementedError('getExposureOffsetStepSize() is not implemented.');
}

@override
Future<double> setExposureOffset(int cameraId, double offset) {
throw UnimplementedError('setExposureOffset() is not implemented.');
}

@override
Future<void> setFocusMode(int cameraId, FocusMode mode) {
throw UnimplementedError('setFocusMode() is not implemented.');
}

@override
Future<void> setFocusPoint(int cameraId, Point<double>? point) {
throw UnimplementedError('setFocusPoint() is not implemented.');
}

@override
Future<double> getMaxZoomLevel(int cameraId) {
throw UnimplementedError('getMaxZoomLevel() is not implemented.');
}

@override
Future<double> getMinZoomLevel(int cameraId) {
throw UnimplementedError('getMinZoomLevel() is not implemented.');
}

@override
Future<void> setZoomLevel(int cameraId, double zoom) {
throw UnimplementedError('setZoomLevel() is not implemented.');
}

@override
Widget buildPreview(int cameraId) {
throw UnimplementedError('buildPreview() is not implemented.');
}

@override
Future<void> dispose(int cameraId) {
throw UnimplementedError('dispose() is not implemented.');
}
}
3 changes: 1 addition & 2 deletions packages/camera/camera_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ dependencies:
sdk: flutter
flutter_web_plugins:
sdk: flutter
camera_platform_interface:
path: ../camera_platform_interface
camera_platform_interface: ^2.0.1

dev_dependencies:
flutter_test:
Expand Down
Loading