Skip to content
This repository was archived by the owner on May 18, 2022. It is now read-only.

Commit f4400bb

Browse files
Larpouxhyochan
Larpoux
authored andcommitted
Create a new method : startPlayerFromBuffer, to play from a buffer (Canardoux#170)
* Create a new method : startPlayerFromBuffer, to play from a memory buffer instead of a URL * Add support for OPUS encoder on iOS. * Actually on iOS, you can choose from two encoders : - AAC (this is the default) - CAF/OPUS * Fix bug in example, when the user try to play from buffer but has not record anything with the recorder * README : default path for Android * Suppress superfluous spaces when calling a dart method * Minor cosmetic fixes, after Hyo remarks
1 parent 1a6fea3 commit f4400bb

21 files changed

+465
-95
lines changed

README.md

+16-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ On *Android* you need to add a permission to `AndroidManifest.xml`:
4343
| Func | Param | Return | Description |
4444
| :------------ |:---------------:| :---------------:| :-----|
4545
| setSubscriptionDuration | `double sec` | `String` message | Set subscription timer in seconds. Default is `0.01` if not using this method.|
46-
| startRecorder | `String uri`, `int sampleRate`, `int numChannels` | `String` uri | Start recording. This will return uri used. |
46+
| startRecorder | `String uri`, `int sampleRate`, `int numChannels`, t_CODEC codec | `String` uri | Start recording. This will return uri used. |
4747
| stopRecorder | | `String` message | Stop recording. |
4848
| startPlayer | `String uri` | `String` message | Start playing. |
49+
| startPlayerFromBuffer | `Uint8List dataBuffer` | `String` message | Start playing. |
4950
| stopPlayer | | `String` message | Stop playing. |
5051
| pausePlayer | | `String` message | Pause playing. |
5152
| resumePlayer | | `String` message | Resume playing. |
@@ -61,7 +62,7 @@ On *Android* you need to add a permission to `AndroidManifest.xml`:
6162
## Default uri path
6263
When uri path is not set during the `function call` in `startRecorder` or `startPlayer`, they are saved in below path depending on the platform.
6364
+ Default path for android
64-
* `sdcard/sound.aac`.
65+
* `Library/Caches/sound.aac`.
6566
+ Default path for ios
6667
* `sound.aac`.
6768

@@ -91,6 +92,17 @@ If you want to take your own path specify it like below.
9192
```
9293
String path = await flutterSound.startRecorder(Platform.isIOS ? 'ios.aac' : 'android.aac');
9394
```
95+
Actually on iOS, you can choose from two encoders :
96+
- AAC (this is the default)
97+
- CAF/OPUS
98+
99+
To encode with OPUS you do the following :
100+
```dart
101+
await flutterSound.startRecorder(null, codec: t_CODEC.CODEC_CAF_OPUS,)
102+
```
103+
Recently, Apple added a support for encoding with the standard OPUS codec. Unfortunetly, Apple encapsulates its data in its own proprietary envelope : CAF. This is really stupid, this is Apple
104+
105+
On Android the OPUS codec is not yet supported by flutter_sound.
94106

95107
#### Stop recorder
96108
```dart
@@ -118,7 +130,8 @@ void dispose() {
118130
```
119131

120132
#### Start player
121-
To start playback of a recording call startPlayer.
133+
- To start playback of a record from a URL call startPlayer.
134+
- To start playback of a record from a memory buffer call startPlayerFromBuffer
122135

123136
You must wait for the return value to complete before attempting to add any listeners
124137
to ensure that the player has fully initialised.

android/build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ rootProject.allprojects {
2222
apply plugin: 'com.android.library'
2323

2424
android {
25-
compileSdkVersion 28
25+
compileSdkVersion 29
2626

2727
defaultConfig {
28-
minSdkVersion 16
28+
minSdkVersion 23
2929
}
3030
lintOptions {
3131
disable 'InvalidPackage'

android/src/main/java/com/dooboolab/fluttersound/AudioInterface.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@
22

33
import io.flutter.plugin.common.MethodChannel;
44

5+
// this enum MUST be synchronized with lib/flutter_sound.dart and ios/Classes/FlutterSoundPlugin.h
6+
enum t_CODEC
7+
{
8+
DEFAULT
9+
, AAC
10+
, OPUS
11+
, CODEC_CAF_OPUS // Apple encapsulates its bits in its own special envelope : .caf instead of a regular ogg/opus (.opus). This is completely stupid, this is Apple.
12+
, MP3
13+
, VORBIS
14+
, PCM
15+
}
16+
517
interface AudioInterface {
6-
void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRate, int androidEncoder, int androidAudioSource, int androidOutputFormat, String path, MethodChannel.Result result);
18+
void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRate, t_CODEC codec, int androidEncoder, int androidAudioSource, int androidOutputFormat, String path, MethodChannel.Result result);
719
void stopRecorder(MethodChannel.Result result);
820
void startPlayer(String path, MethodChannel.Result result);
921
void stopPlayer(MethodChannel.Result result);
@@ -15,3 +27,4 @@ interface AudioInterface {
1527
void setDbPeakLevelUpdate(double intervalInSecs, MethodChannel.Result result);
1628
void setDbLevelEnabled(boolean enabled, MethodChannel.Result result);
1729
}
30+

android/src/main/java/com/dooboolab/fluttersound/AudioModel.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import android.os.Environment;
66

77
public class AudioModel {
8-
final public static String DEFAULT_FILE_LOCATION = Environment.getExternalStorageDirectory().getPath() + "/default.aac";
8+
final public static String DEFAULT_FILE_LOCATION = Environment.getDataDirectory().getPath() + "/default.aac"; // SDK 29 : you may not write in getExternalStorageDirectory()
99
public int subsDurationMillis = 10;
1010
public long peakLevelUpdateMillis = 800;
1111
public boolean shouldProcessDbLevel = true;

android/src/main/java/com/dooboolab/fluttersound/FlutterSoundPlugin.java

+116-17
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import android.content.pm.PackageManager;
55
import android.media.MediaPlayer;
66
import android.media.MediaRecorder;
7+
import android.media.MediaDataSource;
78
import android.os.Build;
9+
import android.os.Environment;
810
import android.os.Handler;
911
import android.os.SystemClock;
1012
import android.util.Log;
1113
import android.os.Environment;
1214

15+
import io.flutter.util.PathUtils;
1316
import org.json.JSONException;
1417
import org.json.JSONObject;
1518

@@ -65,9 +68,11 @@ public void onMethodCall(final MethodCall call, final Result result) {
6568
Integer numChannels = call.argument("numChannels");
6669
Integer bitRate = call.argument("bitRate");
6770
int androidEncoder = call.argument("androidEncoder");
71+
int _codec = call.argument("codec");
72+
t_CODEC codec = t_CODEC.values()[_codec];
6873
int androidAudioSource = call.argument("androidAudioSource");
6974
int androidOutputFormat = call.argument("androidOutputFormat");
70-
startRecorder(numChannels, sampleRate, bitRate, androidEncoder, androidAudioSource, androidOutputFormat, path, result);
75+
startRecorder(numChannels, sampleRate, bitRate, codec, androidEncoder, androidAudioSource, androidOutputFormat, path, result);
7176
});
7277
break;
7378
case "stopRecorder":
@@ -76,6 +81,11 @@ public void onMethodCall(final MethodCall call, final Result result) {
7681
case "startPlayer":
7782
this.startPlayer(path, result);
7883
break;
84+
case "startPlayerFromBuffer":
85+
byte[] dataBuffer = call.argument("dataBuffer");
86+
this.startPlayerFromBuffer(dataBuffer, result);
87+
break;
88+
7989
case "stopPlayer":
8090
this.stopPlayer(result);
8191
break;
@@ -124,8 +134,10 @@ public boolean onRequestPermissionsResult(int requestCode, String[] permissions,
124134
return false;
125135
}
126136

137+
int codecArray[] = {MediaRecorder.AudioEncoder.AAC, MediaRecorder.AudioEncoder.OPUS};
138+
127139
@Override
128-
public void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRate, int androidEncoder, int androidAudioSource, int androidOutputFormat, String path, final Result result) {
140+
public void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRate, t_CODEC codec, int androidEncoder, int androidAudioSource, int androidOutputFormat, String path, final Result result) {
129141
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
130142
if (
131143
reg.activity().checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
@@ -140,17 +152,66 @@ public void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRa
140152
}
141153
}
142154

143-
if (path == null) {
144-
path = AudioModel.DEFAULT_FILE_LOCATION;
145-
} else {
146-
path = Environment.getExternalStorageDirectory().getPath() + "/" + path;
147-
}
148-
149-
if (this.model.getMediaRecorder() == null) {
155+
path = PathUtils.getDataDirectory(reg.context()) + "/" + path; // SDK 29 : you may not write in getExternalStorageDirectory() [LARPOUX]
156+
157+
if (this.model.getMediaRecorder() == null)
158+
{
150159
this.model.setMediaRecorder(new MediaRecorder());
151-
this.model.getMediaRecorder().setAudioSource(androidAudioSource);
152-
this.model.getMediaRecorder().setOutputFormat(androidOutputFormat);
153-
this.model.getMediaRecorder().setAudioEncoder(androidEncoder);
160+
} else
161+
{
162+
this.model.getMediaRecorder().reset();
163+
}
164+
this.model.getMediaRecorder().setAudioSource(androidAudioSource);
165+
int codecArray[] =
166+
{
167+
0 // DEFAULT
168+
, MediaRecorder.AudioEncoder.AAC
169+
, MediaRecorder.AudioEncoder.OPUS
170+
, 0 // CODEC_CAF_OPUS (specific Apple)
171+
, 0 // CODEC_MP3 (not implemented)
172+
, 0 // CODEC_VORBIS (not implemented)
173+
, 0 // CODEC_PCM (not implemented)
174+
};
175+
int formatsArray[] =
176+
{
177+
MediaRecorder.OutputFormat.MPEG_4 // DEFAULT
178+
, MediaRecorder.OutputFormat.MPEG_4 // CODEC_AAC
179+
, MediaRecorder.OutputFormat.OGG // CODEC_OPUS
180+
, 0 // CODEC_CAF_OPUS (this is apple specific)
181+
, 0 // CODEC_MP3
182+
, MediaRecorder.OutputFormat.OGG // CODEC_VORBIS
183+
, 0 // CODEC_PCM
184+
185+
};
186+
if (codecArray[codec.ordinal()] != 0)
187+
{
188+
androidEncoder = codecArray[codec.ordinal()];
189+
androidOutputFormat = formatsArray[codec.ordinal()];
190+
}
191+
this.model.getMediaRecorder().setOutputFormat (androidOutputFormat);
192+
model.getMediaRecorder().setAudioEncoder(androidEncoder);
193+
194+
if (path == null)
195+
{
196+
switch(androidEncoder)
197+
{
198+
case MediaRecorder.AudioEncoder.AAC:
199+
path = "sound.acc";
200+
break;
201+
case MediaRecorder.AudioEncoder.OPUS:
202+
path = "sound.opus";
203+
break;
204+
case MediaRecorder.AudioEncoder.VORBIS:
205+
path = "sound.ogg";
206+
break;
207+
case MediaRecorder.AudioEncoder.DEFAULT:
208+
path = "sound.acc";
209+
break; // ?
210+
default:
211+
path = "sound.acc";
212+
break; // ?
213+
}
214+
}
154215

155216
this.model.getMediaRecorder().setOutputFile(path);
156217

@@ -165,7 +226,6 @@ public void startRecorder(Integer numChannels, Integer sampleRate, Integer bitRa
165226
// If bitrate is defined, then use it, otherwise use the OS default
166227
if (bitRate != null) {
167228
this.model.getMediaRecorder().setAudioEncodingBitRate(bitRate);
168-
}
169229
}
170230

171231
try {
@@ -264,9 +324,8 @@ public void run() {
264324

265325
}
266326

267-
@Override
268-
public void startPlayer(final String path, final Result result) {
269-
if (this.model.getMediaPlayer() != null) {
327+
public void _startPlayer(final String path, MediaDataSource dataBuffer, final Result result) {
328+
if (this.model.getMediaPlayer() != null) {
270329
Boolean isPaused = !this.model.getMediaPlayer().isPlaying()
271330
&& this.model.getMediaPlayer().getCurrentPosition() > 1;
272331

@@ -285,6 +344,10 @@ public void startPlayer(final String path, final Result result) {
285344
mTimer = new Timer();
286345

287346
try {
347+
if (dataBuffer != null)
348+
{
349+
model.getMediaPlayer().setDataSource(dataBuffer);
350+
} else
288351
if (path == null) {
289352
this.model.getMediaPlayer().setDataSource(AudioModel.DEFAULT_FILE_LOCATION);
290353
} else {
@@ -322,7 +385,12 @@ public void run() {
322385
};
323386

324387
mTimer.schedule(mTask, 0, model.subsDurationMillis);
325-
String resolvedPath = path == null ? AudioModel.DEFAULT_FILE_LOCATION : path;
388+
String resolvedPath;
389+
if (dataBuffer != null)
390+
{
391+
resolvedPath = "(From memory buffer)";
392+
} else
393+
resolvedPath = (path == null) ? AudioModel.DEFAULT_FILE_LOCATION : path;
326394
result.success((resolvedPath));
327395
});
328396
/*
@@ -357,6 +425,37 @@ public void run() {
357425
}
358426
}
359427

428+
@Override
429+
public void startPlayer(final String path, final Result result)
430+
{
431+
_startPlayer(path, null, result) ;
432+
}
433+
434+
public void startPlayerFromBuffer(final byte[] dataBuffer, final Result result)
435+
{
436+
class flutterSoundBuffer extends MediaDataSource
437+
{
438+
byte[] dataBuffer;
439+
/* ctor */ flutterSoundBuffer(byte[] buffer){dataBuffer = buffer;}
440+
public int readAt(long position, byte[] buffer, int offset, int size)
441+
{
442+
if (position > dataBuffer.length)
443+
return -1; // end of buffer
444+
long ln = size;
445+
if (position + size > dataBuffer.length)
446+
ln = dataBuffer.length - position;
447+
System.arraycopy(dataBuffer, (int)position, buffer, offset, (int)ln);
448+
return (int)ln;
449+
}
450+
public long getSize(){return dataBuffer.length;}
451+
public void close(){dataBuffer = null;}
452+
}
453+
454+
_startPlayer(null, new flutterSoundBuffer(dataBuffer), result) ;
455+
}
456+
457+
458+
360459
@Override
361460
public void stopPlayer(final Result result) {
362461
mTimer.cancel();

example/.flutter-plugins-dependencies

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"flutter_sound","dependencies":[]}]}

example/android/app/build.gradle

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
2525
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
2626

2727
android {
28-
compileSdkVersion 28
28+
compileSdkVersion 29
2929

3030
lintOptions {
3131
disable 'InvalidPackage'
@@ -34,8 +34,8 @@ android {
3434
defaultConfig {
3535
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
3636
applicationId "com.dooboolab.fluttersoundexample"
37-
minSdkVersion 16
38-
targetSdkVersion 28
37+
minSdkVersion 23
38+
targetSdkVersion 29
3939
versionCode flutterVersionCode.toInteger()
4040
versionName flutterVersionName
4141
}

example/android/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
}
66

77
dependencies {
8-
classpath 'com.android.tools.build:gradle:3.4.0'
8+
classpath 'com.android.tools.build:gradle:3.5.3'
99
}
1010
}
1111

example/android/gradle.properties

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
org.gradle.jvmargs=-Xmx1536M
2+
android.enableR8=true
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
#Thu May 09 09:01:19 CEST 2019
1+
#Fri Dec 13 14:24:13 CET 2019
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

example/ios/Flutter/Flutter.podspec

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#
2+
# NOTE: This podspec is NOT to be published. It is only used as a local source!
3+
#
4+
5+
Pod::Spec.new do |s|
6+
s.name = 'Flutter'
7+
s.version = '1.0.0'
8+
s.summary = 'High-performance, high-fidelity mobile apps.'
9+
s.description = <<-DESC
10+
Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS.
11+
DESC
12+
s.homepage = 'https://flutter.io'
13+
s.license = { :type => 'MIT' }
14+
s.author = { 'Flutter Dev Team' => '[email protected]' }
15+
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
16+
s.ios.deployment_target = '8.0'
17+
s.vendored_frameworks = 'Flutter.framework'
18+
end

0 commit comments

Comments
 (0)