Skip to content

Commit 67afe64

Browse files
author
xianing
committed
add more features in mediaPlayer kit.
add custom audio renderer feature.
1 parent 1e79b38 commit 67afe64

7 files changed

Lines changed: 321 additions & 95 deletions

File tree

Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayerKit.java

Lines changed: 84 additions & 71 deletions
Large diffs are not rendered by default.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.agora.api.example.examples.advanced.customaudio;
2+
3+
import android.media.AudioFormat;
4+
import android.media.AudioTrack;
5+
import android.util.Log;
6+
7+
public class AudioPlayer {
8+
9+
private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;
10+
private static final String TAG = "AudioPlayer";
11+
12+
private AudioTrack mAudioTrack;
13+
private AudioStatus mAudioStatus = AudioStatus.STOPPED ;
14+
15+
public AudioPlayer(int streamType, int sampleRateInHz, int channelConfig, int audioFormat){
16+
if(mAudioStatus == AudioStatus.STOPPED) {
17+
int Val = 0;
18+
if(1 == channelConfig)
19+
Val = AudioFormat.CHANNEL_OUT_MONO;
20+
else if(2 == channelConfig)
21+
Val = AudioFormat.CHANNEL_OUT_STEREO;
22+
else
23+
Log.e(TAG, "channelConfig is wrong !");
24+
25+
int mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, Val, audioFormat);
26+
Log.e(TAG, " sampleRateInHz :" + sampleRateInHz + " channelConfig :" + channelConfig + " audioFormat: " + audioFormat + " mMinBufferSize: " + mMinBufferSize);
27+
if (mMinBufferSize == AudioTrack.ERROR_BAD_VALUE) {
28+
Log.e(TAG,"AudioTrack.ERROR_BAD_VALUE : " + AudioTrack.ERROR_BAD_VALUE) ;
29+
}
30+
31+
mAudioTrack = new AudioTrack(streamType, sampleRateInHz, Val, audioFormat, mMinBufferSize, DEFAULT_PLAY_MODE);
32+
if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
33+
throw new RuntimeException("Error on AudioTrack created");
34+
}
35+
mAudioStatus = AudioStatus.INITIALISING;
36+
}
37+
Log.e(TAG, "mAudioStatus: " + mAudioStatus);
38+
}
39+
40+
public boolean startPlayer() {
41+
if(mAudioStatus == AudioStatus.INITIALISING) {
42+
mAudioTrack.play();
43+
mAudioStatus = AudioStatus.RUNNING;
44+
}
45+
Log.e("AudioPlayer", "mAudioStatus: " + mAudioStatus);
46+
return true;
47+
}
48+
49+
public void stopPlayer() {
50+
if(null != mAudioTrack){
51+
mAudioStatus = AudioStatus.STOPPED;
52+
mAudioTrack.stop();
53+
mAudioTrack.release();
54+
mAudioTrack = null;
55+
}
56+
Log.e(TAG, "mAudioStatus: " + mAudioStatus);
57+
}
58+
59+
public boolean play(byte[] audioData, int offsetInBytes, int sizeInBytes) {
60+
if(mAudioStatus == AudioStatus.RUNNING) {
61+
mAudioTrack.write(audioData, offsetInBytes, sizeInBytes);
62+
}else{
63+
Log.e(TAG, "=== No data to AudioTrack !! mAudioStatus: " + mAudioStatus);
64+
}
65+
return true;
66+
}
67+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.agora.api.example.examples.advanced.customaudio;
2+
3+
public enum AudioStatus {
4+
INITIALISING,
5+
RUNNING,
6+
STOPPED
7+
}

Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import android.content.Context;
44
import android.content.Intent;
5+
import android.media.AudioFormat;
6+
import android.media.AudioManager;
7+
import android.os.AsyncTask;
58
import android.os.Bundle;
69
import android.os.Handler;
710
import android.text.TextUtils;
@@ -46,6 +49,9 @@ public class CustomAudioSource extends BaseFragment implements View.OnClickListe
4649
private int myUid;
4750
private boolean joined = false;
4851
public static RtcEngine engine;
52+
private static final Integer SAMPLE_RATE = 44100;
53+
private static final Integer SAMPLE_NUM_OF_CHANNEL = 1;
54+
private AudioPlayer mAudioPlayer;
4955

