Navaron Bracke

fix frozen video stream on second try on web

@@ -35,9 +35,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -35,9 +35,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
35 StreamSubscription<Object?>? _barcodesSubscription; 35 StreamSubscription<Object?>? _barcodesSubscription;
36 36
37 /// The container div element for the camera view. 37 /// The container div element for the camera view.
38 - ///  
39 - /// This container element is used by the barcode reader.  
40 - HTMLDivElement? _divElement; 38 + late HTMLDivElement _divElement;
41 39
42 /// The flag that keeps track of whether a permission request is in progress. 40 /// The flag that keeps track of whether a permission request is in progress.
43 /// 41 ///
@@ -54,8 +52,14 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -54,8 +52,14 @@ class MobileScannerWeb extends MobileScannerPlatform {
54 final StreamController<MediaTrackSettings> _settingsController = 52 final StreamController<MediaTrackSettings> _settingsController =
55 StreamController.broadcast(); 53 StreamController.broadcast();
56 54
57 - /// The view type for the platform view factory.  
58 - static const String _viewType = 'mobile-scanner-view'; 55 + /// The texture ID for the camera view.
  56 + int _textureId = 1;
  57 +
  58 + /// The video element for the camera view.
  59 + late HTMLVideoElement _videoElement;
  60 +
  61 + /// Get the view type for the platform view factory.
  62 + String _getViewType(int textureId) => 'mobile-scanner-view-$textureId';
59 63
60 static void registerWith(Registrar registrar) { 64 static void registerWith(Registrar registrar) {
61 MobileScannerPlatform.instance = MobileScannerWeb(); 65 MobileScannerPlatform.instance = MobileScannerWeb();
@@ -72,6 +76,33 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -72,6 +76,33 @@ class MobileScannerWeb extends MobileScannerPlatform {
72 Stream<double> get zoomScaleStateStream => 76 Stream<double> get zoomScaleStateStream =>
73 _settingsController.stream.map((_) => 1.0); 77 _settingsController.stream.map((_) => 1.0);
74 78
  79 + /// Create the [HTMLVideoElement] along with its parent container [HTMLDivElement].
  80 + HTMLVideoElement _createVideoElement(int textureId) {
  81 + final HTMLVideoElement videoElement = HTMLVideoElement();
  82 +
  83 + videoElement.style
  84 + ..height = '100%'
  85 + ..width = '100%'
  86 + ..objectFit = 'cover'
  87 + ..transformOrigin = 'center'
  88 + ..pointerEvents = 'none';
  89 +
  90 + // Attach the video element to its parent container
  91 + // and setup the PlatformView factory for this `textureId`.
  92 + _divElement = HTMLDivElement()
  93 + ..style.objectFit = 'cover'
  94 + ..style.height = '100%'
  95 + ..style.width = '100%'
  96 + ..append(videoElement);
  97 +
  98 + ui_web.platformViewRegistry.registerViewFactory(
  99 + _getViewType(textureId),
  100 + (_) => _divElement,
  101 + );
  102 +
  103 + return videoElement;
  104 + }
  105 +
75 void _handleMediaTrackSettingsChange(MediaTrackSettings settings) { 106 void _handleMediaTrackSettingsChange(MediaTrackSettings settings) {
76 if (_settingsController.isClosed) { 107 if (_settingsController.isClosed) {
77 return; 108 return;
@@ -80,6 +111,32 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -80,6 +111,32 @@ class MobileScannerWeb extends MobileScannerPlatform {
80 _settingsController.add(settings); 111 _settingsController.add(settings);
81 } 112 }
82 113
  114 + /// Flip the [videoElement] horizontally,
  115 + /// if the [videoStream] indicates that is facing the user.
  116 + void _maybeFlipVideoPreview(
  117 + HTMLVideoElement videoElement,
  118 + MediaStream videoStream,
  119 + ) {
  120 + final List<MediaStreamTrack> tracks = videoStream.getVideoTracks().toDart;
  121 +
  122 + if (tracks.isEmpty) {
  123 + return;
  124 + }
  125 +
  126 + final MediaStreamTrack videoTrack = tracks.first;
  127 + final MediaTrackCapabilities capabilities = videoTrack.getCapabilities();
  128 +
  129 + // TODO: this is empty on MacOS, where there is no facing mode, but one, user facing camera.
  130 + // Facing mode is not supported by this track, do nothing.
  131 + if (capabilities.facingMode.toDart.isEmpty) {
  132 + return;
  133 + }
  134 +
  135 + if (videoTrack.getSettings().facingMode == 'user') {
  136 + videoElement.style.transform = 'scaleX(-1)';
  137 + }
  138 + }
  139 +
83 /// Prepare a [MediaStream] for the video output. 140 /// Prepare a [MediaStream] for the video output.
84 /// 141 ///
85 /// This method requests permission to use the camera. 142 /// This method requests permission to use the camera.
@@ -168,7 +225,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -168,7 +225,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
168 return const SizedBox(); 225 return const SizedBox();
169 } 226 }
170 227
171 - return const HtmlElementView(viewType: _viewType); 228 + return HtmlElementView(viewType: _getViewType(_textureId));
172 } 229 }
173 230
174 @override 231 @override
@@ -213,18 +270,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -213,18 +270,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
213 alternateScriptUrl: _alternateScriptUrl, 270 alternateScriptUrl: _alternateScriptUrl,
214 ); 271 );
215 272
216 - // Setup the view factory & container element.  
217 - if (_divElement == null) {  
218 - _divElement = (document.createElement('div') as HTMLDivElement)  
219 - ..style.width = '100%'  
220 - ..style.height = '100%';  
221 -  
222 - ui_web.platformViewRegistry.registerViewFactory(  
223 - _viewType,  
224 - (int id) => _divElement!,  
225 - );  
226 - }  
227 -  
228 if (_barcodeReader.isScanning) { 273 if (_barcodeReader.isScanning) {
229 throw const MobileScannerException( 274 throw const MobileScannerException(
230 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 275 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
@@ -251,21 +296,15 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -251,21 +296,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
251 _handleMediaTrackSettingsChange, 296 _handleMediaTrackSettingsChange,
252 ); 297 );
253 298
254 - final HTMLVideoElement videoElement; 299 + _textureId += 1; // Request a new texture.
255 300
256 - // Attach the video element to the DOM, through its parent container.  
257 - // If a video element is already present, reuse it.  
258 - if (_divElement!.children.length == 0) {  
259 - videoElement = document.createElement('video') as HTMLVideoElement; 301 + _videoElement = _createVideoElement(_textureId);
260 302
261 - _divElement!.appendChild(videoElement);  
262 - } else {  
263 - videoElement = _divElement!.children.item(0)! as HTMLVideoElement;  
264 - } 303 + _maybeFlipVideoPreview(_videoElement, videoStream);
265 304
266 await _barcodeReader.start( 305 await _barcodeReader.start(
267 startOptions, 306 startOptions,
268 - videoElement: videoElement, 307 + videoElement: _videoElement,
269 videoStream: videoStream, 308 videoStream: videoStream,
270 ); 309 );
271 } catch (error, stackTrace) { 310 } catch (error, stackTrace) {