Toggle navigation
Toggle navigation
This project
Loading...
Sign in
flutter_package
/
mobile_scanner
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
Julian Steenbakker
2022-03-10 16:10:44 +0100
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
80364d055c544c507ef9e2461850f31bc258b9d4
80364d05
1 parent
56092925
feat: update web integration
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
245 additions
and
275 deletions
lib/mobile_scanner_web_plugin.dart
lib/src/mobile_scanner.dart
lib/src/mobile_scanner_arguments.dart
lib/src/mobile_scanner_controller.dart
lib/src/web/flutter_qr_web.dart
lib/src/web/jsqr.dart
pubspec.yaml
lib/mobile_scanner_web_plugin.dart
0 → 100644
View file @
80364d0
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
});
}
}
}
...
...
lib/src/mobile_scanner.dart
View file @
80364d0
...
...
@@ -2,10 +2,6 @@ import 'package:flutter/foundation.dart';
import
'package:flutter/material.dart'
;
import
'package:mobile_scanner/mobile_scanner.dart'
;
import
'mobile_scanner_arguments.dart'
;
import
'web/flutter_qr_web.dart'
;
enum
Ratio
{
ratio_4_3
,
ratio_16_9
}
/// A widget showing a live camera preview.
...
...
@@ -64,12 +60,6 @@ class _MobileScannerState extends State<MobileScanner>
@override
Widget
build
(
BuildContext
context
)
{
if
(
kIsWeb
)
{
return
WebScanner
(
onDetect:
(
barcode
)
=>
widget
.
onDetect
!(
barcode
,
null
),
cameraFacing:
CameraFacing
.
back
,
);
}
else
{
return
LayoutBuilder
(
builder:
(
context
,
BoxConstraints
constraints
)
{
return
ValueListenableBuilder
(
valueListenable:
controller
.
args
,
...
...
@@ -96,7 +86,7 @@ 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
!
),
),
),
),
...
...
@@ -105,7 +95,6 @@ class _MobileScannerState extends State<MobileScanner>
});
});
}
}
@override
void
didUpdateWidget
(
covariant
MobileScanner
oldWidget
)
{
...
...
lib/src/mobile_scanner_arguments.dart
View file @
80364d0
...
...
@@ -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
});
}
...
...
lib/src/mobile_scanner_controller.dart
View file @
80364d0
...
...
@@ -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'
;
...
...
@@ -98,6 +99,9 @@ class MobileScannerController {
case
'barcodeMac'
:
barcodesController
.
add
(
Barcode
(
rawValue:
data
[
'payload'
]));
break
;
case
'barcodeWeb'
:
barcodesController
.
add
(
Barcode
(
rawValue:
data
));
break
;
default
:
throw
UnimplementedError
();
}
...
...
@@ -125,19 +129,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
;
...
...
@@ -174,10 +181,20 @@ 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
;
}
...
...
lib/src/web/flutter_qr_web.dart
deleted
100644 → 0
View file @
5609292
// ignore_for_file: avoid_web_libraries_in_flutter
import
'dart:async'
;
import
'dart:core'
;
import
'dart:html'
as
html
;
import
'dart:ui'
as
ui
;
import
'package:flutter/material.dart'
;
import
'../../mobile_scanner.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
WebScanner
extends
StatefulWidget
{
final
Function
(
Barcode
)
onDetect
;
final
CameraFacing
?
cameraFacing
;
const
WebScanner
(
{
Key
?
key
,
required
this
.
onDetect
,
this
.
cameraFacing
=
CameraFacing
.
front
})
:
super
(
key:
key
);
@override
_WebScannerState
createState
()
=>
_WebScannerState
();
// need a global for the registerViewFactory
static
html
.
DivElement
vidDiv
=
html
.
DivElement
();
static
Future
<
bool
>
cameraAvailable
()
async
{
final
sources
=
await
html
.
window
.
navigator
.
mediaDevices
!.
enumerateDevices
();
// List<String> vidIds = [];
var
hasCam
=
false
;
for
(
final
e
in
sources
)
{
if
(
e
.
kind
==
'videoinput'
)
{
// vidIds.add(e['deviceId']);
hasCam
=
true
;
}
}
return
hasCam
;
}
}
class
_WebScannerState
extends
State
<
WebScanner
>
{
// Which way the camera is facing
// late CameraFacing facing;
// The camera stream to display to the user
html
.
MediaStream
?
_localStream
;
// Check if analyzer is processing barcode
bool
_currentlyProcessing
=
false
;
// QRViewControllerWeb? _controller;
// Set size of the webview
// Size _size = const Size(0, 0);
// TODO: Timer for capture?
Timer
?
timer
;
// String? code;
// TODO: Error message if error
String
?
_errorMsg
;
// Video element to be played on
html
.
VideoElement
video
=
html
.
VideoElement
();
// ID of the video feed
String
viewID
=
'WebScanner-'
+
DateTime
.
now
().
millisecondsSinceEpoch
.
toString
();
// final StreamController<Barcode> _scanUpdateController =
// StreamController<Barcode>();
// Timer for interval capture
Timer
?
_frameIntervall
;
@override
void
initState
()
{
super
.
initState
();
// facing = widget.cameraFacing ?? CameraFacing.front;
WebScanner
.
vidDiv
.
children
=
[
video
];
// ignore: UNDEFINED_PREFIXED_NAME
ui
.
platformViewRegistry
.
registerViewFactory
(
viewID
,
(
int
id
)
=>
WebScanner
.
vidDiv
);
// giving JavaScipt some time to process the DOM changes
Timer
(
const
Duration
(
milliseconds:
500
),
()
{
start
();
});
}
/// Initialize camera and capture frame
Future
start
()
async
{
await
_startVideoStream
();
_frameIntervall
?.
cancel
();
_frameIntervall
=
Timer
.
periodic
(
const
Duration
(
milliseconds:
200
),
(
timer
)
{
_captureFrame
();
});
}
void
cancel
()
{
if
(
timer
!=
null
)
{
timer
!.
cancel
();
timer
=
null
;
}
if
(
_currentlyProcessing
)
{
_stopVideoStream
();
}
}
@override
void
dispose
()
{
cancel
();
super
.
dispose
();
}
/// Starts a video stream if not started already
Future
<
void
>
_startVideoStream
()
async
{
// Check if stream is running
if
(
_localStream
!=
null
)
return
;
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:
(
widget
.
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
;
// required to tell iOS safari we don't want fullscreen
video
.
setAttribute
(
'playsinline'
,
'true'
);
// TODO: Check controller
// 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
<
void
>
_stopVideoStream
()
async
{
try
{
// Stop the camera stream
_localStream
!.
getTracks
().
forEach
((
track
)
{
if
(
track
.
readyState
==
'live'
)
{
track
.
stop
();
}
});
video
.
srcObject
=
null
;
_localStream
=
null
;
}
catch
(
e
)
{
debugPrint
(
'Failed to stop stream:
$e
'
);
}
}
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 size =
// Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0);
// if (size != _size) {
// setState(() {
// _setCanvasSize(size);
// });
// }
// debugPrint('img.data: ${imgData.data}');
final
code
=
jsQR
(
imgData
.
data
,
canvas
.
width
,
canvas
.
height
);
// ignore: unnecessary_null_comparison
if
(
code
!=
null
)
{
debugPrint
(
'CODE:
$code
'
);
// widget.onDetect(Barcode(rawValue: code.data));
// print('Barcode: ${code.data}');
// _scanUpdateController
// .add(Barcode(rawValue: code.data));
}
}
@override
Widget
build
(
BuildContext
context
)
{
if
(
_errorMsg
!=
null
)
{
return
Center
(
child:
Text
(
_errorMsg
!));
}
if
(
_localStream
==
null
)
{
return
const
Center
(
child:
CircularProgressIndicator
());
}
return
SizedBox
(
width:
MediaQuery
.
of
(
context
).
size
.
width
,
height:
MediaQuery
.
of
(
context
).
size
.
height
,
child:
FittedBox
(
child:
SizedBox
(
width:
video
.
videoWidth
.
toDouble
(),
height:
video
.
videoHeight
.
toDouble
(),
child:
HtmlElementView
(
viewType:
viewID
))));
}
}
lib/src/web/jsqr.dart
View file @
80364d0
...
...
@@ -4,7 +4,7 @@ library jsqr;
import
'package:js/js.dart'
;
@JS
(
'jsQR'
)
external
Code
jsQR
(
var
data
,
int
?
width
,
int
?
height
);
external
Code
?
jsQR
(
var
data
,
int
?
width
,
int
?
height
);
@JS
()
class
Code
{
...
...
pubspec.yaml
View file @
80364d0
...
...
@@ -11,6 +11,8 @@ dependencies:
js
:
^0.6.4
flutter
:
sdk
:
flutter
flutter_web_plugins
:
sdk
:
flutter
dev_dependencies
:
flutter_test
:
...
...
@@ -26,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
...
...
Please
register
or
login
to post a comment