p-mazhnik

refactor: abstract web scanner

@@ -2,13 +2,11 @@ import 'dart:async'; @@ -2,13 +2,11 @@ import 'dart:async';
2 import 'dart:html' as html; 2 import 'dart:html' as html;
3 import 'dart:ui' as ui; 3 import 'dart:ui' as ui;
4 4
5 -import 'package:flutter/material.dart';  
6 import 'package:flutter/services.dart'; 5 import 'package:flutter/services.dart';
7 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 6 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
8 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 7 import 'package:mobile_scanner/src/enums/camera_facing.dart';
9 import 'package:mobile_scanner/src/web/base.dart'; 8 import 'package:mobile_scanner/src/web/base.dart';
10 import 'package:mobile_scanner/src/web/jsqr.dart'; 9 import 'package:mobile_scanner/src/web/jsqr.dart';
11 -import 'package:mobile_scanner/src/web/media.dart';  
12 10
13 /// This plugin is the web implementation of mobile_scanner. 11 /// This plugin is the web implementation of mobile_scanner.
14 /// It only supports QR codes. 12 /// It only supports QR codes.
@@ -39,7 +37,10 @@ class MobileScannerWebPlugin { @@ -39,7 +37,10 @@ class MobileScannerWebPlugin {
39 // Determine wether device has flas 37 // Determine wether device has flas
40 bool hasFlash = false; 38 bool hasFlash = false;
41 39
42 - final WebBarcodeReaderBase _barCodeReader = JsQrCodeReader(); 40 + final html.DivElement vidDiv = html.DivElement();
  41 +
  42 + late final WebBarcodeReaderBase _barCodeReader =
  43 + JsQrCodeReader(videoContainer: vidDiv);
43 StreamSubscription? _barCodeStreamSubscription; 44 StreamSubscription? _barCodeStreamSubscription;
44 45
45 /// Handle incomming messages 46 /// Handle incomming messages
@@ -80,6 +81,17 @@ class MobileScannerWebPlugin { @@ -80,6 +81,17 @@ class MobileScannerWebPlugin {
80 cameraFacing = CameraFacing.values[arguments['facing'] as int]; 81 cameraFacing = CameraFacing.values[arguments['facing'] as int];
81 } 82 }
82 83
  84 + // See https://github.com/flutter/flutter/issues/41563
  85 + // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls
  86 + ui.platformViewRegistry.registerViewFactory(
  87 + viewID,
  88 + (int id) {
  89 + return vidDiv
  90 + ..style.width = '100%'
  91 + ..style.height = '100%';
  92 + },
  93 + );
  94 +
83 // Check if stream is running 95 // Check if stream is running
84 if (_barCodeReader.isStarted) { 96 if (_barCodeReader.isStarted) {
85 return { 97 return {
@@ -90,7 +102,6 @@ class MobileScannerWebPlugin { @@ -90,7 +102,6 @@ class MobileScannerWebPlugin {
90 } 102 }
91 try { 103 try {
92 await _barCodeReader.start( 104 await _barCodeReader.start(
93 - viewID: viewID,  
94 cameraFacing: cameraFacing, 105 cameraFacing: cameraFacing,
95 ); 106 );
96 107
  1 +import 'dart:html';
  2 +
  3 +import 'package:flutter/material.dart';
1 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 4 import 'package:mobile_scanner/src/enums/camera_facing.dart';
  5 +import 'package:mobile_scanner/src/web/media.dart';
2 6
3 abstract class WebBarcodeReaderBase { 7 abstract class WebBarcodeReaderBase {
4 /// Timer used to capture frames to be analyzed 8 /// Timer used to capture frames to be analyzed
5 - final frameInterval = const Duration(milliseconds: 200); 9 + final Duration frameInterval;
  10 + final DivElement videoContainer;
  11 +
  12 + const WebBarcodeReaderBase({
  13 + required this.videoContainer,
  14 + this.frameInterval = const Duration(milliseconds: 200),
  15 + });
6 16
7 bool get isStarted; 17 bool get isStarted;
8 18
@@ -11,7 +21,6 @@ abstract class WebBarcodeReaderBase { @@ -11,7 +21,6 @@ abstract class WebBarcodeReaderBase {
11 21
12 /// Starts streaming video 22 /// Starts streaming video
13 Future<void> start({ 23 Future<void> start({
14 - required String viewID,  
15 required CameraFacing cameraFacing, 24 required CameraFacing cameraFacing,
16 }); 25 });
17 26
@@ -21,3 +30,59 @@ abstract class WebBarcodeReaderBase { @@ -21,3 +30,59 @@ abstract class WebBarcodeReaderBase {
21 /// Stops streaming video 30 /// Stops streaming video
22 Future<void> stop(); 31 Future<void> stop();
23 } 32 }
  33 +
  34 +mixin InternalStreamCreation on WebBarcodeReaderBase {
  35 + /// The video stream.
  36 + /// Will be initialized later to see which camera needs to be used.
  37 + MediaStream? localMediaStream;
  38 + final VideoElement video = VideoElement();
  39 +
  40 + @override
  41 + int get videoWidth => video.videoWidth;
  42 + @override
  43 + int get videoHeight => video.videoHeight;
  44 +
  45 + Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
  46 + // Check if browser supports multiple camera's and set if supported
  47 + final Map? capabilities =
  48 + window.navigator.mediaDevices?.getSupportedConstraints();
  49 + final Map<String, dynamic> constraints;
  50 + if (capabilities != null && capabilities['facingMode'] as bool) {
  51 + constraints = {
  52 + 'video': VideoOptions(
  53 + facingMode:
  54 + cameraFacing == CameraFacing.front ? 'user' : 'environment',
  55 + )
  56 + };
  57 + } else {
  58 + constraints = {'video': true};
  59 + }
  60 + final stream =
  61 + await window.navigator.mediaDevices?.getUserMedia(constraints);
  62 + return stream;
  63 + }
  64 +
  65 + void prepareVideoElement(VideoElement videoSource);
  66 +
  67 + Future<void> attachStreamToVideo(
  68 + MediaStream stream,
  69 + VideoElement videoSource,
  70 + );
  71 +
  72 + @override
  73 + Future<void> stop() async {
  74 + try {
  75 + // Stop the camera stream
  76 + localMediaStream?.getTracks().forEach((track) {
  77 + if (track.readyState == 'live') {
  78 + track.stop();
  79 + }
  80 + });
  81 + } catch (e) {
  82 + debugPrint('Failed to stop stream: $e');
  83 + }
  84 + video.srcObject = null;
  85 + localMediaStream = null;
  86 + videoContainer.children = [];
  87 + }
  88 +}
@@ -4,15 +4,11 @@ library jsqr; @@ -4,15 +4,11 @@ library jsqr;
4 import 'dart:async'; 4 import 'dart:async';
5 import 'dart:html'; 5 import 'dart:html';
6 import 'dart:typed_data'; 6 import 'dart:typed_data';
7 -import 'dart:ui' as ui;  
8 7
9 -import 'package:flutter/widgets.dart';  
10 import 'package:js/js.dart'; 8 import 'package:js/js.dart';
11 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 9 import 'package:mobile_scanner/src/enums/camera_facing.dart';
12 import 'package:mobile_scanner/src/web/base.dart'; 10 import 'package:mobile_scanner/src/web/base.dart';
13 11
14 -import 'media.dart';  
15 -  
16 @JS('jsQR') 12 @JS('jsQR')
17 external Code? jsQR(dynamic data, int? width, int? height); 13 external Code? jsQR(dynamic data, int? width, int? height);
18 14
@@ -24,55 +20,19 @@ class Code { @@ -24,55 +20,19 @@ class Code {
24 } 20 }
25 21
26 22
27 -class JsQrCodeReader extends WebBarcodeReaderBase {  
28 - // The video stream. Will be initialized later to see which camera needs to be used.  
29 - MediaStream? _localStream;  
30 -  
31 - VideoElement video = VideoElement();  
32 -  
33 - DivElement vidDiv = DivElement();  
34 -  
35 - @override  
36 - bool get isStarted => _localStream != null; 23 +class JsQrCodeReader extends WebBarcodeReaderBase with InternalStreamCreation {
  24 + JsQrCodeReader({required super.videoContainer});
37 25
38 @override 26 @override
39 - int get videoWidth => video.videoWidth;  
40 - @override  
41 - int get videoHeight => video.videoHeight; 27 + bool get isStarted => localMediaStream != null;
42 28
43 @override 29 @override
44 Future<void> start({ 30 Future<void> start({
45 - required String viewID,  
46 required CameraFacing cameraFacing, 31 required CameraFacing cameraFacing,
47 }) async { 32 }) async {
48 - vidDiv.children = [video];  
49 - // See https://github.com/flutter/flutter/issues/41563  
50 - // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls  
51 - ui.platformViewRegistry.registerViewFactory(  
52 - viewID,  
53 - (int id) => vidDiv  
54 - ..style.width = '100%'  
55 - ..style.height = '100%',  
56 - );  
57 - // Check if browser supports multiple camera's and set if supported  
58 - final Map? capabilities =  
59 - window.navigator.mediaDevices?.getSupportedConstraints();  
60 - if (capabilities != null && capabilities['facingMode'] as bool) {  
61 - final constraints = {  
62 - 'video': VideoOptions(  
63 - facingMode:  
64 - cameraFacing == CameraFacing.front ? 'user' : 'environment',  
65 - )  
66 - };  
67 -  
68 - _localStream =  
69 - await window.navigator.mediaDevices?.getUserMedia(constraints);  
70 - } else {  
71 - _localStream = await window.navigator.mediaDevices  
72 - ?.getUserMedia({'video': true});  
73 - } 33 + videoContainer.children = [video];
74 34
75 - video.srcObject = _localStream; 35 + final stream = await initMediaStream(cameraFacing);
76 36
77 // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 37 // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
78 // final track = _localStream?.getVideoTracks(); 38 // final track = _localStream?.getVideoTracks();
@@ -81,9 +41,26 @@ class JsQrCodeReader extends WebBarcodeReaderBase { @@ -81,9 +41,26 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
81 // final photoCapabilities = await imageCapture.getPhotoCapabilities(); 41 // final photoCapabilities = await imageCapture.getPhotoCapabilities();
82 // } 42 // }
83 43
  44 + prepareVideoElement(video);
  45 + if (stream != null) {
  46 + await attachStreamToVideo(stream, video);
  47 + }
  48 + }
  49 +
  50 + @override
  51 + void prepareVideoElement(VideoElement videoSource) {
84 // required to tell iOS safari we don't want fullscreen 52 // required to tell iOS safari we don't want fullscreen
85 - video.setAttribute('playsinline', 'true');  
86 - await video.play(); 53 + videoSource.setAttribute('playsinline', 'true');
  54 + }
  55 +
  56 + @override
  57 + Future<void> attachStreamToVideo(
  58 + MediaStream stream,
  59 + VideoElement videoSource,
  60 + ) async {
  61 + localMediaStream = stream;
  62 + videoSource.srcObject = stream;
  63 + await videoSource.play();
87 } 64 }
88 65
89 @override 66 @override
@@ -95,7 +72,7 @@ class JsQrCodeReader extends WebBarcodeReaderBase { @@ -95,7 +72,7 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
95 72
96 /// Captures a frame and analyzes it for QR codes 73 /// Captures a frame and analyzes it for QR codes
97 Future<Code?> _captureFrame(VideoElement video) async { 74 Future<Code?> _captureFrame(VideoElement video) async {
98 - if (_localStream == null) return null; 75 + if (localMediaStream == null) return null;
99 final canvas = CanvasElement(width: video.videoWidth, height: video.videoHeight); 76 final canvas = CanvasElement(width: video.videoWidth, height: video.videoHeight);
100 final ctx = canvas.context2D; 77 final ctx = canvas.context2D;
101 78
@@ -105,21 +82,4 @@ class JsQrCodeReader extends WebBarcodeReaderBase { @@ -105,21 +82,4 @@ class JsQrCodeReader extends WebBarcodeReaderBase {
105 final code = jsQR(imgData.data, canvas.width, canvas.height); 82 final code = jsQR(imgData.data, canvas.width, canvas.height);
106 return code; 83 return code;
107 } 84 }
108 -  
109 - @override  
110 - Future<void> stop() async {  
111 - try {  
112 - // Stop the camera stream  
113 - _localStream?.getTracks().forEach((track) {  
114 - if (track.readyState == 'live') {  
115 - track.stop();  
116 - }  
117 - });  
118 - } catch (e) {  
119 - debugPrint('Failed to stop stream: $e');  
120 - }  
121 -  
122 - video.srcObject = null;  
123 - _localStream = null;  
124 - }  
125 } 85 }