Skip to content

Commit

Permalink
✨ added streams to listen to current duration and player state for io…
Browse files Browse the repository at this point in the history
…s 📝 ios internal re-structure
  • Loading branch information
ujas-m-simformsolutions committed May 11, 2022
1 parent 73b73ed commit 87d08c5
Show file tree
Hide file tree
Showing 19 changed files with 504 additions and 308 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
## [0.1.2] - 4 May, 2022
## [0.1.2] - 11 May, 2022

* Fixed ios resume recording issue
* Fixed player state not getting update when playing is finished by providing PlayerState stream.
* Added current duration stream for PlayerController.
* Breaking: Replaced `seekToStart` parameter from `startPlayer()` with `FinishMode` enum for
better controls when audio is finished
* Breaking: Renamed `disposeFunc()` to simpler `dispose()` for both controllers
* Internal native restructure.
* Minor documentation update

## [0.1.1] - 28 April, 2022

Expand Down
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ Use this plugin to generate waveforms while recording audio in any file formats
by given encoders or from audio files. We can use gestures to scroll through the waveforms or seek to
any position while playing audio and also style waveforms.


### Recorder

## Preview
<a href="https://raw.githubusercontent.com/SimformSolutionsPvtLtd/audio_waveforms/main/preview/demo.gif"><img src="https://raw.githubusercontent.com/SimformSolutionsPvtLtd/audio_waveforms/main/preview/demo.gif" width="390px;" height="700px;"/></a>

### Recorder

## Platform specific configuration

### Android
Expand Down Expand Up @@ -57,7 +56,6 @@ import 'package:audio_waveforms/audio_waveforms.dart';
1. Initialise RecorderController
```dart
late final RecorderController recorderController;
@override
void initState() {
super.initState();
Expand All @@ -76,7 +74,7 @@ AudioWaveforms(
await recorderController.record();
```
You can provide file name with extension and full path in path parameter of record function. If
not provided .aac is default extension and dateTime will be file name.
not provided .aac is the default extension and dateTime will be the file name.

4. Pause recording
```dart
Expand All @@ -92,7 +90,7 @@ Calling this will save the recording at provided path and it will return path to
```dart
@override
void dispose() {
recorderController.disposeFunc();
recorderController.dispose();
super.dispose();
}
```
Expand All @@ -104,13 +102,13 @@ AudioWaveforms(
enableGesture: true,
),
```
By enabling gestures, you can scroll through waveform in recording state or paused state
By enabling gestures, you can scroll through waveform in recording state or paused state.

2. Refreshing the wave to initial position after scrolling
```dart
recorderController.refresh();
```
Once scrolled waveform will stop updating position with newly added wave while recording so we can
Once scrolled waveform will stop updating position with newly added waves while recording so we can
use this to get waves updating again. It can also be used in paused/stopped state.