5056
@Override
5157
public void onCreate(@Nullable Bundle savedInstanceState)
@@ -93,6 +99,14 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState)
9399
* The SDK uses this class to report to the app on SDK runtime events.*/
94100
engine = RtcEngine.create(getContext().getApplicationContext(), getString(R.string.agora_app_id),
95101
iRtcEngineEventHandler);
102+
103+
// Notify the SDK that you want to use the external audio sink.
104+
engine.setExternalAudioSink(
105+
true, // Enable the external audio sink.
106+
SAMPLE_RATE, // Set the audio sample rate as 8k, 16k, 32k, 44.1k or 48kHz.
107+
SAMPLE_NUM_OF_CHANNEL // Number of channels. The maximum number is 2.
108+
);
109+
mAudioPlayer = new AudioPlayer(AudioManager.STREAM_VOICE_CALL, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, AudioFormat.CHANNEL_OUT_MONO);
96110
}
97111
catch (Exception e)
98112
{
@@ -113,6 +127,8 @@ public void onDestroy()
113127
}
114128
handler.post(RtcEngine::destroy);
115129
engine = null;
130+
mAudioPlayer.stopPlayer();
131+
playerTask.cancel(true);
116132
}
117133

118134
@Override
@@ -243,6 +259,30 @@ private void stopAudioRecord()
243259
getActivity().stopService(intent);
244260
}
245261

262+
private final AsyncTask playerTask = new AsyncTask() {
263+
@Override
264+
protected Object doInBackground(Object[] objects) {
265+
while (true) {
266+
if (engine != null) {
267+
int length = SAMPLE_RATE / 1000 * 2 * SAMPLE_NUM_OF_CHANNEL * 10;
268+
byte[] data = new byte[length];
269+
/**
270+
* Pulls the remote audio frame.
271+
* Before calling this method, call the setExternalAudioSink(enabled: true) method to enable and set the external audio sink.
272+
* After a successful method call, the app pulls the decoded and mixed audio data for playback.
273+
* @Param data: The audio data that you want to pull. The data format is in byte[].
274+
* @Param lengthInByte: The data length (byte) of the external audio data. The value of this parameter is related to the audio duration,
275+
* and the values of the sampleRate and channels parameters that you set in setExternalAudioSink. Agora recommends setting the audio duration no shorter than 10 ms.
276+
* The formula for lengthInByte is:
277+
* lengthInByte = sampleRate/1000 × 2 × channels × audio duration (ms).
278+
*/
279+
engine.pullPlaybackAudioFrame(data, length);
280+
mAudioPlayer.play(data, 0, length);
281+
}
282+
}
283+
}
284+
};
285+
246286
/**IRtcEngineEventHandler is an abstract class providing default implementation.
247287
* The SDK uses this class to report to the app on SDK runtime events.*/
248288
private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
@@ -326,6 +366,8 @@ public void run()
326366
public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed)
327367
{
328368
super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
369+
mAudioPlayer.startPlayer();
370+
playerTask.execute();
329371
Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
330372
}
331373
};

Android/APIExample/app/src/main/res/layout/fragment_media_player_kit.xml

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,126 @@
55
android:layout_height="match_parent"
66
tools:context=".examples.advanced.MediaPlayerKit">
77

8-
<View
9-
android:id="@+id/strut1"
10-
android:layout_width="0dp"
11-
android:layout_height="0dp"
12-
android:layout_centerVertical="true" />
13-
14-
<FrameLayout
15-
android:id="@+id/fl_local"
8+
<LinearLayout
9+
android:id="@+id/video_container"
1610
android:layout_width="match_parent"
1711
android:layout_height="match_parent"
18-
android:layout_alignParentStart="true"
19-
android:layout_alignTop="@id/strut1"
20-
android:layout_alignBottom="@id/ll_join"
21-
android:layout_alignParentEnd="true" />
12+
android:layout_alignParentTop="true"
13+
android:layout_marginBottom="186dp"
14+
android:orientation="vertical">
15+
16+
<FrameLayout
17+
android:id="@+id/fl_local"
18+
android:layout_weight="1.0"
19+
android:layout_width="match_parent"
20+
android:layout_height="wrap_content" />
21+
22+
<FrameLayout
23+
android:id="@+id/fl_remote"
24+
android:layout_marginTop="1dp"
25+
android:layout_weight="1.0"
26+
android:layout_width="match_parent"
27+
android:layout_height="wrap_content" />
28+
</LinearLayout>
29+
30+
2231

