Skip to content

Commit 7b5e8e5

Browse files
authored
feat: [Android] Implement custom audio source example (#475)
1 parent 17a4493 commit 7b5e8e5

File tree

14 files changed

+1032
-0
lines changed

14 files changed

+1032
-0
lines changed

example/android/app/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
2828
android {
2929
compileSdkVersion 28
3030

31+
compileOptions {
32+
sourceCompatibility JavaVersion.VERSION_1_8
33+
targetCompatibility JavaVersion.VERSION_1_8
34+
}
35+
36+
kotlinOptions {
37+
jvmTarget = '1.8'
38+
}
39+
3140
sourceSets {
3241
main.java.srcDirs += 'src/main/kotlin'
3342
test.java.srcDirs += 'src/test/kotlin'

example/android/app/src/main/AndroidManifest.xml

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
package="io.agora.agora_rtc_engine_example">
33
<uses-permission android:name="android.permission.WAKE_LOCK" />
4+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
45
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
56
calls FlutterMain.startInitialization(this); in its onCreate method.
67
In most cases you can leave this as-is, but you if you want to provide
@@ -44,5 +45,9 @@
4445
<meta-data
4546
android:name="flutterEmbedding"
4647
android:value="2" />
48+
49+
<service
50+
android:name="io.agora.agora_rtc_engine_example.custom_audio_source.AudioRecordService"
51+
android:exported="false" />
4752
</application>
4853
</manifest>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
package io.agora.agora_rtc_engine_example
22

3+
import android.os.Bundle
4+
import io.agora.agora_rtc_engine_example.custom_audio_source.CustomAudioPlugin
5+
import io.agora.agora_rtc_engine_example.custom_audio_source.CustomAudioSource
6+
import io.agora.rtc.base.RtcEnginePlugin
37
import io.flutter.embedding.android.FlutterActivity
8+
import io.flutter.embedding.engine.FlutterEngine
9+
import java.lang.ref.WeakReference
410

511
class MainActivity: FlutterActivity() {
12+
13+
private val customAudioPlugin = CustomAudioPlugin(WeakReference(this))
14+
15+
override fun onCreate(savedInstanceState: Bundle?) {
16+
super.onCreate(savedInstanceState)
17+
18+
RtcEnginePlugin.register(customAudioPlugin)
19+
}
20+
21+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
22+
super.configureFlutterEngine(flutterEngine)
23+
CustomAudioSource.CustomAudioSourceApi.setup(flutterEngine.dartExecutor, customAudioPlugin)
24+
}
25+
26+
override fun onDestroy() {
27+
super.onDestroy()
28+
29+
RtcEnginePlugin.unregister(customAudioPlugin)
30+
}
631
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package io.agora.agora_rtc_engine_example.custom_audio_source;
2+
3+
import android.app.Notification;
4+
import android.app.NotificationChannel;
5+
import android.app.NotificationManager;
6+
import android.app.PendingIntent;
7+
import android.app.Service;
8+
import android.content.Context;
9+
import android.content.Intent;
10+
import android.media.AudioFormat;
11+
import android.media.AudioRecord;
12+
import android.media.MediaRecorder;
13+
import android.os.Build;
14+
import android.os.IBinder;
15+
import android.util.Log;
16+
17+
import androidx.annotation.Nullable;
18+
import androidx.core.app.NotificationCompat;
19+
20+
21+
public class AudioRecordService extends Service
22+
{
23+
private static final String TAG = AudioRecordService.class.getSimpleName();
24+
25+
private volatile boolean stopped;
26+
private int mSampleRate;
27+
private int mChannels;
28+
29+
public static void start(
30+
Context context,
31+
Class<?> notificationActivity,
32+
int sampleRate,
33+
int channels
34+
) {
35+
Intent intent = new Intent(context, AudioRecordService.class);
36+
intent.putExtra("notificationActivity", notificationActivity);
37+
intent.putExtra("sampleRate", sampleRate);
38+
intent.putExtra("channels", channels);
39+
context.startService(intent);
40+
}
41+
42+
@Nullable
43+
@Override
44+
public IBinder onBind(Intent intent)
45+
{
46+
return null;
47+
}
48+
49+
@Override
50+
public int onStartCommand(Intent intent, int flags, int startId) {
51+
mSampleRate = intent.getIntExtra("sampleRate", -1);
52+
mChannels = intent.getIntExtra("channels", -1);
53+
startForeground((Class<?>) intent.getSerializableExtra("notificationActivity"));
54+
startRecording();
55+
return Service.START_STICKY;
56+
}
57+
58+
private void startForeground(Class<?> clazz)
59+
{
60+
createNotificationChannel();
61+
Intent notificationIntent = new Intent(this, clazz);
62+
PendingIntent pendingIntent = PendingIntent.getActivity(this,
63+
0, notificationIntent, 0);
64+
65+
Notification notification = new NotificationCompat.Builder(this, TAG)
66+
.setContentTitle(TAG)
67+
.setContentIntent(pendingIntent)
68+
.build();
69+
70+
startForeground(1, notification);
71+
}
72+
73+
private void createNotificationChannel()
74+
{
75+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
76+
{
77+
NotificationChannel serviceChannel = new NotificationChannel(
78+
TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT
79+
);
80+
81+
NotificationManager manager = getSystemService(NotificationManager.class);
82+
manager.createNotificationChannel(serviceChannel);
83+
}
84+
}
85+
86+
private void startRecording() {
87+
RecordThread thread = new RecordThread();
88+
thread.start();
89+
}
90+
91+
private void stopRecording()
92+
{
93+
stopped = true;
94+
}
95+
96+
@Override
97+
public void onDestroy()
98+
{
99+
stopRecording();
100+
super.onDestroy();
101+
}
102+
103+
private void sendData(byte[] buffer) {
104+
Intent intent = new Intent("AudioRecordRead");
105+
intent.putExtra("buffer", buffer);
106+
intent.putExtra("sampleRate", mSampleRate);
107+
intent.putExtra("channels", mChannels);
108+
sendBroadcast(intent);
109+
}
110+
111+
public class RecordThread extends Thread
112+
{
113+
private final AudioRecord audioRecord;
114+
private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
115+
private byte[] buffer;
116+
117+
RecordThread()
118+
{
119+
int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, DEFAULT_CHANNEL_CONFIG,
120+
AudioFormat.ENCODING_PCM_16BIT);
121+
audioRecord = new AudioRecord(
122+
MediaRecorder.AudioSource.MIC,
123+
mSampleRate,
124+
mChannels,
125+
AudioFormat.ENCODING_PCM_16BIT, bufferSize);
126+
127+
buffer = new byte[bufferSize];
128+
}
129+
130+
@Override
131+
public void run()
132+
{
133+
try
134+
{
135+
audioRecord.startRecording();
136+
while (!stopped)
137+
{
138+
int result = audioRecord.read(buffer, 0, buffer.length);
139+
if (result >= 0)
140+
{
141+
/**Pushes the external audio frame to the Agora SDK for encoding.
142+
* @param data External audio data to be pushed.
143+
* @param timeStamp Timestamp of the external audio frame. It is mandatory.
144+
* You can use this parameter for the following purposes:
145+
* 1:Restore the order of the captured audio frame.
146+
* 2:Synchronize audio and video frames in video-related
147+
* scenarios, including scenarios where external video sources are used.
148+
* @return
149+
* 0: Success.
150+
* < 0: Failure.*/
151+
sendData(buffer);
152+
}
153+
else
154+
{
155+
logRecordError(result);
156+
}
157+
Log.d(TAG, "byte size is :" + result);
158+
}
159+
release();
160+
}
161+
catch (Exception e)
162+
{e.printStackTrace();}
163+
}
164+
165+
private void logRecordError(int error)
166+
{
167+
String message = "";
168+
switch (error)
169+
{
170+
case AudioRecord.ERROR:
171+
message = "generic operation failure";
172+
break;
173+
case AudioRecord.ERROR_BAD_VALUE:
174+
message = "failure due to the use of an invalid value";
175+
break;
176+
case AudioRecord.ERROR_DEAD_OBJECT:
177+
message = "object is no longer valid and needs to be recreated";
178+
break;
179+
case AudioRecord.ERROR_INVALID_OPERATION:
180+
message = "failure due to the improper use of method";
181+
break;
182+
}
183+
Log.e(TAG, message);
184+
}
185+
186+
private void release()
187+
{
188+
if (audioRecord != null)
189+
{
190+
audioRecord.stop();
191+
buffer = null;
192+
}
193+
}
194+
}
195+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.agora.agora_rtc_engine_example.custom_audio_source;
2+
3+
public enum AudioStatus {
4+
INITIALISING,
5+
RUNNING,
6+
STOPPED
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.agora.agora_rtc_engine_example.custom_audio_source
2+
3+
import android.app.Activity
4+
import android.content.BroadcastReceiver
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.IntentFilter
8+
import android.media.AudioFormat
9+
import io.agora.rtc.Constants
10+
import io.agora.rtc.RtcEngine
11+
import io.agora.rtc.base.RtcEnginePlugin
12+
import java.lang.ref.WeakReference
13+
14+
class CustomAudioPlugin(private val activity: WeakReference<Activity>) :
15+
RtcEnginePlugin,
16+
CustomAudioSource.CustomAudioSourceApi {
17+
18+
private var rtcEngine: RtcEngine? = null
19+
20+
@Volatile
21+
private var sourcePos: Int = Constants.AudioExternalSourcePos.getValue(
22+
Constants.AudioExternalSourcePos.AUDIO_EXTERNAL_PLAYOUT_SOURCE)
23+
24+
private val broadcastReceiver = object : BroadcastReceiver() {
25+
override fun onReceive(context: Context?, intent: Intent?) {
26+
intent?.apply {
27+
val buffer = getByteArrayExtra("buffer")
28+
val sampleRate = getIntExtra("sampleRate", -1)
29+
val channels = getIntExtra("channels", -1)
30+
31+
rtcEngine?.pushExternalAudioFrame(
32+
buffer,
33+
System.currentTimeMillis(),
34+
sampleRate,
35+
channels,
36+
AudioFormat.ENCODING_PCM_16BIT,
37+
sourcePos)
38+
}
39+
}
40+
}
41+
42+
override fun onRtcEngineCreated(rtcEngine: RtcEngine?) {
43+
this.rtcEngine = rtcEngine
44+
activity.get()?.registerReceiver(broadcastReceiver, IntentFilter("AudioRecordRead"))
45+
}
46+
47+
override fun onRtcEngineDestroyed() {
48+
activity.get()?.unregisterReceiver(broadcastReceiver)
49+
rtcEngine = null
50+
}
51+
52+
override fun setExternalAudioSource(enabled: Boolean?, sampleRate: Long?, channels: Long?) {
53+
rtcEngine?.setExternalAudioSource(enabled!!, sampleRate!!.toInt(), channels!!.toInt())
54+
}
55+
56+
override fun setExternalAudioSourceVolume(sourcePos: Long?, volume: Long?) {
57+
this.sourcePos = sourcePos!!.toInt()
58+
rtcEngine?.setExternalAudioSourceVolume(this.sourcePos, volume!!.toInt())
59+
}
60+
61+
override fun startAudioRecord(sampleRate: Long?, channels: Long?) {
62+
activity.get()?.apply {
63+
AudioRecordService.start(this, this::class.java, sampleRate!!.toInt(), channels!!.toInt())
64+
}
65+
}
66+
67+
override fun stopAudioRecord() {
68+
activity.get()?.apply {
69+
val intent = Intent(this, AudioRecordService::class.java)
70+
stopService(intent)
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)