3. Changing style of the waves
Expand Down Expand Up @@ -218,7 +216,7 @@ await playerController.stopPlayer();
```dart
@override
void dispose() {
playerController.disposeFunc();
playerController.dispose();
super.dispose();
}
```
Expand All @@ -242,4 +240,20 @@ AudioFileWaveforms(
enableSeekGesture: true,
)
```
Audio also can be seeked using gestures on waveforms (enabled by default).
Audio also can be seeked using gestures on waveforms (enabled by default).
5. Ending audio with different modes
```dart
await playerController.startPlayer(finishMode: FinishMode.stop);
```
Using `FinishMode.stop` will stop the player, `FinishMode.pause` will pause the player and
`FinishMode.loop` will loop the player.

6. Listening to player state changes
```dart
playerController.onPlayerStateChanged.listen((state) {});
```
7. Listening to current duration
```dart
playerController.onCurrentDurationChanged.listen((duration) {});
```
Duration is in milliseconds.
187 changes: 97 additions & 90 deletions android/src/main/kotlin/com/simform/audio_waveforms/AudioPlayer.kt
Original file line number Diff line number Diff line change
@@ -1,129 +1,150 @@
package com.simform.audio_waveforms

import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import io.flutter.plugin.common.MethodChannel
import java.lang.Exception

class AudioPlayer(context: Context, channel: MethodChannel) {
private val LOG_TAG = "AudioWaveforms"
class AudioPlayer(context: Context, channel: MethodChannel, playerKey: String) {
private var handler: Handler = Handler(Looper.getMainLooper())
private var runnable = mutableMapOf<String, Runnable?>()
private var runnable: Runnable? = null
private var methodChannel = channel
private var appContext = context
private var players = mutableMapOf<String, ExoPlayer?>()
private var playerListeners = mutableMapOf<String, Player.Listener?>()
private var preparedPlayers = mutableMapOf<String, Boolean>()
private var seekToStart = true
private var player: ExoPlayer? = null
private var playerListener: Player.Listener? = null
private var isPlayerPrepared: Boolean = false
private var finishMode = FinishMode.Stop
private var key = playerKey

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun preparePlayer(
result: MethodChannel.Result,
path: String?,
volume: Float?,
key: String?
volume: Float?
) {

//TODO: meta data of song
if (key != null && path != null) {
if (path != null) {
val mediaItem = MediaItem.fromUri(path)
players[key] = ExoPlayer.Builder(appContext).build()
players[key]?.addMediaItem(mediaItem)
players[key]?.prepare()
playerListeners[key] = object : Player.Listener {
player = ExoPlayer.Builder(appContext).build()
player?.addMediaItem(mediaItem)
player?.prepare()
playerListener = object : Player.Listener {
override fun onPlayerStateChanged(isReady: Boolean, state: Int) {
if (preparedPlayers[key] == false || preparedPlayers[key] == null) {
if (!isPlayerPrepared) {
if (state == Player.STATE_READY) {
players[key]?.volume = volume ?: 1F
preparedPlayers[key] = true
player?.volume = volume ?: 1F
isPlayerPrepared = true
result.success(true)
}
}
if (state == Player.STATE_ENDED && seekToStart) {
players[key]?.seekTo(0)
players[key]?.pause()
if (state == Player.STATE_ENDED) {
val args: MutableMap<String, Any?> = HashMap()
when (finishMode) {
FinishMode.Loop -> {
player?.seekTo(0)
player?.play()
args[Constants.finishType] = 0
}
FinishMode.Pause -> {
player?.seekTo(0)
player?.playWhenReady = false
stopListening()
args[Constants.finishType] = 1
}
else -> {
player?.stop()
player?.release()
player = null
stopListening()
args[Constants.finishType] = 2
}
}
args[Constants.playerKey] = key
methodChannel.invokeMethod(
Constants.onDidFinishPlayingAudio,
args
)
}
}
}
players[key]?.addListener(playerListeners[key]!!)
player?.addListener(playerListener!!)
} else {
result.error(LOG_TAG, "path to audio file or unique key can't be null", "")
result.error(Constants.LOG_TAG, "path to audio file or unique key can't be null", "")
}
}

@RequiresApi(Build.VERSION_CODES.O)
fun seekToPosition(result: MethodChannel.Result, progress: Long?, key: String?) {
if (progress != null && key != null) {
players[key]?.seekTo(progress)
fun seekToPosition(result: MethodChannel.Result, progress: Long?) {
if (progress != null) {
player?.seekTo(progress)
result.success(true)
} else {
result.success(false)
}
}

fun start(result: MethodChannel.Result, seekToStart: Boolean, key: String?) {
fun start(result: MethodChannel.Result, finishMode: Int?) {
try {
this.seekToStart = seekToStart
players[key]?.play()
if (finishMode != null && finishMode == 0) {
this.finishMode = FinishMode.Loop
} else if (finishMode != null && finishMode == 1) {
this.finishMode = FinishMode.Pause
} else {
this.finishMode = FinishMode.Stop
}
player?.playWhenReady = true
player?.play()
result.success(true)
startListening(result, key)
startListening(result)
} catch (e: Exception) {
result.error(LOG_TAG, "Can not start the player", e.toString())
result.error(Constants.LOG_TAG, "Can not start the player", e.toString())
}
}

fun getDuration(result: MethodChannel.Result, durationType: DurationType, key: String?) {
fun getDuration(result: MethodChannel.Result, durationType: DurationType) {
try {
if (durationType == DurationType.Current) {
val duration = players[key]?.currentPosition
val duration = player?.currentPosition
result.success(duration)
} else {
val duration = players[key]?.duration
val duration = player?.duration
result.success(duration)
}
} catch (e: Exception) {
result.error(LOG_TAG, "Can not get duration", e.toString())
result.error(Constants.LOG_TAG, "Can not get duration", e.toString())
}
}

fun stop(result: MethodChannel.Result, key: String?) {
if (key != null) {
stopListening(key)
if (playerListeners[key] != null) {
players[key]?.removeListener(playerListeners[key]!!)
playerListeners.remove(key)
}
preparedPlayers.remove(key)
players[key]?.stop()
players[key]?.release()
result.success(true)
fun stop(result: MethodChannel.Result) {
stopListening()
if (playerListener != null) {
player?.removeListener(playerListener!!)
}
isPlayerPrepared = false
player?.stop()
player?.release()
result.success(true)
}


fun pause(result: MethodChannel.Result, key: String?) {
if (key != null) {
try {
stopListening(key)
players[key]?.pause()
result.success(true)
} catch (e: Exception) {
result.error(LOG_TAG, "Failed to pause the player", e.toString())
}
fun pause(result: MethodChannel.Result) {
try {
stopListening()
player?.pause()
result.success(true)
} catch (e: Exception) {
result.error(Constants.LOG_TAG, "Failed to pause the player", e.toString())
}

}

fun setVolume(volume: Float?, result: MethodChannel.Result, key: String?) {
fun setVolume(volume: Float?, result: MethodChannel.Result) {
try {
if (volume != null && key != null) {
players[key]?.volume = volume
if (volume != null) {
player?.volume = volume
result.success(true)
} else {
result.success(false)
Expand All @@ -133,40 +154,26 @@ class AudioPlayer(context: Context, channel: MethodChannel) {
}
}

private fun startListening(result: MethodChannel.Result, key: String?) {
if (key != null) {
runnable[key] = object : Runnable {
override fun run() {
val currentPosition = players[key]?.currentPosition
if (currentPosition != null) {
val args: MutableMap<String, Any?> = HashMap()
args[Constants.current] = currentPosition
args[Constants.playerKey] = key
methodChannel.invokeMethod(Constants.onCurrentDuration, args)
handler.postDelayed(this, 200)
} else {
result.error(LOG_TAG, "Can't get current Position of player", "")
}
private fun startListening(result: MethodChannel.Result) {
runnable = object : Runnable {
override fun run() {
val currentPosition = player?.currentPosition
if (currentPosition != null) {
val args: MutableMap<String, Any?> = HashMap()
args[Constants.current] = currentPosition
args[Constants.playerKey] = key
methodChannel.invokeMethod(Constants.onCurrentDuration, args)
handler.postDelayed(this, 200)
} else {
result.error(Constants.LOG_TAG, "Can't get current Position of player", "")
}
}
handler.post(runnable[key]!!)
}
handler.post(runnable!!)

}

private fun stopListening(key: String?) {
runnable[key]?.let { handler.removeCallbacks(it) }
}

fun stopAllPlayers(result: MethodChannel.Result) {
for ((key, _) in players) {
players[key]?.stop()
players[key] = null
}
for ((key, _) in runnable) {
runnable[key]?.let { handler.removeCallbacks(it) }
runnable[key] = null
}
result.success(true)
private fun stopListening() {
runnable?.let { handler.removeCallbacks(it) }
}
}
Loading

0 comments on commit 87d08c5

Please sign in to comment.