Skip to content

Commit

Permalink
feat: [Android] Implement custom audio source example (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
littleGnAl committed Oct 26, 2021
1 parent 17a4493 commit 7b5e8e5
Show file tree
Hide file tree
Showing 14 changed files with 1,032 additions and 0 deletions.
9 changes: 9 additions & 0 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
Expand Down
5 changes: 5 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.agora.agora_rtc_engine_example">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
Expand Down Expand Up @@ -44,5 +45,9 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<service
android:name="io.agora.agora_rtc_engine_example.custom_audio_source.AudioRecordService"
android:exported="false" />
</application>
</manifest>
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)
}
}
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;
}
}
}
}
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
}
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)
}
}
}
Loading

0 comments on commit 7b5e8e5

Please sign in to comment.