-
Notifications
You must be signed in to change notification settings - Fork 381
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [Android] Implement custom audio source example (#475)
- Loading branch information
1 parent
17a4493
commit 7b5e8e5
Showing
14 changed files
with
1,032 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
example/android/app/src/main/kotlin/io/agora/agora_rtc_engine_example/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,31 @@ | ||
package io.agora.agora_rtc_engine_example | ||
|
||
import android.os.Bundle | ||
import io.agora.agora_rtc_engine_example.custom_audio_source.CustomAudioPlugin | ||
import io.agora.agora_rtc_engine_example.custom_audio_source.CustomAudioSource | ||
import io.agora.rtc.base.RtcEnginePlugin | ||
import io.flutter.embedding.android.FlutterActivity | ||
import io.flutter.embedding.engine.FlutterEngine | ||
import java.lang.ref.WeakReference | ||
|
||
class MainActivity: FlutterActivity() { | ||
|
||
private val customAudioPlugin = CustomAudioPlugin(WeakReference(this)) | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
RtcEnginePlugin.register(customAudioPlugin) | ||
} | ||
|
||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | ||
super.configureFlutterEngine(flutterEngine) | ||
CustomAudioSource.CustomAudioSourceApi.setup(flutterEngine.dartExecutor, customAudioPlugin) | ||
} | ||
|
||
override fun onDestroy() { | ||
super.onDestroy() | ||
|
||
RtcEnginePlugin.unregister(customAudioPlugin) | ||
} | ||
} |
195 changes: 195 additions & 0 deletions
195
...main/kotlin/io/agora/agora_rtc_engine_example/custom_audio_source/AudioRecordService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package io.agora.agora_rtc_engine_example.custom_audio_source; | ||
|
||
import android.app.Notification; | ||
import android.app.NotificationChannel; | ||
import android.app.NotificationManager; | ||
import android.app.PendingIntent; | ||
import android.app.Service; | ||
import android.content.Context; | ||
import android.content.Intent; | ||
import android.media.AudioFormat; | ||
import android.media.AudioRecord; | ||
import android.media.MediaRecorder; | ||
import android.os.Build; | ||
import android.os.IBinder; | ||
import android.util.Log; | ||
|
||
import androidx.annotation.Nullable; | ||
import androidx.core.app.NotificationCompat; | ||
|
||
|
||
public class AudioRecordService extends Service | ||
{ | ||
private static final String TAG = AudioRecordService.class.getSimpleName(); | ||
|
||
private volatile boolean stopped; | ||
private int mSampleRate; | ||
private int mChannels; | ||
|
||
public static void start( | ||
Context context, | ||
Class<?> notificationActivity, | ||
int sampleRate, | ||
int channels | ||
) { | ||
Intent intent = new Intent(context, AudioRecordService.class); | ||
intent.putExtra("notificationActivity", notificationActivity); | ||
intent.putExtra("sampleRate", sampleRate); | ||
intent.putExtra("channels", channels); | ||
context.startService(intent); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public IBinder onBind(Intent intent) | ||
{ | ||
return null; | ||
} | ||
|
||
@Override | ||
public int onStartCommand(Intent intent, int flags, int startId) { | ||
mSampleRate = intent.getIntExtra("sampleRate", -1); | ||
mChannels = intent.getIntExtra("channels", -1); | ||
startForeground((Class<?>) intent.getSerializableExtra("notificationActivity")); | ||
startRecording(); | ||
return Service.START_STICKY; | ||
} | ||
|
||
private void startForeground(Class<?> clazz) | ||
{ | ||
createNotificationChannel(); | ||
Intent notificationIntent = new Intent(this, clazz); | ||
PendingIntent pendingIntent = PendingIntent.getActivity(this, | ||
0, notificationIntent, 0); | ||
|
||
Notification notification = new NotificationCompat.Builder(this, TAG) | ||
.setContentTitle(TAG) | ||
.setContentIntent(pendingIntent) | ||
.build(); | ||
|
||
startForeground(1, notification); | ||
} | ||
|
||
private void createNotificationChannel() | ||
{ | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
{ | ||
NotificationChannel serviceChannel = new NotificationChannel( | ||
TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT | ||
); | ||
|
||
NotificationManager manager = getSystemService(NotificationManager.class); | ||
manager.createNotificationChannel(serviceChannel); | ||
} | ||
} | ||
|
||
private void startRecording() { | ||
RecordThread thread = new RecordThread(); | ||
thread.start(); | ||
} | ||
|
||
private void stopRecording() | ||
{ | ||
stopped = true; | ||
} | ||
|
||
@Override | ||
public void onDestroy() | ||
{ | ||
stopRecording(); | ||
super.onDestroy(); | ||
} | ||
|
||
private void sendData(byte[] buffer) { | ||
Intent intent = new Intent("AudioRecordRead"); | ||
intent.putExtra("buffer", buffer); | ||
intent.putExtra("sampleRate", mSampleRate); | ||
intent.putExtra("channels", mChannels); | ||
sendBroadcast(intent); | ||
} | ||
|
||
public class RecordThread extends Thread | ||
{ | ||
private final AudioRecord audioRecord; | ||
private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; | ||
private byte[] buffer; | ||
|
||
RecordThread() | ||
{ | ||
int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, DEFAULT_CHANNEL_CONFIG, | ||
AudioFormat.ENCODING_PCM_16BIT); | ||
audioRecord = new AudioRecord( | ||
MediaRecorder.AudioSource.MIC, | ||
mSampleRate, | ||
mChannels, | ||
AudioFormat.ENCODING_PCM_16BIT, bufferSize); | ||
|
||
buffer = new byte[bufferSize]; | ||
} | ||
|
||
@Override | ||
public void run() | ||
{ | ||
try | ||
{ | ||
audioRecord.startRecording(); | ||
while (!stopped) | ||
{ | ||
int result = audioRecord.read(buffer, 0, buffer.length); | ||
if (result >= 0) | ||
{ | ||
/**Pushes the external audio frame to the Agora SDK for encoding. | ||
* @param data External audio data to be pushed. | ||
* @param timeStamp Timestamp of the external audio frame. It is mandatory. | ||
* You can use this parameter for the following purposes: | ||
* 1:Restore the order of the captured audio frame. | ||
* 2:Synchronize audio and video frames in video-related | ||
* scenarios, including scenarios where external video sources are used. | ||
* @return | ||
* 0: Success. | ||
* < 0: Failure.*/ | ||
sendData(buffer); | ||
} | ||
else | ||
{ | ||
logRecordError(result); | ||
} | ||
Log.d(TAG, "byte size is :" + result); | ||
} | ||
release(); | ||
} | ||
catch (Exception e) | ||
{e.printStackTrace();} | ||
} | ||
|
||
private void logRecordError(int error) | ||
{ | ||
String message = ""; | ||
switch (error) | ||
{ | ||
case AudioRecord.ERROR: | ||
message = "generic operation failure"; | ||
break; | ||
case AudioRecord.ERROR_BAD_VALUE: | ||
message = "failure due to the use of an invalid value"; | ||
break; | ||
case AudioRecord.ERROR_DEAD_OBJECT: | ||
message = "object is no longer valid and needs to be recreated"; | ||
break; | ||
case AudioRecord.ERROR_INVALID_OPERATION: | ||
message = "failure due to the improper use of method"; | ||
break; | ||
} | ||
Log.e(TAG, message); | ||
} | ||
|
||
private void release() | ||
{ | ||
if (audioRecord != null) | ||
{ | ||
audioRecord.stop(); | ||
buffer = null; | ||
} | ||
} | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
...pp/src/main/kotlin/io/agora/agora_rtc_engine_example/custom_audio_source/AudioStatus.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package io.agora.agora_rtc_engine_example.custom_audio_source; | ||
|
||
public enum AudioStatus { | ||
INITIALISING, | ||
RUNNING, | ||
STOPPED | ||
} |
73 changes: 73 additions & 0 deletions
73
...rc/main/kotlin/io/agora/agora_rtc_engine_example/custom_audio_source/CustomAudioPlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package io.agora.agora_rtc_engine_example.custom_audio_source | ||
|
||
import android.app.Activity | ||
import android.content.BroadcastReceiver | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.content.IntentFilter | ||
import android.media.AudioFormat | ||
import io.agora.rtc.Constants | ||
import io.agora.rtc.RtcEngine | ||
import io.agora.rtc.base.RtcEnginePlugin | ||
import java.lang.ref.WeakReference | ||
|
||
class CustomAudioPlugin(private val activity: WeakReference<Activity>) : | ||
RtcEnginePlugin, | ||
CustomAudioSource.CustomAudioSourceApi { | ||
|
||
private var rtcEngine: RtcEngine? = null | ||
|
||
@Volatile | ||
private var sourcePos: Int = Constants.AudioExternalSourcePos.getValue( | ||
Constants.AudioExternalSourcePos.AUDIO_EXTERNAL_PLAYOUT_SOURCE) | ||
|
||
private val broadcastReceiver = object : BroadcastReceiver() { | ||
override fun onReceive(context: Context?, intent: Intent?) { | ||
intent?.apply { | ||
val buffer = getByteArrayExtra("buffer") | ||
val sampleRate = getIntExtra("sampleRate", -1) | ||
val channels = getIntExtra("channels", -1) | ||
|
||
rtcEngine?.pushExternalAudioFrame( | ||
buffer, | ||
System.currentTimeMillis(), | ||
sampleRate, | ||
channels, | ||
AudioFormat.ENCODING_PCM_16BIT, | ||
sourcePos) | ||
} | ||
} | ||
} | ||
|
||
override fun onRtcEngineCreated(rtcEngine: RtcEngine?) { | ||
this.rtcEngine = rtcEngine | ||
activity.get()?.registerReceiver(broadcastReceiver, IntentFilter("AudioRecordRead")) | ||
} | ||
|
||
override fun onRtcEngineDestroyed() { | ||
activity.get()?.unregisterReceiver(broadcastReceiver) | ||
rtcEngine = null | ||
} | ||
|
||
override fun setExternalAudioSource(enabled: Boolean?, sampleRate: Long?, channels: Long?) { | ||
rtcEngine?.setExternalAudioSource(enabled!!, sampleRate!!.toInt(), channels!!.toInt()) | ||
} | ||
|
||
override fun setExternalAudioSourceVolume(sourcePos: Long?, volume: Long?) { | ||
this.sourcePos = sourcePos!!.toInt() | ||
rtcEngine?.setExternalAudioSourceVolume(this.sourcePos, volume!!.toInt()) | ||
} | ||
|
||
override fun startAudioRecord(sampleRate: Long?, channels: Long?) { | ||
activity.get()?.apply { | ||
AudioRecordService.start(this, this::class.java, sampleRate!!.toInt(), channels!!.toInt()) | ||
} | ||
} | ||
|
||
override fun stopAudioRecord() { | ||
activity.get()?.apply { | ||
val intent = Intent(this, AudioRecordService::class.java) | ||
stopService(intent) | ||
} | ||
} | ||
} |
Oops, something went wrong.