diff --git a/example/web/favicon.png b/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/example/web/favicon.png differ
diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/example/web/icons/Icon-192.png differ
diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/example/web/icons/Icon-512.png differ
diff --git a/example/web/index.html b/example/web/index.html
new file mode 100644
index 0000000..f243f37
--- /dev/null
+++ b/example/web/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ example
+
+
+
+
+
+
+
+
+
diff --git a/example/web/manifest.json b/example/web/manifest.json
new file mode 100644
index 0000000..8c01291
--- /dev/null
+++ b/example/web/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "example",
+ "short_name": "example",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "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"
+ }
+ ]
+}
diff --git a/lib/src/web/flutter_qr_stub.dart b/lib/src/web/flutter_qr_stub.dart
new file mode 100644
index 0000000..1193cf4
--- /dev/null
+++ b/lib/src/web/flutter_qr_stub.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/widgets.dart';
+import 'package:qr_code_scanner/src/types/camera.dart';
+
+Widget createWebQrView({onPlatformViewCreated, CameraFacing? cameraFacing}) =>
+ throw UnimplementedError();
diff --git a/lib/src/web/flutter_qr_web.dart b/lib/src/web/flutter_qr_web.dart
new file mode 100644
index 0000000..22896ae
--- /dev/null
+++ b/lib/src/web/flutter_qr_web.dart
@@ -0,0 +1,334 @@
+import 'dart:async';
+import 'dart:core';
+import 'dart:html' as html;
+import 'dart:js_util';
+import 'dart:ui' as ui;
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+
+import '../../qr_code_scanner.dart';
+import '../qr_code_scanner.dart';
+import '../types/camera.dart';
+import 'jsqr.dart';
+import 'media.dart';
+
+/// Even though it has been highly modified, the origial implementation has been
+/// adopted from https://github.com:treeder/jsqr_flutter
+///
+/// Copyright 2020 @treeder
+/// Copyright 2021 The one with the braid
+
+class WebQrView extends StatefulWidget {
+ final QRViewCreatedCallback onPlatformViewCreated;
+ final CameraFacing? cameraFacing;
+
+ const WebQrView(
+ {Key? key,
+ required this.onPlatformViewCreated,
+ this.cameraFacing = CameraFacing.front})
+ : super(key: key);
+
+ @override
+ _WebQrViewState createState() => _WebQrViewState();
+
+ static html.DivElement vidDiv =
+ html.DivElement(); // need a global for the registerViewFactory
+
+ static Future cameraAvailable() async {
+ final sources =
+ await html.window.navigator.mediaDevices!.enumerateDevices();
+ // List vidIds = [];
+ var hasCam = false;
+ for (final e in sources) {
+ if (e.kind == 'videoinput') {
+ // vidIds.add(e['deviceId']);
+ hasCam = true;
+ }
+ }
+ return hasCam;
+ }
+}
+
+class _WebQrViewState extends State {
+ html.MediaStream? _localStream;
+ // html.CanvasElement canvas;
+ // html.CanvasRenderingContext2D ctx;
+ bool _currentlyProcessing = false;
+ html.MediaRecorder? _mediaRecorder;
+
+ QRViewControllerWeb? _controller;
+
+ late Size _size = Size(0, 0);
+ Timer? timer;
+ String? code;
+ String? _errorMsg;
+ var video;
+ String viewID = 'QRVIEW-' + DateTime.now().millisecondsSinceEpoch.toString();
+
+ final StreamController _scanUpdateController =
+ StreamController();
+ late CameraFacing facing;
+
+ Timer? _frameIntervall;
+
+ final _canvasKey = GlobalKey();
+
+ double _scaleFactor = 1;
+
+ @override
+ void initState() {
+ super.initState();
+
+ facing = widget.cameraFacing ?? CameraFacing.front;
+
+ video = html.VideoElement();
+ WebQrView.vidDiv.children = [video];
+ // ignore: UNDEFINED_PREFIXED_NAME
+ ui.platformViewRegistry
+ .registerViewFactory(viewID, (int id) => WebQrView.vidDiv);
+ // giving JavaScipt some time to process the DOM changes
+ Timer(Duration(milliseconds: 500), () {
+ start();
+ });
+ }
+
+ Future start() async {
+ await _makeCall();
+ _frameIntervall?.cancel();
+ _frameIntervall = Timer.periodic(Duration(milliseconds: 200), (timer) {
+ _captureFrame2();
+ });
+ }
+
+ void cancel() {
+ if (timer != null) {
+ timer!.cancel();
+ timer = null;
+ }
+ if (_currentlyProcessing) {
+ _stopStream();
+ }
+ }
+
+ @override
+ void dispose() {
+ cancel();
+ super.dispose();
+ }
+
+ // Platform messages are asynchronous, so we initialize in an async method.
+ Future _makeCall() async {
+ if (_localStream != null) {
+ return;
+ }
+
+ try {
+ var constraints = UserMediaOptions(
+ video: VideoOptions(
+ facingMode: (facing == CameraFacing.front ? 'user' : 'environment'),
+ ));
+ // dart style, not working properly:
+ // var stream =
+ // await html.window.navigator.mediaDevices.getUserMedia(constraints);
+ // straight JS:
+ var stream = await promiseToFuture(getUserMedia(constraints));
+ _localStream = stream;
+ video.srcObject = _localStream;
+ video.setAttribute('playsinline',
+ 'true'); // required to tell iOS safari we don't want fullscreen
+ if (_controller == null) {
+ _controller = QRViewControllerWeb(this);
+ widget.onPlatformViewCreated(_controller!);
+ }
+ await video.play();
+ } catch (e) {
+ cancel();
+ setState(() {
+ _errorMsg = e.toString();
+ });
+ return;
+ }
+ if (!mounted) return;
+
+ setState(() {
+ _currentlyProcessing = true;
+ });
+ }
+
+ Future _stopStream() async {
+ try {
+ // await _localStream.dispose();
+ _localStream!.getTracks().forEach((track) {
+ if (track.readyState == 'live') {
+ track.stop();
+ }
+ });
+ // video.stop();
+ video.srcObject = null;
+ _localStream = null;
+ // _localRenderer.srcObject = null;
+ // ignore: empty_catches
+ } catch (e) {}
+ }
+
+ Future _captureFrame2() async {
+ if (_localStream == null) {
+ return null;
+ }
+ final canvas =
+ html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
+ final ctx = canvas.context2D;
+ // canvas.width = video.videoWidth;
+ // canvas.height = video.videoHeight;
+ ctx.drawImage(video, 0, 0);
+ final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
+
+ final size =
+ Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0);
+ if (size != _size) {
+ setState(() {
+ _setCanvasSize(size);
+ });
+ }
+
+ final code = jsQR(imgData.data, canvas.width, canvas.height);
+ // ignore: unnecessary_null_comparison
+ if (code != null) {
+ _scanUpdateController
+ .add(Barcode(code.data, BarcodeFormat.qrcode, code.data.codeUnits));
+ }
+ }
+
+ Future _captureImage() async {
+ if (_localStream == null) {
+ return null;
+ }
+ final canvas =
+ html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
+ final ctx = canvas.context2D;
+ // canvas.width = video.videoWidth;
+ // canvas.height = video.videoHeight;
+ ctx.drawImage(video, 0, 0);
+ var dataUrl = canvas.toDataUrl('image/jpeg', 0.9);
+ return dataUrl;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_errorMsg != null) {
+ return Center(child: Text(_errorMsg!));
+ }
+ if (_localStream == null) {
+ return Center(child: CircularProgressIndicator());
+ }
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ var zoom = 1.0;
+
+ if (_size.height != 0) zoom = constraints.maxHeight / _size.height;
+
+ if (_size.width != 0) {
+ final horizontalZoom = constraints.maxWidth / _size.width;
+ if (horizontalZoom > zoom) {
+ zoom = horizontalZoom;
+ }
+ }
+
+ return SizedBox(
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ child: Center(
+ child: SizedBox.fromSize(
+ size: _size,
+ child: Transform.scale(
+ alignment: Alignment.center,
+ scale: zoom,
+ child: HtmlElementView(viewType: viewID),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ void _setCanvasSize(ui.Size size) {
+ setState(() {
+ _size = size;
+ });
+ }
+}
+
+class QRViewControllerWeb implements QRViewController {
+ final _WebQrViewState _state;
+
+ QRViewControllerWeb(this._state);
+ @override
+ void dispose() => _state.cancel();
+
+ @override
+ Future flipCamera() async {
+ // TODO: improve error handling
+ _state.facing = _state.facing == CameraFacing.front
+ ? CameraFacing.back
+ : CameraFacing.front;
+ await _state.start();
+ return _state.facing;
+ }
+
+ @override
+ Future getCameraInfo() async {
+ return _state.facing;
+ }
+
+ @override
+ Future getFlashStatus() async {
+ // TODO: flash is simply not supported by JavaScipt. To avoid issuing applications, we always return it to be off.
+ return false;
+ }
+
+ @override
+ Future getSystemFeatures() {
+ // TODO: implement getSystemFeatures
+ throw UnimplementedError();
+ }
+
+ @override
+ // TODO: implement hasPermissions. Blocking: WebQrView.cameraAvailable() returns a Future whereas a bool is required
+ bool get hasPermissions => throw UnimplementedError();
+
+ @override
+ Future pauseCamera() {
+ // TODO: implement pauseCamera
+ throw UnimplementedError();
+ }
+
+ @override
+ Future resumeCamera() {
+ // TODO: implement resumeCamera
+ throw UnimplementedError();
+ }
+
+ @override
+ Stream get scannedDataStream => _state._scanUpdateController.stream;
+
+ @override
+ Future stopCamera() {
+ // TODO: implement stopCamera
+ throw UnimplementedError();
+ }
+
+ @override
+ Future toggleFlash() async {
+ // TODO: flash is simply not supported by JavaScipt
+ return;
+ }
+}
+
+Widget createWebQrView({onPlatformViewCreated, CameraFacing? cameraFacing}) =>
+ WebQrView(
+ onPlatformViewCreated: onPlatformViewCreated,
+ cameraFacing: cameraFacing,
+ );
diff --git a/lib/src/web/jsqr.dart b/lib/src/web/jsqr.dart
new file mode 100644
index 0000000..0c7a36f
--- /dev/null
+++ b/lib/src/web/jsqr.dart
@@ -0,0 +1,12 @@
+@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;
+}
diff --git a/lib/src/web/media.dart b/lib/src/web/media.dart
new file mode 100644
index 0000000..f1a4175
--- /dev/null
+++ b/lib/src/web/media.dart
@@ -0,0 +1,35 @@
+// 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 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 factory VideoOptions({String? facingMode, DeviceIdOptions? deviceId});
+}
+
+@JS()
+@anonymous
+class DeviceIdOptions {
+ external String get exact;
+
+ external factory DeviceIdOptions({String? exact});
+}