dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.25k stars 1.58k forks source link

Unable to pass nested maps to javascript methods in html package #42576

Open aksharpatel47 opened 4 years ago

aksharpatel47 commented 4 years ago

While making a QR code reader using Flutter web, I came across a bug where nested maps are not being converted as expected to Javascript dictionaries when calling the methods in the HTML package. Dart version 2.9.0-14.1.beta. The code that I'm using is:

import 'dart:html' as html;
// ... Flutter Code
html.window.navigator.mediaDevices.getUserMedia({
  "audio": false,
  "video": {
    "facingMode": "environment",
  },
}).then(...)

Looking into the implementation for getUserMedia, I found that it is using convertDartToNative_Dictionary to convert the map object passed into getUserMedia into a javascript dictionary.

Below is the implementation of convertDartToNative_Dictionary at sdk/sdk/lib/html/html_common/conversions_dart2js.dart:

/// Converts a flat Dart map into a JavaScript object with properties.
convertDartToNative_Dictionary(Map? dict, [void postCreate(Object? f)?]) {
  if (dict == null) return null;
  var object = JS('var', '{}');
  if (postCreate != null) {
    postCreate(object);
  }
  dict.forEach((key, value) {
    JS('void', '#[#] = #', object, key, value);
  });
  return object;
}

The above function only converts flat dart maps. However, there are many APIs that make use of nested maps. Due to this, the object being passed to getUserMedia is not what is expected. Below is the object that is being passed to the getUserMedia after conversion to Javascript.

{audio: false, video: cf}
audio: false
video: cf {a: 1, b: {…}, c: null, d: null, e: Zs, …}
__proto__: Object

As a result of this, the code to open up the rear camera on mobile devices does not work. The value of o.video.facingMode is undefined. Any constraints passed to the video inside the nested map, are not taken into consideration.

Should the implementation of convertDartToNative_Dictionary be changed to something similar to what is mentioned below to accommodate nested dictionaries?

dict.forEach((key, value) {
  if (value is Map) {
    JS('void', '#[#] = #', object, key, convertDartToNative_Dictionary(value));
  } else {
    JS('void', '#[#] = #', object, key, value);
  }
});
sigmundch commented 4 years ago

Thanks for filing an issue and the careful explanation.

@rakudrama - do you recollect why we generally do a shallow conversion? Should some of these APIs that may contain nested maps switch to use a structured cloning like SerializedScriptValue?

@aksharpatel47 - As a workaround, I believe you can convert the nested JS map on your end. I haven't tested this, but I believe JsObject.jsify({"facingMode": "environment"}) would do the trick, where JsObject is defined in dart:js.

aksharpatel47 commented 4 years ago

@sigmundch Thanks for looking into this. I tried JsObject.jsify({"facingMode": "environment"}). The object created on the javascript end is as below:

{audio: false, video: ff}
audio: false
video: ff { a: {facingMode: "environment", ___dart__$dart_dartObject_ZxYxX_0_: ff } }

An additional object key a is added inside the video object, which in turn has the required object. Due to this, the issue still persists.

sigmundch commented 4 years ago

Oh sorry for a confusion on my end - we have two jsify methods that behave differently. The one in dart:js should eventually get deprecated. There is one in dart:js_util that I believe should work for you. I just tested it out and seems to do the right thing.

import `dart:js_util` as js_util;

... js_util.jsify({"facingMode": "environment"});

If that gives you trouble for any reason, there are other options available, but require more declarations, though. One alternative way with package:js, which I just tried locally and saw didn't contain any additional properties, so this should hopefully work as an option as well:

@JS()
library x;

import 'package:js/js.dart';

@JS()
@anonymous // <-- declares that this will be used as an object literal
class MediaVideoOptions {
  external factory MediaVideoOptions({String facingMode});
}

Then you can write:
```dart
getUserMedia({
  "audio": false,
  "video": MediaVideoOptions(facingMode: "environment"),
  },

The other option is to store the options in a variable, and use dart:js_util to store additional properties via setProperty.

aksharpatel47 commented 4 years ago

Thanks @sigmundch. js_util.jsify({"facingMode": "environment"}) worked. Should the issue stay open in case this should be supported natively in dartlang?