diff --git a/feature_parity_table.md b/feature_parity_table.md
index c74213e40..7f01ecc92 100644
--- a/feature_parity_table.md
+++ b/feature_parity_table.md
@@ -59,7 +59,7 @@ Note: LLM means Low Latency Mode.
stay awake | yes (except LLM) | yes | no | no | no | no |
recording active | not yet | yes | no | no | no | no |
playing route | yes (except LLM) | yes | no | no | no | no |
- balance | no | no | no | no | yes | yes |
+ balance | no | no | no | yes | yes | yes |
Streams |
duration event | yes | yes | yes | yes | yes | yes |
position event | yes | yes | yes | yes | yes | yes |
diff --git a/packages/audioplayers_web/lib/web_audio_js.dart b/packages/audioplayers_web/lib/web_audio_js.dart
new file mode 100644
index 000000000..b5589f121
--- /dev/null
+++ b/packages/audioplayers_web/lib/web_audio_js.dart
@@ -0,0 +1,57 @@
+import 'dart:html';
+
+import 'package:js/js.dart';
+
+@JS('AudioContext')
+@staticInterop
+class JsAudioContext {
+ external JsAudioContext();
+}
+
+extension JsAudioContextExtension on JsAudioContext {
+ external MediaElementAudioSourceNode createMediaElementSource(
+ AudioElement element,);
+
+ external StereoPannerNode createStereoPanner();
+
+ external AudioNode get destination;
+}
+
+@JS()
+@staticInterop
+abstract class AudioNode {
+ external AudioNode();
+
+}
+
+extension AudioNodeExtension on AudioNode {
+ external AudioNode connect(AudioNode audioNode);
+}
+
+@JS()
+@staticInterop
+class AudioParam {
+ external AudioParam();
+}
+
+extension AudioParamExtension on AudioParam {
+ external num value;
+}
+
+@JS()
+@staticInterop
+class StereoPannerNode
+ implements AudioNode {
+ external StereoPannerNode();
+}
+
+extension StereoPannerNodeExtension on StereoPannerNode {
+ external AudioParam get pan;
+}
+
+@JS()
+@staticInterop
+class MediaElementAudioSourceNode
+ implements AudioNode {
+ external MediaElementAudioSourceNode();
+}
diff --git a/packages/audioplayers_web/lib/wrapped_player.dart b/packages/audioplayers_web/lib/wrapped_player.dart
index 2bbe632cf..caf552b7f 100644
--- a/packages/audioplayers_web/lib/wrapped_player.dart
+++ b/packages/audioplayers_web/lib/wrapped_player.dart
@@ -4,6 +4,7 @@ import 'dart:html';
import 'package:audioplayers_platform_interface/api/release_mode.dart';
import 'package:audioplayers_platform_interface/streams_interface.dart';
import 'package:audioplayers_web/num_extension.dart';
+import 'package:audioplayers_web/web_audio_js.dart';
class WrappedPlayer {
final String playerId;
@@ -17,6 +18,7 @@ class WrappedPlayer {
bool isPlaying = false;
AudioElement? player;
+ StereoPannerNode? stereoPanner;
StreamSubscription? playerTimeUpdateSubscription;
StreamSubscription? playerEndedSubscription;
StreamSubscription? playerLoadedDataSubscription;
@@ -44,7 +46,7 @@ class WrappedPlayer {
}
void setBalance(double balance) {
- throw UnimplementedError('setBalance is not currently implemented on Web');
+ stereoPanner?.pan.value = balance;
}
void setPlaybackRate(double rate) {
@@ -58,9 +60,18 @@ class WrappedPlayer {
}
final p = player = AudioElement(currentUrl);
+ p.crossOrigin = 'anonymous'; // need for stereo panning to work
p.loop = shouldLoop();
p.volume = currentVolume;
p.playbackRate = currentPlaybackRate;
+
+ // setup stereo panning
+ final audioContext = JsAudioContext();
+ final source = audioContext.createMediaElementSource(player!);
+ stereoPanner = audioContext.createStereoPanner();
+ source.connect(stereoPanner!);
+ stereoPanner?.connect(audioContext.destination);
+
playerPlaySubscription = p.onPlay.listen((_) {
streamsInterface.emitDuration(
playerId,
@@ -99,6 +110,7 @@ class WrappedPlayer {
void release() {
_cancel();
player = null;
+ stereoPanner = null;
playerLoadedDataSubscription?.cancel();
playerLoadedDataSubscription = null;