23-
<FrameLayout
24-
android:id="@+id/fl_remote"
32+
<LinearLayout
33+
android:id="@+id/player_controller"
2534
android:layout_width="match_parent"
26-
android:layout_height="match_parent"
27-
android:layout_alignParentStart="true"
28-
android:layout_alignBottom="@id/strut1"
29-
android:layout_alignParentEnd="true" />
35+
android:layout_height="wrap_content"
36+
android:layout_alignBottom="@id/ctrl_progress_bar"
37+
android:layout_marginBottom="30dp"
38+
android:gravity="center_vertical"
39+
android:orientation="horizontal">
40+
41+
<androidx.appcompat.widget.AppCompatButton
42+
android:id="@+id/play"
43+
android:layout_width="wrap_content"
44+
android:layout_height="wrap_content"
45+
android:layout_weight="1.0"
46+
android:textSize="10sp"
47+
android:enabled="false"
48+
android:text="@string/play" />
49+
50+
<androidx.appcompat.widget.AppCompatButton
51+
android:id="@+id/stop"
52+
android:layout_width="wrap_content"
53+
android:layout_height="wrap_content"
54+
android:layout_weight="1.0"
55+
android:layout_marginLeft="10dp"
56+
android:textSize="10sp"
57+
android:enabled="false"
58+
android:text="@string/stop" />
59+
60+
<androidx.appcompat.widget.AppCompatButton
61+
android:id="@+id/pause"
62+
android:layout_width="wrap_content"
63+
android:layout_height="wrap_content"
64+
android:layout_weight="1.0"
65+
android:layout_marginLeft="10dp"
66+
android:textSize="10sp"
67+
android:enabled="false"
68+
android:text="@string/pause" />
69+
70+
<androidx.appcompat.widget.AppCompatButton
71+
android:id="@+id/publish"
72+
android:layout_width="wrap_content"
73+
android:layout_height="wrap_content"
74+
android:layout_weight="1.0"
75+
android:layout_marginLeft="10dp"
76+
android:text="@string/publish"
77+
android:enabled="false"
78+
android:textSize="10sp" />
79+
80+
<androidx.appcompat.widget.AppCompatButton
81+
android:id="@+id/unpublish"
82+
android:layout_width="wrap_content"
83+
android:layout_height="wrap_content"
84+
android:layout_weight="2.0"
85+
android:layout_marginLeft="10dp"
86+
android:text="@string/unpublish"
87+
android:enabled="false"
88+
android:textSize="10sp" />
89+
90+
91+
</LinearLayout>
3092

3193
<SeekBar
3294
android:id="@+id/ctrl_progress_bar"
3395
android:layout_width="match_parent"
3496
android:layout_height="wrap_content"
35-
android:layout_marginBottom="86dp"
97+
android:layout_marginBottom="50dp"
3698
android:layout_marginStart="16dp"
3799
android:layout_marginEnd="16dp"
38-
android:layout_alignBottom="@id/ll_join" />
100+
android:layout_alignBottom="@id/link_box" />
101+
102+
<LinearLayout
103+
android:id="@+id/link_box"
104+
android:layout_width="match_parent"
105+
android:layout_height="wrap_content"
106+
android:layout_alignBottom="@id/ll_join"
107+
android:layout_marginBottom="45dp"
108+
android:gravity="center_vertical"
109+
android:orientation="horizontal">
110+
111+
<androidx.appcompat.widget.AppCompatEditText
112+
android:id="@+id/link"
113+
android:layout_width="0dp"
114+
android:layout_height="wrap_content"
115+
android:layout_weight="1"
116+
android:hint="@string/channel_id"
117+
android:singleLine="true"
118+
android:digits="@string/chanel_support_char"/>
119+
120+
<androidx.appcompat.widget.AppCompatButton
121+
android:id="@+id/open"
122+
android:layout_width="wrap_content"
123+
android:layout_height="wrap_content"
124+
android:enabled="false"
125+
android:text="@string/open" />
126+
127+
</LinearLayout>
39128

40129
<LinearLayout
41130
android:id="@+id/ll_join"

