p-mazhnik

refactor: abstract web scanner

... ... @@ -2,13 +2,11 @@ import 'dart:async';
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/web/base.dart';
import 'package:mobile_scanner/src/web/jsqr.dart';
import 'package:mobile_scanner/src/web/media.dart';
/// This plugin is the web implementation of mobile_scanner.
/// It only supports QR codes.
... ... @@ -39,7 +37,10 @@ class MobileScannerWebPlugin {
// Determine wether device has flas
bool hasFlash = false;
final WebBarcodeReaderBase _barCodeReader = JsQrCodeReader();
final html.DivElement vidDiv = html.DivElement();
late final WebBarcodeReaderBase _barCodeReader =
JsQrCodeReader(videoContainer: vidDiv);
StreamSubscription? _barCodeStreamSubscription;
/// Handle incomming messages
... ... @@ -80,6 +81,17 @@ class MobileScannerWebPlugin {
cameraFacing = CameraFacing.values[arguments['facing'] as int];
}
// See https://github.com/flutter/flutter/issues/41563
// ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls
ui.platformViewRegistry.registerViewFactory(
viewID,
(int id) {
return vidDiv
..style.width = '100%'
..style.height = '100%';
},
);
// Check if stream is running
if (_barCodeReader.isStarted) {
return {
... ... @@ -90,7 +102,6 @@ class MobileScannerWebPlugin {
}
try {
await _barCodeReader.start(
viewID: viewID,
cameraFacing: cameraFacing,
);
... ...
import 'dart:html';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/web/media.dart';
abstract class WebBarcodeReaderBase {
/// Timer used to capture frames to be analyzed
final frameInterval = const Duration(milliseconds: 200);
final Duration frameInterval;
final DivElement videoContainer;
const WebBarcodeReaderBase({
required this.videoContainer,
this.frameInterval = const Duration(milliseconds: 200),
});
bool get isStarted;
... ... @@ -11,7 +21,6 @@ abstract class WebBarcodeReaderBase {
/// Starts streaming video
Future<void> start({
required String viewID,
required CameraFacing cameraFacing,
});
... ... @@ -21,3 +30,59 @@ abstract class WebBarcodeReaderBase {
/// Stops streaming video
Future<void> stop();
}
mixin InternalStreamCreation on WebBarcodeReaderBase {
/// The video stream.
/// Will be initialized later to see which camera needs to be used.
MediaStream? localMediaStream;
final VideoElement video = VideoElement();
@override
int get videoWidth => video.videoWidth;
@override
int get videoHeight => video.videoHeight;
Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
// Check if browser supports multiple camera's and set if supported
final Map? capabilities =
window.navigator.mediaDevices?.getSupportedConstraints();
final Map<String, dynamic> constraints;
if (capabilities != null && capabilities['facingMode'] as bool) {
constraints = {
'video': VideoOptions(
facingMode:
cameraFacing == CameraFacing.front ? 'user' : 'environment',
)
};
} else {
constraints = {'video': true};
}
final stream =
await window.navigator.mediaDevices?.getUserMedia(constraints);
return stream;
}
void prepareVideoElement(VideoElement videoSource);
Future<void> attachStreamToVideo(
MediaStream stream,
VideoElement videoSource,
);
@override
Future<void> stop() async {
try {
// Stop the camera stream
localMediaStream?.getTracks().forEach((track) {
if (track.readyState == 'live') {
track.stop();
}
});
} catch (e) {
debugPrint('Failed to stop stream: $e');
}
video.srcObject = null;
localMediaStream = null;
videoContainer.children = [];
}
}
... ...
... ... @@ -4,15 +4,11 @@ library jsqr;
import 'dart:async';
import 'dart:html';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:js/js.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/web/base.dart';
import 'media.dart';
@JS('jsQR')
external Code? jsQR(dynamic data, int? width, int? height);
... ... @@ -24,55 +20,19 @@ class Code {
}
class JsQrCodeReader extends WebBarcodeReaderBase {
// The video stream. Will be initialized later to see which camera needs to be used.
MediaStream? _localStream;
VideoElement video = VideoElement();
DivElement vidDiv = DivElement();
@override
bool get isStarted => _localStream != null;
class JsQrCodeReader extends WebBarcodeReaderBase with InternalStreamCreation {
JsQrCodeReader({required super.videoContainer});
@override
int get videoWidth => video.videoWidth;
@override
int get videoHeight => video.videoHeight;
bool get isStarted => localMediaStream != null;
@override
Future<void> start({
required String viewID,
required CameraFacing cameraFacing,
}) async {
vidDiv.children = [video];
// See https://github.com/flutter/flutter/issues/41563
// ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls
ui.platformViewRegistry.registerViewFactory(
viewID,
(int id) => vidDiv
..style.width = '100%'
..style.height = '100%',
);
// Check if browser supports multiple camera's and set if supported
final Map? capabilities =
window.navigator.mediaDevices?.getSupportedConstraints();
if (capabilities != null && capabilities['facingMode'] as bool) {
final constraints = {
'video': VideoOptions(
facingMode:
cameraFacing == CameraFacing.front ? 'user' : 'environment',
)
};
_localStream =
await window.navigator.mediaDevices?.getUserMedia(constraints);
} else {
_localStream = await window.navigator.mediaDevices
?.getUserMedia({'video': true});
}
videoContainer.children = [video];
video.srcObject = _localStream;
final stream = await initMediaStream(cameraFacing);
// TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
// final track = _localStream?.getVideoTracks();
... ... @@ -81,9 +41,26 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
// final photoCapabilities = await imageCapture.getPhotoCapabilities();
// }
prepareVideoElement(video);
if (stream != null) {
await attachStreamToVideo(stream, video);
}
}
@override
void prepareVideoElement(VideoElement videoSource) {
// required to tell iOS safari we don't want fullscreen
video.setAttribute('playsinline', 'true');
await video.play();
videoSource.setAttribute('playsinline', 'true');
}
@override
Future<void> attachStreamToVideo(
MediaStream stream,
VideoElement videoSource,
) async {
localMediaStream = stream;
videoSource.srcObject = stream;
await videoSource.play();
}
@override
... ... @@ -95,7 +72,7 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
/// Captures a frame and analyzes it for QR codes
Future<Code?> _captureFrame(VideoElement video) async {
if (_localStream == null) return null;
if (localMediaStream == null) return null;
final canvas = CanvasElement(width: video.videoWidth, height: video.videoHeight);
final ctx = canvas.context2D;
... ... @@ -105,21 +82,4 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
final code = jsQR(imgData.data, canvas.width, canvas.height);
return code;
}
@override
Future<void> stop() async {
try {
// Stop the camera stream
_localStream?.getTracks().forEach((track) {
if (track.readyState == 'live') {
track.stop();
}
});
} catch (e) {
debugPrint('Failed to stop stream: $e');
}
video.srcObject = null;
_localStream = null;
}
}
... ...