mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Migrate Flutter Web DOM usage to JS static interop - 9. (flutter/engine#33155)
This commit is contained in:
parent
9276dc1ff1
commit
a011438306
@ -3,11 +3,11 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../dom.dart';
|
||||
import '../html_image_codec.dart';
|
||||
import '../safe_browser_api.dart';
|
||||
import '../util.dart';
|
||||
@ -61,7 +61,7 @@ void skiaDecodeImageFromPixels(
|
||||
);
|
||||
|
||||
if (skImage == null) {
|
||||
html.window.console.warn('Failed to create image from pixels.');
|
||||
domWindow.console.warn('Failed to create image from pixels.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -82,11 +82,11 @@ class ImageCodecException implements Exception {
|
||||
|
||||
const String _kNetworkImageMessage = 'Failed to load network image.';
|
||||
|
||||
typedef HttpRequestFactory = html.HttpRequest Function();
|
||||
typedef HttpRequestFactory = DomXMLHttpRequest Function();
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
HttpRequestFactory httpRequestFactory = () => html.HttpRequest();
|
||||
HttpRequestFactory httpRequestFactory = () => createDomXMLHttpRequest();
|
||||
void debugRestoreHttpRequestFactory() {
|
||||
httpRequestFactory = () => html.HttpRequest();
|
||||
httpRequestFactory = () => createDomXMLHttpRequest();
|
||||
}
|
||||
|
||||
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
|
||||
@ -106,23 +106,24 @@ Future<Uint8List> fetchImage(
|
||||
String url, WebOnlyImageCodecChunkCallback? chunkCallback) {
|
||||
final Completer<Uint8List> completer = Completer<Uint8List>();
|
||||
|
||||
final html.HttpRequest request = httpRequestFactory();
|
||||
request.open('GET', url, async: true);
|
||||
final DomXMLHttpRequest request = httpRequestFactory();
|
||||
request.open('GET', url, true);
|
||||
request.responseType = 'arraybuffer';
|
||||
if (chunkCallback != null) {
|
||||
request.onProgress.listen((html.ProgressEvent event) {
|
||||
request.addEventListener('progress', allowInterop((DomEvent event) {
|
||||
event = event as DomProgressEvent;
|
||||
chunkCallback.call(event.loaded!, event.total!);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
request.onError.listen((html.ProgressEvent event) {
|
||||
request.addEventListener('error', allowInterop((DomEvent event) {
|
||||
completer.completeError(ImageCodecException('$_kNetworkImageMessage\n'
|
||||
'Image URL: $url\n'
|
||||
'Trying to load an image from another domain? Find answers at:\n'
|
||||
'https://flutter.dev/docs/development/platform-integration/web-images'));
|
||||
});
|
||||
}));
|
||||
|
||||
request.onLoad.listen((html.ProgressEvent event) {
|
||||
request.addEventListener('load', allowInterop((DomEvent event) {
|
||||
final int status = request.status!;
|
||||
final bool accepted = status >= 200 && status < 300;
|
||||
final bool fileUri = status == 0; // file:// URIs have status of 0.
|
||||
@ -140,7 +141,7 @@ Future<Uint8List> fetchImage(
|
||||
}
|
||||
|
||||
completer.complete(Uint8List.view(request.response as ByteBuffer));
|
||||
});
|
||||
}));
|
||||
|
||||
request.send();
|
||||
return completer.future;
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show base64;
|
||||
import 'dart:html' as html;
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
@ -18,6 +17,7 @@ import 'package:meta/meta.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../alarm_clock.dart';
|
||||
import '../dom.dart';
|
||||
import '../safe_browser_api.dart';
|
||||
import '../util.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
@ -202,8 +202,8 @@ class CkBrowserImageDecoder implements ui.Codec {
|
||||
|
||||
return webDecoder;
|
||||
} catch (error) {
|
||||
if (error is html.DomException) {
|
||||
if (error.name == html.DomException.NOT_SUPPORTED) {
|
||||
if (domInstanceOfString(error, 'DOMException')) {
|
||||
if ((error as DomException).name == DomException.notSupported) {
|
||||
throw ImageCodecException(
|
||||
'Image file format ($contentType) is not supported by this browser\'s ImageDecoder API.\n'
|
||||
'Image source: $debugSource',
|
||||
@ -455,11 +455,10 @@ Future<ByteBuffer> readVideoFramePixelsUnmodified(VideoFrame videoFrame) async {
|
||||
Future<Uint8List> encodeVideoFrameAsPng(VideoFrame videoFrame) async {
|
||||
final int width = videoFrame.displayWidth;
|
||||
final int height = videoFrame.displayHeight;
|
||||
final html.CanvasElement canvas = html.CanvasElement()
|
||||
..width = width
|
||||
..height = height;
|
||||
final html.CanvasRenderingContext2D ctx = canvas.context2D;
|
||||
ctx.drawImage(videoFrame, 0, 0);
|
||||
final String pngBase64 = canvas.toDataUrl().substring('data:image/png;base64,'.length);
|
||||
final DomCanvasElement canvas = createDomCanvasElement(width: width, height:
|
||||
height);
|
||||
final DomCanvasRenderingContext2D ctx = canvas.getContext2D;
|
||||
ctx.drawImage(videoFrame as DomCanvasImageSource, 0, 0);
|
||||
final String pngBase64 = canvas.toDataURL().substring('data:image/png;base64,'.length);
|
||||
return base64.decode(pngBase64);
|
||||
}
|
||||
|
||||
@ -19,11 +19,20 @@ import 'package:js/js_util.dart' as js_util;
|
||||
class DomWindow {}
|
||||
|
||||
extension DomWindowExtension on DomWindow {
|
||||
external DomConsole get console;
|
||||
external DomDocument get document;
|
||||
external DomNavigator get navigator;
|
||||
external DomPerformance get performance;
|
||||
Future<Object?> fetch(String url) =>
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'fetch', <String>[url]));
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'fetch', <String>[url]));
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomConsole {}
|
||||
|
||||
extension DomConsoleExtension on DomConsole {
|
||||
external void warn(Object? arg);
|
||||
}
|
||||
|
||||
@JS('window')
|
||||
@ -88,6 +97,15 @@ extension DomEventExtension on DomEvent {
|
||||
external void stopPropagation();
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomProgressEvent extends DomEvent {}
|
||||
|
||||
extension DomProgressEventExtension on DomProgressEvent {
|
||||
external int? get loaded;
|
||||
external int? get total;
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomNode extends DomEventTarget {}
|
||||
@ -225,6 +243,7 @@ extension DomCanvasElementExtension on DomCanvasElement {
|
||||
external set width(int? value);
|
||||
external int? get height;
|
||||
external set height(int? value);
|
||||
external String toDataURL([String? type]);
|
||||
|
||||
Object? getContext(String contextType, [Map<dynamic, dynamic>? attributes]) {
|
||||
return js_util.callMethod(this, 'getContext', <Object?>[
|
||||
@ -232,25 +251,79 @@ extension DomCanvasElementExtension on DomCanvasElement {
|
||||
if (attributes != null) js_util.jsify(attributes)
|
||||
]);
|
||||
}
|
||||
|
||||
DomCanvasRenderingContext2D get getContext2D =>
|
||||
getContext('2d')! as DomCanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
abstract class DomCanvasImageSource {}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomCanvasRenderingContext2D {}
|
||||
|
||||
extension DomCanvasRenderingContext2DExtension on DomCanvasRenderingContext2D {
|
||||
external void drawImage(DomCanvasImageSource source, num destX, num destY);
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomXMLHttpRequestEventTarget extends DomEventTarget {}
|
||||
|
||||
@JS('XMLHttpRequest')
|
||||
@staticInterop
|
||||
class DomXMLHttpRequest extends DomXMLHttpRequestEventTarget {}
|
||||
|
||||
DomXMLHttpRequest createDomXMLHttpRequest() =>
|
||||
domCallConstructorString('XMLHttpRequest', <Object?>[])!
|
||||
as DomXMLHttpRequest;
|
||||
|
||||
extension DomXMLHttpRequestExtension on DomXMLHttpRequest {
|
||||
external dynamic get response;
|
||||
external String get responseType;
|
||||
external int? get status;
|
||||
external set responseType(String value);
|
||||
external void open(String method, String url, [bool? async]);
|
||||
external void send();
|
||||
}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomResponse {}
|
||||
|
||||
@JS()
|
||||
@staticInterop
|
||||
class DomException {
|
||||
static const String notSupported = 'NotSupportedError';
|
||||
}
|
||||
|
||||
extension DomExceptionExtension on DomException {
|
||||
external String get name;
|
||||
}
|
||||
|
||||
extension DomResponseExtension on DomResponse {
|
||||
Future<dynamic> arrayBuffer() =>
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'arrayBuffer', <Object>[]));
|
||||
Future<dynamic> arrayBuffer() => js_util
|
||||
.promiseToFuture(js_util.callMethod(this, 'arrayBuffer', <Object>[]));
|
||||
|
||||
Future<dynamic> json() =>
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'json', <Object>[]));
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'json', <Object>[]));
|
||||
|
||||
Future<String> text() =>
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'text', <Object>[]));
|
||||
js_util.promiseToFuture(js_util.callMethod(this, 'text', <Object>[]));
|
||||
}
|
||||
|
||||
Object? domGetConstructor(String constructorName) =>
|
||||
js_util.getProperty(domWindow, constructorName);
|
||||
|
||||
Object? domCallConstructorString(String constructorName, List<Object?> args) {
|
||||
final Object? constructor = domGetConstructor(constructorName);
|
||||
if (constructor == null) {
|
||||
return null;
|
||||
}
|
||||
return js_util.callConstructor(constructor, args);
|
||||
}
|
||||
|
||||
bool domInstanceOfString(Object? element, String objectType) =>
|
||||
js_util.instanceof(element, domGetConstructor(objectType)!);
|
||||
|
||||
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:html' as html;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
@ -201,15 +202,15 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
|
||||
test('skiaInstantiateWebImageCodec loads an image from the network',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
final TestHttpRequestMock mock = TestHttpRequestMock()
|
||||
..status = 200
|
||||
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test progress event'),
|
||||
])
|
||||
..response = kTransparentImage.buffer;
|
||||
};
|
||||
final ui.Codec codec = await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null);
|
||||
httpRequestFactory = () => TestHttpRequest(mock);
|
||||
final Future<ui.Codec> futureCodec =
|
||||
skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg',
|
||||
null);
|
||||
mock.sendEvent('load', DomProgressEvent());
|
||||
final ui.Codec codec = await futureCodec;
|
||||
expect(codec.frameCount, 1);
|
||||
final ui.Image image = (await codec.getNextFrame()).image;
|
||||
expect(image.height, 1);
|
||||
@ -251,14 +252,13 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
|
||||
test('skiaInstantiateWebImageCodec throws exception on request error',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
..onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test error'),
|
||||
]);
|
||||
};
|
||||
final TestHttpRequestMock mock = TestHttpRequestMock();
|
||||
httpRequestFactory = () => TestHttpRequest(mock);
|
||||
try {
|
||||
await skiaInstantiateWebImageCodec('url-does-not-matter', null);
|
||||
final Future<ui.Codec> futureCodec = skiaInstantiateWebImageCodec(
|
||||
'url-does-not-matter', null);
|
||||
mock.sendEvent('error', DomProgressEvent());
|
||||
await futureCodec;
|
||||
fail('Expected to throw');
|
||||
} on ImageCodecException catch (exception) {
|
||||
expect(
|
||||
@ -290,16 +290,15 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
|
||||
test('skiaInstantiateWebImageCodec includes URL in the error for malformed image',
|
||||
() async {
|
||||
httpRequestFactory = () {
|
||||
return TestHttpRequest()
|
||||
final TestHttpRequestMock mock = TestHttpRequestMock()
|
||||
..status = 200
|
||||
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
|
||||
html.ProgressEvent('test progress event'),
|
||||
])
|
||||
..response = Uint8List(0).buffer;
|
||||
};
|
||||
httpRequestFactory = () => TestHttpRequest(mock);
|
||||
try {
|
||||
await skiaInstantiateWebImageCodec('http://image-server.com/picture.jpg', null);
|
||||
final Future<ui.Codec> futureCodec = skiaInstantiateWebImageCodec(
|
||||
'http://image-server.com/picture.jpg', null);
|
||||
mock.sendEvent('load', DomProgressEvent());
|
||||
await futureCodec;
|
||||
fail('Expected to throw');
|
||||
} on ImageCodecException catch (exception) {
|
||||
if (!browserSupportsImageDecoder) {
|
||||
@ -702,116 +701,53 @@ void _testCkBrowserImageDecoder() {
|
||||
});
|
||||
}
|
||||
|
||||
class TestHttpRequest implements html.HttpRequest {
|
||||
@override
|
||||
class TestHttpRequestMock {
|
||||
String responseType = 'invalid';
|
||||
|
||||
@override
|
||||
int? timeout = 10;
|
||||
|
||||
@override
|
||||
bool? withCredentials = false;
|
||||
|
||||
@override
|
||||
void abort() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void addEventListener(String type, html.EventListener? listener, [bool? useCapture]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool dispatchEvent(html.Event event) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String getAllResponseHeaders() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String getResponseHeader(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
html.Events get on => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onAbort => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onLoadEnd => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onLoadStart => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onProgress => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.Event> get onReadyStateChange => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Stream<html.ProgressEvent> get onTimeout => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void open(String method, String url, {bool? async, String? user, String? password}) {}
|
||||
|
||||
@override
|
||||
void overrideMimeType(String mime) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get readyState => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void removeEventListener(String type, html.EventListener? listener, [bool? useCapture]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int timeout = 10;
|
||||
bool withCredentials = false;
|
||||
dynamic response;
|
||||
|
||||
@override
|
||||
Map<String, String> get responseHeaders => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get responseText => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
String get responseUrl => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
html.Document get responseXml => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void send([dynamic bodyOrData]) {
|
||||
}
|
||||
|
||||
@override
|
||||
void setRequestHeader(String name, String value) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int status = -1;
|
||||
Map<String, DomEventListener> listeners = <String, DomEventListener>{};
|
||||
|
||||
@override
|
||||
String get statusText => throw UnimplementedError();
|
||||
void open(String method, String url, [bool? async]) {}
|
||||
void send() {}
|
||||
void addEventListener(String eventType, DomEventListener listener, [bool?
|
||||
useCapture]) =>
|
||||
listeners[eventType] = listener;
|
||||
|
||||
@override
|
||||
html.HttpRequestUpload get upload => throw UnimplementedError();
|
||||
void sendEvent(String eventType, DomProgressEvent event) =>
|
||||
listeners[eventType]!(event);
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
@staticInterop
|
||||
class TestHttpRequest implements DomXMLHttpRequest {
|
||||
factory TestHttpRequest(TestHttpRequestMock mock) {
|
||||
return TestHttpRequest._(
|
||||
responseType: mock.responseType,
|
||||
timeout: mock.timeout,
|
||||
withCredentials: mock.withCredentials,
|
||||
response: mock.response,
|
||||
status: mock.status,
|
||||
open: allowInterop((String method, String url, [bool? async]) =>
|
||||
mock.open(method, url, async)),
|
||||
send: allowInterop(() => mock.send()),
|
||||
addEventListener: allowInterop((String eventType, DomEventListener
|
||||
listener, [bool? useCapture]) =>
|
||||
mock.addEventListener(eventType, listener, useCapture)));
|
||||
}
|
||||
|
||||
external factory TestHttpRequest._({
|
||||
String responseType,
|
||||
int timeout,
|
||||
bool withCredentials,
|
||||
dynamic response,
|
||||
int status,
|
||||
void Function(String method, String url, [bool? async]) open,
|
||||
void Function() send,
|
||||
void Function(String eventType, DomEventListener listener) addEventListener
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> expectFrameData(ui.FrameInfo frame, List<int> data) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user