[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 <rileyporter@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Srujan Gaddam <srujzs@google.com>
This commit is contained in:
Srujan Gaddam 2022-02-17 23:44:51 +00:00 committed by Commit Bot
parent fee5309e61
commit 30195a437b
6 changed files with 293 additions and 30 deletions

View file

@ -225,29 +225,89 @@ class AudioContext extends BaseAudioContext {
}
}
@JSName('decodeAudioData')
Future<AudioBuffer> _decodeAudioData(ByteBuffer audioData,
[DecodeSuccessCallback? successCallback,
DecodeErrorCallback? errorCallback]) native;
Future<AudioBuffer> 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<Object>();
var errorInCallbackIsNull = false;
void success(AudioBuffer decodedData) {
completer.complete(decodedData);
successCallback!.call(decodedData);
}
var completer = new Completer<AudioBuffer>();
_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<AudioBuffer>(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

View file

@ -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<void> 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<void>();
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);
}
}
}
});
});
}

BIN
tests/lib/html/small.mp3 Normal file

Binary file not shown.

View file

@ -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<void> 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<void>();
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);
}
}
}
});
});
}

BIN
tests/lib_2/html/small.mp3 Normal file

Binary file not shown.

View file

@ -41,28 +41,88 @@ $!MEMBERS
}
}
@JSName('decodeAudioData')
Future$#NULLSAFECAST(<AudioBuffer>) _decodeAudioData(ByteBuffer audioData,
[DecodeSuccessCallback$NULLABLE successCallback,
DecodeErrorCallback$NULLABLE errorCallback]) native;
Future<AudioBuffer> 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<Object>();
var errorInCallbackIsNull = false;
void success(AudioBuffer decodedData) {
completer.complete(decodedData);
successCallback$NULLASSERT.call(decodedData);
}
var completer = new Completer<AudioBuffer>();
_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<AudioBuffer>(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;
}
}