Julian Steenbakker
Committed by GitHub

Merge pull request #49 from juliansteenbakker/web

Feature: web support
... ... @@ -16,10 +16,10 @@ class _BarcodeScannerWithControllerState
String? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
);
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
);
bool isStarted = true;
... ...
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- 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="icon" type="image/png" href="favicon.png"/>
<title>example</title>
<link rel="manifest" href="manifest.json">
<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>-->
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
</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('flutter-first-frame', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
... ...
{
"name": "Mobile Scanner Example",
"short_name": "mobile_scanner_example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A barcode and qr code scanner example.",
"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"
}
]
}
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/web/jsqr.dart';
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:mobile_scanner/src/web/media.dart';
/// This plugin is the web implementation of mobile_scanner.
/// It only supports QR codes.
class MobileScannerWebPlugin {
static void registerWith(Registrar registrar) {
PluginEventChannel event = PluginEventChannel(
'dev.steenbakker.mobile_scanner/scanner/event',
const StandardMethodCodec(),
registrar);
MethodChannel channel = MethodChannel(
'dev.steenbakker.mobile_scanner/scanner/method',
const StandardMethodCodec(),
registrar);
final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
WidgetsFlutterBinding.ensureInitialized();
channel.setMethodCallHandler(instance.handleMethodCall);
event.setController(instance.controller);
}
// Controller to send events back to the framework
StreamController controller = StreamController();
// The video stream. Will be initialized later to see which camera needs to be used.
html.MediaStream? _localStream;
html.VideoElement video = html.VideoElement();
// ID of the video feed
String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}';
// Determine wether device has flas
bool hasFlash = false;
// Timer used to capture frames to be analyzed
Timer? _frameInterval;
html.DivElement vidDiv = html.DivElement();
/// Handle incomming messages
Future<dynamic> handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'start':
return await _start(call.arguments);
case 'torch':
return await _torch(call.arguments);
case 'stop':
return await cancel();
default:
throw PlatformException(
code: 'Unimplemented',
details: "The mobile_scanner plugin for web doesn't implement "
"the method '${call.method}'");
}
}
/// Can enable or disable the flash if available
Future<void> _torch(arguments) async {
if (hasFlash) {
final track = _localStream?.getVideoTracks();
await track!.first.applyConstraints({
'advanced': {'torch': arguments == 1}
});
} else {
controller.addError('Device has no flash');
}
}
/// Starts the video stream and the scanner
Future<Map> _start(arguments) async {
vidDiv.children = [video];
final CameraFacing cameraFacing =
arguments['cameraFacing'] ?? CameraFacing.front;
// See https://github.com/flutter/flutter/issues/41563
// ignore: UNDEFINED_PREFIXED_NAME
ui.platformViewRegistry.registerViewFactory(
viewID,
(int id) => vidDiv
..style.width = '100%'
..style.height = '100%');
// Check if stream is running
if (_localStream != null) {
return {
'ViewID': viewID,
'videoWidth': video.videoWidth,
'videoHeight': video.videoHeight
};
}
try {
// Check if browser supports multiple camera's and set if supported
Map? capabilities =
html.window.navigator.mediaDevices?.getSupportedConstraints();
if (capabilities != null && capabilities['facingMode']) {
UserMediaOptions constraints = UserMediaOptions(
video: VideoOptions(
facingMode:
(cameraFacing == CameraFacing.front ? 'user' : 'environment'),
width: {'ideal': 4096},
height: {'ideal': 2160},
));
_localStream =
await html.window.navigator.getUserMedia(video: constraints);
} else {
_localStream = await html.window.navigator.getUserMedia(video: true);
}
video.srcObject = _localStream;
// TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
// final track = _localStream?.getVideoTracks();
// if (track != null) {
// final imageCapture = html.ImageCapture(track.first);
// final photoCapabilities = await imageCapture.getPhotoCapabilities();
// }
// required to tell iOS safari we don't want fullscreen
video.setAttribute('playsinline', 'true');
await video.play();
// Then capture a frame to be analyzed every 200 miliseconds
_frameInterval =
Timer.periodic(const Duration(milliseconds: 200), (timer) {
_captureFrame();
});
return {
'ViewID': viewID,
'videoWidth': video.videoWidth,
'videoHeight': video.videoHeight,
'torchable': hasFlash
};
} catch (e) {
throw PlatformException(code: 'MobileScannerWeb', message: e.toString());
}
}
/// Check if any camera's are available
static Future<bool> cameraAvailable() async {
final sources =
await html.window.navigator.mediaDevices!.enumerateDevices();
for (final e in sources) {
if (e.kind == 'videoinput') {
return true;
}
}
return false;
}
/// Stops the video feed and analyzer
Future<void> cancel() 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;
_frameInterval?.cancel();
_frameInterval = null;
}
/// Captures a frame and analyzes it for QR codes
Future<dynamic> _captureFrame() async {
if (_localStream == null) return null;
final canvas =
html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
final ctx = canvas.context2D;
ctx.drawImage(video, 0, 0);
final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
final code = jsQR(imgData.data, canvas.width, canvas.height);
if (code != null) {
controller.add({'name': 'barcodeWeb', 'data': code.data});
}
}
}
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
... ... @@ -12,7 +13,7 @@ class MobileScanner extends StatefulWidget {
///
/// [barcode] The barcode object with all information about the scanned code.
/// [args] Information about the state of the MobileScanner widget
final Function(Barcode barcode, MobileScannerArguments args)? onDetect;
final Function(Barcode barcode, MobileScannerArguments? args)? onDetect;
/// TODO: Function that gets called when the Widget is initialized. Can be usefull
/// to check wether the device has a torch(flash) or not.
... ... @@ -78,7 +79,9 @@ class _MobileScannerState extends State<MobileScanner>
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: Texture(textureId: value.textureId),
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
),
... ...
... ... @@ -3,14 +3,16 @@ import 'package:flutter/material.dart';
/// Camera args for [CameraView].
class MobileScannerArguments {
/// The texture id.
final int textureId;
final int? textureId;
/// Size of the texture.
final Size size;
final bool hasTorch;
final String? webId;
/// Create a [MobileScannerArguments].
MobileScannerArguments(
{required this.textureId, required this.size, required this.hasTorch});
{this.textureId, required this.size, required this.hasTorch, this.webId});
}
... ...
... ... @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
... ... @@ -97,6 +98,9 @@ class MobileScannerController {
case 'barcodeMac':
barcodesController.add(Barcode(rawValue: data['payload']));
break;
case 'barcodeWeb':
barcodesController.add(Barcode(rawValue: data));
break;
default:
throw UnimplementedError();
}
... ... @@ -124,19 +128,22 @@ class MobileScannerController {
// setAnalyzeMode(AnalyzeMode.barcode.index);
// Check authorization status
MobileScannerState state =
MobileScannerState.values[await methodChannel.invokeMethod('state')];
switch (state) {
case MobileScannerState.undetermined:
final bool result = await methodChannel.invokeMethod('request');
state =
result ? MobileScannerState.authorized : MobileScannerState.denied;
break;
case MobileScannerState.denied:
isStarting = false;
throw PlatformException(code: 'NO ACCESS');
case MobileScannerState.authorized:
break;
if (!kIsWeb) {
MobileScannerState state =
MobileScannerState.values[await methodChannel.invokeMethod('state')];
switch (state) {
case MobileScannerState.undetermined:
final bool result = await methodChannel.invokeMethod('request');
state = result
? MobileScannerState.authorized
: MobileScannerState.denied;
break;
case MobileScannerState.denied:
isStarting = false;
throw PlatformException(code: 'NO ACCESS');
case MobileScannerState.authorized:
break;
}
}
cameraFacingState.value = facing;
... ... @@ -173,10 +180,19 @@ class MobileScannerController {
}
hasTorch = startResult['torchable'];
args.value = MobileScannerArguments(
textureId: startResult['textureId'],
size: toSize(startResult['size']),
hasTorch: hasTorch);
if (kIsWeb) {
args.value = MobileScannerArguments(
webId: startResult['ViewID'],
size: Size(startResult['videoWidth'], startResult['videoHeight']),
hasTorch: hasTorch);
} else {
args.value = MobileScannerArguments(
textureId: startResult['textureId'],
size: toSize(startResult['size']),
hasTorch: hasTorch);
}
isStarting = false;
}
... ...
@JS()
library jsqr;
import 'package:js/js.dart';
@JS('jsQR')
external Code? jsQR(var data, int? width, int? height);
@JS()
class Code {
external String get data;
}
... ...
// // This is here because dart doesn't seem to support this properly
// // https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart
@JS('navigator.mediaDevices')
library media_devices;
import 'package:js/js.dart';
@JS('getUserMedia')
external Future<dynamic> getUserMedia(UserMediaOptions constraints);
@JS()
@anonymous
class UserMediaOptions {
external VideoOptions get video;
external factory UserMediaOptions({VideoOptions? video});
}
@JS()
@anonymous
class VideoOptions {
external String get facingMode;
// external DeviceIdOptions get deviceId;
external Map get width;
external Map get height;
external factory VideoOptions(
{String? facingMode, DeviceIdOptions? deviceId, Map? width, Map? height});
}
@JS()
@anonymous
class DeviceIdOptions {
external String get exact;
external factory DeviceIdOptions({String? exact});
}
... ...
@JS()
library qrscanner;
import 'package:js/js.dart';
@JS('QrScanner')
external String scanImage(var data);
@JS()
class QrScanner {
external String get scanImage;
}
... ...
... ... @@ -8,8 +8,11 @@ environment:
flutter: ">=1.10.0"
dependencies:
js: ^0.6.3
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
dev_dependencies:
flutter_test:
... ... @@ -25,4 +28,7 @@ flutter:
ios:
pluginClass: MobileScannerPlugin
macos:
pluginClass: MobileScannerPlugin
\ No newline at end of file
pluginClass: MobileScannerPlugin
web:
pluginClass: MobileScannerWebPlugin
fileName: mobile_scanner_web_plugin.dart
\ No newline at end of file
... ...