Android/APIExample/app/src/main/res/values-zh/strings.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
<string name="encryption_password">加密密钥</string>
1212
<string name="join">加入</string>
1313
<string name="blur">打开模糊</string>
14+
<string name="open">打开</string>
15+
<string name="play">播放</string>
16+
<string name="pause">暂停</string>
17+
<string name="unpublish">停止发流</string>
1418
<string name="closeblur">关闭模糊</string>
1519
<string name="leave">离开</string>
1620
<string name="stop">停止</string>
@@ -71,7 +75,7 @@
7175
<string name="item_pushexternal">自定义视频采集(Push)</string>
7276
<string name="item_switchexternal">自定义视频采集(MediaIO)/屏幕共享</string>
7377
<string name="item_cameraorscreen">摄像头和屏幕共享切换</string>
74-
<string name="item_customaudiosource">自定义音频采集</string>
78+
<string name="item_customaudiosource">自定义音频采集/渲染</string>
7579
<string name="item_customremoterender">自定义视频渲染</string>
7680
<string name="item_processraw">原始音频/视频数据</string>
7781
<string name="item_videometadata">插入视频元数据(SEI)</string>
@@ -84,7 +88,7 @@
8488
<string name="joinchannelvideo">此示例演示了如何使用SDK加入频道进行音视频通话的功能。</string>
8589
<string name="adjustvolume">此示例演示了如何使用SDK在频道中调整通话音量的功能。</string>
8690
<string name="precalltest">此示例演示了如何使用SDK在进入频道前检测网络质量状况。</string>
87-
<string name="customaudio">此示例演示了在语音通话过程中如何自采集音频帧并主动推送的功能。</string>
91+
<string name="customaudio">此示例演示了在语音通话过程中如何使用CustomSource和CustomSink接口进行自采集音频帧和自渲染音频帧的功能。</string>
8892
<string name="customremoterender">此示例演示了在音视频通话过程中如何自定义远端视频流渲染器的功能。</string>
8993
<string name="processrawdata">此示例演示了在音视频通话过程中如何通过回调获取裸数据,以及在数据被处理后如何返回给SDK的功能。\nPS:裸数据包括视频帧和音频帧。</string>
9094
<string name="pushexternalvideo">此示例演示了在音视频通话过程中如何以主动Push的方式进行视频自采集的功能。</string>

Android/APIExample/app/src/main/res/values/strings.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
<string name="encryption_password">Encryption Password</string>
1111
<string name="join">Join</string>
1212
<string name="blur">Open Blur</string>
13+
<string name="open">Open</string>
1314
<string name="closeblur">Close Blur</string>
1415
<string name="leave">Leave</string>
1516
<string name="stop">Stop</string>
17+
<string name="play">Play</string>
18+
<string name="pause">Pause</string>
19+
<string name="unpublish">Unpublish</string>
1620
<string name="permission">Please granted the request permissions</string>
1721
<string name="earpiece">Earpiece</string>
1822
<string name="speaker">Speaker</string>
@@ -74,7 +78,7 @@
7478
<string name="item_pushexternal">Custom Video Source(Push)</string>
7579
<string name="item_switchexternal">Custom Video Source(MediaIO)/Share the Screen</string>
7680
<string name="item_cameraorscreen">Switch Camera and Screen Share</string>
77-
<string name="item_customaudiosource">Custom Audio Source</string>
81+
<string name="item_customaudiosource">Custom Audio Source/Renderer</string>
7882
<string name="item_customremoterender">Custom Video Renderer</string>
7983
<string name="item_processraw">Raw Video/Audio Data</string>
8084
<string name="item_videometadata">Video Metadata(SEI)</string>
@@ -88,7 +92,7 @@
8892
<string name="joinchannelvideo">This example demonstrates how to use the SDK to join channels for audio and video calls.</string>
8993
<string name="adjustvolume">This example demonstrates how to use the SDK to adjust in cal volume.</string>
9094
<string name="precalltest">This example demonstrates how to use the SDK to check uplink network condition before joining the channel.</string>
91-
<string name="customaudio">This example demonstrates how to automatically collect audio frames and actively push them during a voice call.</string>
95+
<string name="customaudio">This example demonstrates how to use custom audio source and custom audio sink API to do Audio capture and renderer. </string>
9296
<string name="customremoterender">This example demonstrates how to customize the functions of the remote video stream renderer during audio and video calls.</string>
9397
<string name="processrawdata">This example demonstrates how to obtain raw data through callback during audio and video calls, and how to return the data to the SDK after processing.\nPS:Bare data includes video frames and audio frames</string>
9498
<string name="pushexternalvideo">This example demonstrates how to use the active push mode to collect video during audio and video calls.</string>

0 commit comments

Comments
 (0)