From 1fecd5ac1f468eed774d1c481600b2fbff3d8ba3 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Mon, 18 Jan 2021 20:24:57 +1100 Subject: [PATCH] Visualizer implementation for Android. --- .../com/ryanheise/just_audio/AudioPlayer.java | 120 ++++++++++++++---- .../just_audio/BetterEventChannel.java | 39 ++++++ .../just_audio/BetterVisualizer.java | 86 +++++++++++++ .../ryanheise/just_audio/JustAudioPlugin.java | 23 +++- .../just_audio/MainMethodCallHandler.java | 16 ++- .../android/app/src/main/AndroidManifest.xml | 2 + just_audio/example/pubspec.lock | 12 +- just_audio/lib/just_audio.dart | 81 ++++++++++++ just_audio/pubspec.lock | 12 +- just_audio/pubspec.yaml | 8 +- .../lib/just_audio_platform_interface.dart | 70 ++++++++++ .../lib/method_channel_just_audio.dart | 27 ++++ just_audio_web/pubspec.yaml | 4 +- 13 files changed, 457 insertions(+), 43 deletions(-) create mode 100644 just_audio/android/src/main/java/com/ryanheise/just_audio/BetterEventChannel.java create mode 100644 just_audio/android/src/main/java/com/ryanheise/just_audio/BetterVisualizer.java diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java index e47a854..9a0fc6c 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/AudioPlayer.java @@ -1,8 +1,14 @@ package com.ryanheise.just_audio; +import android.Manifest; +import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; +import android.media.audiofx.Visualizer; import android.net.Uri; import android.os.Handler; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; @@ -35,13 +41,13 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import io.flutter.Log; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.EventChannel.EventSink; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -50,7 +56,7 @@ import java.util.List; import java.util.Map; import java.util.Random; -public class AudioPlayer implements MethodCallHandler, Player.EventListener, AudioListener, MetadataOutput { +public class AudioPlayer implements MethodCallHandler, Player.EventListener, AudioListener, MetadataOutput, PluginRegistry.RequestPermissionsResultListener { static final String TAG = "AudioPlayer"; @@ -58,8 +64,8 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud private final Context context; private final MethodChannel methodChannel; - private final EventChannel eventChannel; - private EventSink eventSink; + private final BetterEventChannel eventChannel; + private ActivityPluginBinding activityPluginBinding; private ProcessingState processingState; private long bufferedPosition; @@ -77,6 +83,12 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud private IcyHeaders icyHeaders; private int errorCount; private AudioAttributes pendingAudioAttributes; + private BetterVisualizer visualizer; + private Result startVisualizerResult; + private boolean enableWaveform; + private boolean enableFft; + private Integer visualizerCaptureRate; + private Integer visualizerCaptureSize; private SimpleExoPlayer player; private Integer audioSessionId; @@ -114,26 +126,50 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud this.context = applicationContext; methodChannel = new MethodChannel(messenger, "com.ryanheise.just_audio.methods." + id); methodChannel.setMethodCallHandler(this); - eventChannel = new EventChannel(messenger, "com.ryanheise.just_audio.events." + id); - eventChannel.setStreamHandler(new EventChannel.StreamHandler() { - @Override - public void onListen(final Object arguments, final EventSink eventSink) { - AudioPlayer.this.eventSink = eventSink; - } - - @Override - public void onCancel(final Object arguments) { - eventSink = null; - } - }); + eventChannel = new BetterEventChannel(messenger, "com.ryanheise.just_audio.events." + id); + visualizer = new BetterVisualizer(messenger, id); processingState = ProcessingState.none; } + private void requestPermissions() { + ActivityCompat.requestPermissions(activityPluginBinding.getActivity(), new String[] { Manifest.permission.RECORD_AUDIO }, 1); + } + + public void setActivityPluginBinding(ActivityPluginBinding activityPluginBinding) { + if (this.activityPluginBinding != null && this.activityPluginBinding != activityPluginBinding) { + this.activityPluginBinding.removeRequestPermissionsResultListener(this); + } + this.activityPluginBinding = activityPluginBinding; + if (activityPluginBinding != null) { + activityPluginBinding.addRequestPermissionsResultListener(this); + // If there is a pending startVisualizer request + if (startVisualizerResult != null) { + requestPermissions(); + } + } + } + private void startWatchingBuffer() { handler.removeCallbacks(bufferWatcher); handler.post(bufferWatcher); } + @Override + public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (int i = 0; i < permissions.length; i++) { + if (permissions[i].equals(Manifest.permission.RECORD_AUDIO)) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + visualizer.setHasPermission(true); + completeStartVisualizer(true); + return true; + } + completeStartVisualizer(false); + break; + } + } + return false; + } + @Override public void onAudioSessionId(int audioSessionId) { if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { @@ -141,6 +177,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud } else { this.audioSessionId = audioSessionId; } + visualizer.onAudioSessionId(this.audioSessionId); } @Override @@ -277,12 +314,48 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud seekResult = null; } + private void completeStartVisualizer(boolean success) { + if (startVisualizerResult == null) return; + if (success) { + visualizer.start(visualizerCaptureRate, visualizerCaptureSize, enableWaveform, enableFft); + Map resultMap = new HashMap(); + resultMap.put("samplingRate", visualizer.getSamplingRate()); + startVisualizerResult.success(resultMap); + } else { + startVisualizerResult.error("RECORD_AUDIO permission denied", null, null); + } + startVisualizerResult = null; + } + @Override public void onMethodCall(final MethodCall call, final Result result) { ensurePlayerInitialized(); try { switch (call.method) { + case "startVisualizer": + Boolean enableWaveform = call.argument("enableWaveform"); + Boolean enableFft = call.argument("enableFft"); + Integer captureRate = call.argument("captureRate"); + Integer captureSize = call.argument("captureSize"); + this.enableWaveform = enableWaveform; + this.enableFft = enableFft; + visualizerCaptureRate = captureRate; + visualizerCaptureSize = captureSize; + startVisualizerResult = result; + if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + visualizer.setHasPermission(true); + completeStartVisualizer(true); + } else if (activityPluginBinding != null && activityPluginBinding.getActivity() != null) { + requestPermissions(); + } else { + // Will request permission in setActivityPluginBinding + } + break; + case "stopVisualizer": + visualizer.stop(); + result.success(new HashMap()); + break; case "load": Long initialPosition = getLong(call.argument("initialPosition")); Integer initialIndex = call.argument("initialIndex"); @@ -565,9 +638,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud event.put("currentIndex", currentIndex); event.put("androidAudioSessionId", audioSessionId); - if (eventSink != null) { - eventSink.success(event); - } + eventChannel.success(event); } private Map collectIcyMetadata() { @@ -615,9 +686,7 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud prepareResult = null; } - if (eventSink != null) { - eventSink.error(errorCode, errorMsg, null); - } + eventChannel.error(errorCode, errorMsg, null); } private void transition(final ProcessingState newState) { @@ -706,9 +775,8 @@ public class AudioPlayer implements MethodCallHandler, Player.EventListener, Aud player = null; transition(ProcessingState.none); } - if (eventSink != null) { - eventSink.endOfStream(); - } + eventChannel.endOfStream(); + visualizer.dispose(); } private void abortSeek() { diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterEventChannel.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterEventChannel.java new file mode 100644 index 0000000..2fdb443 --- /dev/null +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterEventChannel.java @@ -0,0 +1,39 @@ +package com.ryanheise.just_audio; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.EventChannel.EventSink; + +public class BetterEventChannel implements EventSink { + private EventSink eventSink; + + public BetterEventChannel(final BinaryMessenger messenger, final String id) { + EventChannel eventChannel = new EventChannel(messenger, id); + eventChannel.setStreamHandler(new EventChannel.StreamHandler() { + @Override + public void onListen(final Object arguments, final EventSink eventSink) { + BetterEventChannel.this.eventSink = eventSink; + } + + @Override + public void onCancel(final Object arguments) { + eventSink = null; + } + }); + } + + @Override + public void success(Object event) { + if (eventSink != null) eventSink.success(event); + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + if (eventSink != null) eventSink.error(errorCode, errorMessage, errorDetails); + } + + @Override + public void endOfStream() { + if (eventSink != null) eventSink.endOfStream(); + } +} diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterVisualizer.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterVisualizer.java new file mode 100644 index 0000000..af274e6 --- /dev/null +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/BetterVisualizer.java @@ -0,0 +1,86 @@ +package com.ryanheise.just_audio; + +import android.media.audiofx.Visualizer; +import io.flutter.plugin.common.BinaryMessenger; + +public class BetterVisualizer { + private Visualizer visualizer; + private final BetterEventChannel waveformEventChannel; + private final BetterEventChannel fftEventChannel; + private Integer audioSessionId; + private int captureRate; + private int captureSize; + private boolean enableWaveform; + private boolean enableFft; + private boolean pendingStartRequest; + private boolean hasPermission; + + public BetterVisualizer(final BinaryMessenger messenger, String id) { + waveformEventChannel = new BetterEventChannel(messenger, "com.ryanheise.just_audio.waveform_events." + id); + fftEventChannel = new BetterEventChannel(messenger, "com.ryanheise.just_audio.fft_events." + id); + } + + public int getSamplingRate() { + return visualizer.getSamplingRate(); + } + + public void setHasPermission(boolean hasPermission) { + this.hasPermission = hasPermission; + } + + public void onAudioSessionId(Integer audioSessionId) { + this.audioSessionId = audioSessionId; + if (audioSessionId != null && hasPermission && pendingStartRequest) { + start(captureRate, captureSize, enableWaveform, enableFft); + } + } + + public void start(Integer captureRate, Integer captureSize, final boolean enableWavefrom, final boolean enableFft) { + if (visualizer != null) return; + if (captureRate == null) { + captureRate = Visualizer.getMaxCaptureRate() / 2; + } else if (captureRate > Visualizer.getMaxCaptureRate()) { + captureRate = Visualizer.getMaxCaptureRate(); + } + if (captureSize == null) { + captureSize = Visualizer.getCaptureSizeRange()[1]; + } else if (captureSize > Visualizer.getCaptureSizeRange()[1]) { + captureSize = Visualizer.getCaptureSizeRange()[1]; + } else if (captureSize < Visualizer.getCaptureSizeRange()[0]) { + captureSize = Visualizer.getCaptureSizeRange()[0]; + } + this.enableWaveform = enableWaveform; + this.enableFft = enableFft; + this.captureRate = captureRate; + if (audioSessionId == null || !hasPermission) { + pendingStartRequest = true; + return; + } + pendingStartRequest = false; + visualizer = new Visualizer(audioSessionId); + visualizer.setCaptureSize(captureSize); + visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() { + public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) { + waveformEventChannel.success(waveform); + } + public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) { + fftEventChannel.success(fft); + } + }, captureRate, enableWavefrom, enableFft); + visualizer.setEnabled(true); + } + + public void stop() { + if (visualizer == null) return; + visualizer.setDataCaptureListener(null, captureRate, enableWaveform, enableFft); + visualizer.setEnabled(false); + visualizer.release(); + visualizer = null; + } + + public void dispose() { + stop(); + waveformEventChannel.endOfStream(); + fftEventChannel.endOfStream(); + } +} diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/JustAudioPlugin.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/JustAudioPlugin.java index 52dbbb6..90aae5c 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/JustAudioPlugin.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/JustAudioPlugin.java @@ -3,6 +3,8 @@ package com.ryanheise.just_audio; import android.content.Context; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry.Registrar; @@ -10,7 +12,7 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; /** * JustAudioPlugin */ -public class JustAudioPlugin implements FlutterPlugin { +public class JustAudioPlugin implements FlutterPlugin, ActivityAware { private MethodChannel channel; private MainMethodCallHandler methodCallHandler; @@ -41,6 +43,25 @@ public class JustAudioPlugin implements FlutterPlugin { stopListening(); } + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + methodCallHandler.setActivityPluginBinding(binding); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + methodCallHandler.setActivityPluginBinding(binding); + } + + @Override + public void onDetachedFromActivity() { + methodCallHandler.setActivityPluginBinding(null); + } + private void startListening(Context applicationContext, BinaryMessenger messenger) { methodCallHandler = new MainMethodCallHandler(applicationContext, messenger); diff --git a/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java b/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java index 5e3064d..b4146de 100644 --- a/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java +++ b/just_audio/android/src/main/java/com/ryanheise/just_audio/MainMethodCallHandler.java @@ -1,7 +1,9 @@ package com.ryanheise.just_audio; +import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -15,6 +17,7 @@ public class MainMethodCallHandler implements MethodCallHandler { private final Context applicationContext; private final BinaryMessenger messenger; + private ActivityPluginBinding activityPluginBinding; private final Map players = new HashMap<>(); @@ -24,6 +27,13 @@ public class MainMethodCallHandler implements MethodCallHandler { this.messenger = messenger; } + void setActivityPluginBinding(ActivityPluginBinding activityPluginBinding) { + this.activityPluginBinding = activityPluginBinding; + for (AudioPlayer player : players.values()) { + player.setActivityPluginBinding(activityPluginBinding); + } + } + @Override public void onMethodCall(MethodCall call, @NonNull Result result) { final Map request = call.arguments(); @@ -34,7 +44,11 @@ public class MainMethodCallHandler implements MethodCallHandler { result.error("Platform player " + id + " already exists", null, null); break; } - players.put(id, new AudioPlayer(applicationContext, messenger, id)); + final AudioPlayer player = new AudioPlayer(applicationContext, messenger, id); + players.put(id, player); + if (activityPluginBinding != null) { + player.setActivityPluginBinding(activityPluginBinding); + } result.success(null); break; } diff --git a/just_audio/example/android/app/src/main/AndroidManifest.xml b/just_audio/example/android/app/src/main/AndroidManifest.xml index e99a2c7..bfbd136 100644 --- a/just_audio/example/android/app/src/main/AndroidManifest.xml +++ b/just_audio/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + +