From 30195a437b689cbb99bce81f891d340e2e5e2cd5 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Thu, 17 Feb 2022 23:44:51 +0000 Subject: [PATCH] [dart:html] Fix decodeAudioData to use both syntaxes Closes https://github.com/dart-lang/sdk/issues/47520 decodeAudioData has an older callback-based syntax and newer Promise-based syntax. In order to be consistent with the method signature as well as be able to use both syntaxes, this CL provides an API that can handle both. Change-Id: I875defcfec9e429496a1ac9866f1b53d204eff69 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/221744 Reviewed-by: Riley Porter Reviewed-by: Sigmund Cherem Commit-Queue: Srujan Gaddam --- .../web_audio/dart2js/web_audio_dart2js.dart | 90 +++++++++++++++--- tests/lib/html/audiocontext_test.dart | 70 ++++++++++++++ tests/lib/html/small.mp3 | Bin 0 -> 9146 bytes tests/lib_2/html/audiocontext_test.dart | 73 ++++++++++++++ tests/lib_2/html/small.mp3 | Bin 0 -> 9146 bytes .../html/impl/impl_AudioContext.darttemplate | 90 +++++++++++++++--- 6 files changed, 293 insertions(+), 30 deletions(-) create mode 100644 tests/lib/html/small.mp3 create mode 100644 tests/lib_2/html/small.mp3 diff --git a/sdk/lib/web_audio/dart2js/web_audio_dart2js.dart b/sdk/lib/web_audio/dart2js/web_audio_dart2js.dart index a7db2d566a9..29544d8f98c 100644 --- a/sdk/lib/web_audio/dart2js/web_audio_dart2js.dart +++ b/sdk/lib/web_audio/dart2js/web_audio_dart2js.dart @@ -225,29 +225,89 @@ class AudioContext extends BaseAudioContext { } } - @JSName('decodeAudioData') - Future _decodeAudioData(ByteBuffer audioData, - [DecodeSuccessCallback? successCallback, - DecodeErrorCallback? errorCallback]) native; - Future decodeAudioData(ByteBuffer audioData, [DecodeSuccessCallback? successCallback, DecodeErrorCallback? errorCallback]) { - if (successCallback != null && errorCallback != null) { - return _decodeAudioData(audioData, successCallback, errorCallback); + // Both callbacks need to be provided if they're being used. + assert((successCallback == null) == (errorCallback == null)); + // `decodeAudioData` can exist either in the older callback syntax or the + // newer `Promise`-based syntax that also accepts callbacks. In the former, + // we synthesize a `Future` to be consistent. + // For more details: + // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData + // https://www.w3.org/TR/webaudio/#dom-baseaudiocontext-decodeaudiodata + final completer = Completer(); + var errorInCallbackIsNull = false; + + void success(AudioBuffer decodedData) { + completer.complete(decodedData); + successCallback!.call(decodedData); } - var completer = new Completer(); - _decodeAudioData(audioData, (value) { - completer.complete(value); - }, (error) { - if (error == null) { - completer.completeError(''); + final nullErrorString = + '[AudioContext.decodeAudioData] completed with a null error.'; + + void error(DomException? error) { + // Safari has a bug where it may return null for the error callback. In + // the case where the Safari version still returns a `Promise` and the + // error is not null after the `Promise` is finished, the error callback + // is called instead in the `Promise`'s `catch` block. Otherwise, and in + // the case where a `Promise` is not returned by the API at all, the + // callback never gets called (for backwards compatibility, it can not + // accept null). Instead, the `Future` completes with a custom string, + // indicating that null was given. + // https://github.com/mdn/webaudio-examples/issues/5 + if (error != null) { + // Note that we `complete` and not `completeError`. This is to make sure + // that errors in the `Completer` are not thrown if the call gets back + // a `Promise`. + completer.complete(error); + errorCallback!.call(error); } else { - completer.completeError(error); + completer.complete(nullErrorString); + errorInCallbackIsNull = true; } + } + + var decodeResult; + if (successCallback == null) { + decodeResult = + JS("creates:AudioBuffer;", "#.decodeAudioData(#)", this, audioData); + } else { + decodeResult = JS( + "creates:AudioBuffer;", + "#.decodeAudioData(#, #, #)", + this, + audioData, + convertDartClosureToJS(success, 1), + convertDartClosureToJS(error, 1)); + } + + if (decodeResult != null) { + // Promise-based syntax. + return promiseToFuture(decodeResult).catchError((error) { + // If the error was null in the callback, but no longer is now that the + // `Promise` is finished, call the error callback. If it's still null, + // throw the error string. This is to handle the aforementioned bug in + // Safari. + if (errorInCallbackIsNull) { + if (error != null) { + errorCallback?.call(error); + } else { + throw nullErrorString; + } + } + throw error; + }); + } + + // Callback-based syntax. We use the above completer to synthesize a + // `Future` from the callback values. Since we don't use `completeError` + // above, `then` is used to simulate an error. + return completer.future.then((value) { + if (value is AudioBuffer) return value; + throw value; }); - return completer.future; } } // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file diff --git a/tests/lib/html/audiocontext_test.dart b/tests/lib/html/audiocontext_test.dart index 831613010e9..a091790d810 100644 --- a/tests/lib/html/audiocontext_test.dart +++ b/tests/lib/html/audiocontext_test.dart @@ -7,6 +7,7 @@ import 'dart:html'; import 'dart:typed_data'; import 'dart:web_audio'; +import 'package:async_helper/async_helper.dart'; import 'package:expect/minitest.dart'; main() { @@ -95,5 +96,74 @@ main() { expect(oscillator.type, equals('triangle')); } }); + + asyncTest(() async { + if (AudioContext.supported) { + final audioSourceUrl = "/root_dart/tests/lib/html/small.mp3"; + + Future requestAudioDecode( + {bool triggerDecodeError: false, + DecodeSuccessCallback? successCallback, + DecodeErrorCallback? errorCallback}) async { + HttpRequest audioRequest = HttpRequest(); + audioRequest.open("GET", audioSourceUrl, async: true); + audioRequest.responseType = "arraybuffer"; + var completer = new Completer(); + audioRequest.onLoad.listen((_) { + ByteBuffer audioData = audioRequest.response; + if (triggerDecodeError) audioData = Uint8List.fromList([]).buffer; + context + .decodeAudioData(audioData, successCallback, errorCallback) + .then((_) { + completer.complete(); + }).catchError((e) { + completer.completeError(e); + }); + }); + audioRequest.send(); + return completer.future; + } + + // Decode successfully without callback. + await requestAudioDecode(); + + // Decode successfully with callback. Use counter to make sure it's only + // called once. + var successCallbackCalled = 0; + await requestAudioDecode( + successCallback: (_) { + successCallbackCalled += 1; + }, + errorCallback: (_) {}); + expect(successCallbackCalled, 1); + + // Fail decode without callback. + try { + await requestAudioDecode(triggerDecodeError: true); + fail('Expected decode failure.'); + } catch (_) {} + + // Fail decode with callback. + var errorCallbackCalled = 0; + try { + await requestAudioDecode( + triggerDecodeError: true, + successCallback: (_) {}, + errorCallback: (_) { + errorCallbackCalled += 1; + }); + fail('Expected decode failure.'); + } catch (e) { + // Safari may return a null error. Assuming Safari is version >= 14.1, + // the Future should complete with a string error if the error + // callback never gets called. + if (errorCallbackCalled == 0) { + expect(e is String, true); + } else { + expect(errorCallbackCalled, 1); + } + } + } + }); }); } diff --git a/tests/lib/html/small.mp3 b/tests/lib/html/small.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3fcc88b1aaa22d7786c0695c551116fb78137e8c GIT binary patch literal 9146 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 If1<#D0Y=bI`~Uy| literal 0 HcmV?d00001 diff --git a/tests/lib_2/html/audiocontext_test.dart b/tests/lib_2/html/audiocontext_test.dart index b66e54b293b..396cf39f678 100644 --- a/tests/lib_2/html/audiocontext_test.dart +++ b/tests/lib_2/html/audiocontext_test.dart @@ -1,3 +1,6 @@ +// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. // @dart = 2.9 import 'dart:async'; @@ -5,6 +8,7 @@ import 'dart:html'; import 'dart:typed_data'; import 'dart:web_audio'; +import 'package:async_helper/async_helper.dart'; import 'package:expect/minitest.dart'; main() { @@ -93,5 +97,74 @@ main() { expect(oscillator.type, equals('triangle')); } }); + + asyncTest(() async { + if (AudioContext.supported) { + final audioSourceUrl = "/root_dart/tests/lib_2/html/small.mp3"; + + Future requestAudioDecode( + {bool triggerDecodeError: false, + DecodeSuccessCallback successCallback, + DecodeErrorCallback errorCallback}) async { + HttpRequest audioRequest = HttpRequest(); + audioRequest.open("GET", audioSourceUrl, async: true); + audioRequest.responseType = "arraybuffer"; + var completer = new Completer(); + audioRequest.onLoad.listen((_) { + ByteBuffer audioData = audioRequest.response; + if (triggerDecodeError) audioData = Uint8List.fromList([]).buffer; + context + .decodeAudioData(audioData, successCallback, errorCallback) + .then((_) { + completer.complete(); + }).catchError((e) { + completer.completeError(e); + }); + }); + audioRequest.send(); + return completer.future; + } + + // Decode successfully without callback. + await requestAudioDecode(); + + // Decode successfully with callback. Use counter to make sure it's only + // called once. + var successCallbackCalled = 0; + await requestAudioDecode( + successCallback: (_) { + successCallbackCalled += 1; + }, + errorCallback: (_) {}); + expect(successCallbackCalled, 1); + + // Fail decode without callback. + try { + await requestAudioDecode(triggerDecodeError: true); + fail('Expected decode failure.'); + } catch (_) {} + + // Fail decode with callback. + var errorCallbackCalled = 0; + try { + await requestAudioDecode( + triggerDecodeError: true, + successCallback: (_) {}, + errorCallback: (_) { + errorCallbackCalled += 1; + }); + fail('Expected decode failure.'); + } catch (e) { + // Safari may return a null error. Assuming Safari is version >= 14.1, + // the Future should complete with a string error if the error + // callback never gets called. + if (errorCallbackCalled == 0) { + expect(e is String, true); + } else { + expect(errorCallbackCalled, 1); + } + } + } + }); }); } diff --git a/tests/lib_2/html/small.mp3 b/tests/lib_2/html/small.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3fcc88b1aaa22d7786c0695c551116fb78137e8c GIT binary patch literal 9146 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 If1<#D0Y=bI`~Uy| literal 0 HcmV?d00001 diff --git a/tools/dom/templates/html/impl/impl_AudioContext.darttemplate b/tools/dom/templates/html/impl/impl_AudioContext.darttemplate index d716afd113d..77f860d4c53 100644 --- a/tools/dom/templates/html/impl/impl_AudioContext.darttemplate +++ b/tools/dom/templates/html/impl/impl_AudioContext.darttemplate @@ -41,28 +41,88 @@ $!MEMBERS } } - @JSName('decodeAudioData') - Future$#NULLSAFECAST() _decodeAudioData(ByteBuffer audioData, - [DecodeSuccessCallback$NULLABLE successCallback, - DecodeErrorCallback$NULLABLE errorCallback]) native; - Future decodeAudioData(ByteBuffer audioData, [DecodeSuccessCallback$NULLABLE successCallback, DecodeErrorCallback$NULLABLE errorCallback]) { - if (successCallback != null && errorCallback != null) { - return _decodeAudioData(audioData, successCallback, errorCallback); + // Both callbacks need to be provided if they're being used. + assert((successCallback == null) == (errorCallback == null)); + // `decodeAudioData` can exist either in the older callback syntax or the + // newer `Promise`-based syntax that also accepts callbacks. In the former, + // we synthesize a `Future` to be consistent. + // For more details: + // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData + // https://www.w3.org/TR/webaudio/#dom-baseaudiocontext-decodeaudiodata + final completer = Completer(); + var errorInCallbackIsNull = false; + + void success(AudioBuffer decodedData) { + completer.complete(decodedData); + successCallback$NULLASSERT.call(decodedData); } - var completer = new Completer(); - _decodeAudioData(audioData, (value) { - completer.complete(value); - }, (error) { - if (error == null) { - completer.completeError(''); + final nullErrorString = + '[AudioContext.decodeAudioData] completed with a null error.'; + + void error(DomException$NULLABLE error) { + // Safari has a bug where it may return null for the error callback. In + // the case where the Safari version still returns a `Promise` and the + // error is not null after the `Promise` is finished, the error callback + // is called instead in the `Promise`'s `catch` block. Otherwise, and in + // the case where a `Promise` is not returned by the API at all, the + // callback never gets called (for backwards compatibility, it can not + // accept null). Instead, the `Future` completes with a custom string, + // indicating that null was given. + // https://github.com/mdn/webaudio-examples/issues/5 + if (error != null) { + // Note that we `complete` and not `completeError`. This is to make sure + // that errors in the `Completer` are not thrown if the call gets back + // a `Promise`. + completer.complete(error); + errorCallback$NULLASSERT.call(error); } else { - completer.completeError(error); + completer.complete(nullErrorString); + errorInCallbackIsNull = true; } + } + + var decodeResult; + if (successCallback == null) { + decodeResult = + JS("creates:AudioBuffer;", "#.decodeAudioData(#)", this, audioData); + } else { + decodeResult = JS( + "creates:AudioBuffer;", + "#.decodeAudioData(#, #, #)", + this, + audioData, + convertDartClosureToJS(success, 1), + convertDartClosureToJS(error, 1)); + } + + if (decodeResult != null) { + // Promise-based syntax. + return promiseToFuture(decodeResult).catchError((error) { + // If the error was null in the callback, but no longer is now that the + // `Promise` is finished, call the error callback. If it's still null, + // throw the error string. This is to handle the aforementioned bug in + // Safari. + if (errorInCallbackIsNull) { + if (error != null) { + errorCallback?.call(error); + } else { + throw nullErrorString; + } + } + throw error; + }); + } + + // Callback-based syntax. We use the above completer to synthesize a + // `Future` from the callback values. Since we don't use `completeError` + // above, `then` is used to simulate an error. + return completer.future.then((value) { + if (value is AudioBuffer) return value; + throw value; }); - return completer.future; } }