mirror of
git://f0xx.org/ac/ac-mobile-android
synced 2026-08-12 18:12:13 +03:00
SFU M2: MediaProjection screen share publish in lab.
Add ScreenCapturerAndroid path with foreground service, ICE servers from SFU health API (coturn), and unit tests for SfuIceServers parsing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -160,6 +160,11 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<service
|
||||
android:name=".sfu.SfuLabForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<service
|
||||
android:name=".receiver.ReceiverCastService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.foxx.androidcast.sfu;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.webrtc.PeerConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** ICE server list from SFU health API (coturn + public STUN). */
|
||||
public final class SfuIceServers {
|
||||
private SfuIceServers() {}
|
||||
|
||||
public static List<PeerConnection.IceServer> defaults() {
|
||||
List<PeerConnection.IceServer> ice = new ArrayList<>();
|
||||
ice.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||
ice.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
||||
return ice;
|
||||
}
|
||||
|
||||
public static List<PeerConnection.IceServer> fromHealth(JSONObject health) {
|
||||
if (health == null) {
|
||||
return defaults();
|
||||
}
|
||||
JSONObject sfu = health.optJSONObject("sfu");
|
||||
if (sfu == null) {
|
||||
return defaults();
|
||||
}
|
||||
JSONArray servers = sfu.optJSONArray("ice_servers");
|
||||
if (servers == null || servers.length() == 0) {
|
||||
return defaults();
|
||||
}
|
||||
List<PeerConnection.IceServer> ice = new ArrayList<>();
|
||||
for (int i = 0; i < servers.length(); i++) {
|
||||
JSONObject entry = servers.optJSONObject(i);
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
PeerConnection.IceServer server = parseEntry(entry);
|
||||
if (server != null) {
|
||||
ice.add(server);
|
||||
}
|
||||
}
|
||||
return ice.isEmpty() ? defaults() : ice;
|
||||
}
|
||||
|
||||
private static PeerConnection.IceServer parseEntry(JSONObject entry) {
|
||||
Object urlsRaw = entry.opt("urls");
|
||||
List<String> urls = new ArrayList<>();
|
||||
if (urlsRaw instanceof JSONArray arr) {
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
String u = arr.optString(i, "").trim();
|
||||
if (!u.isEmpty()) {
|
||||
urls.add(u);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String u = entry.optString("urls", "").trim();
|
||||
if (!u.isEmpty()) {
|
||||
urls.add(u);
|
||||
}
|
||||
}
|
||||
if (urls.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
PeerConnection.IceServer.Builder builder = PeerConnection.IceServer.builder(urls);
|
||||
String user = entry.optString("username", entry.optString("user", ""));
|
||||
String pass = entry.optString("credential", entry.optString("password", ""));
|
||||
if (!user.isEmpty()) {
|
||||
builder.setUsername(user);
|
||||
}
|
||||
if (!pass.isEmpty()) {
|
||||
builder.setPassword(pass);
|
||||
}
|
||||
return builder.createIceServer();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.foxx.androidcast.sfu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -20,10 +21,12 @@ import com.foxx.androidcast.R;
|
||||
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
|
||||
/** Dev-only SFU lab: libwebrtc publish/subscribe to Janus VideoRoom (M1). */
|
||||
/** Dev-only SFU lab: libwebrtc publish/subscribe to Janus VideoRoom (M1 camera, M2 screen). */
|
||||
public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.Callback {
|
||||
private static final int REQ_LOGIN = 9108;
|
||||
|
||||
private int pendingScreenRoomId;
|
||||
|
||||
private SurfaceViewRenderer localVideo;
|
||||
private SurfaceViewRenderer remoteVideo;
|
||||
private TextView logView;
|
||||
@@ -37,6 +40,16 @@ public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.C
|
||||
refreshAuthStatus();
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<Intent> screenCaptureLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() != RESULT_OK || result.getData() == null) {
|
||||
Toast.makeText(this, R.string.sfu_lab_screen_denied, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
SfuLabForegroundService.start(this);
|
||||
session.publishScreenShare(pendingScreenRoomId, result.getData());
|
||||
});
|
||||
|
||||
public static void openIfAvailable(Context context) {
|
||||
if (!SfuRelayGate.isAvailable(context)) {
|
||||
Toast.makeText(context, R.string.sfu_lab_unavailable, Toast.LENGTH_LONG).show();
|
||||
@@ -83,6 +96,7 @@ public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.C
|
||||
|
||||
findViewById(R.id.btn_sfu_create_room).setOnClickListener(v -> createRoom());
|
||||
findViewById(R.id.btn_sfu_publish).setOnClickListener(v -> publish());
|
||||
findViewById(R.id.btn_sfu_publish_screen).setOnClickListener(v -> publishScreen());
|
||||
findViewById(R.id.btn_sfu_subscribe).setOnClickListener(v -> subscribe());
|
||||
findViewById(R.id.btn_sfu_stop).setOnClickListener(v -> stopSession());
|
||||
|
||||
@@ -93,6 +107,7 @@ public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.C
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopSession();
|
||||
SfuLabForegroundService.stop(this);
|
||||
localVideo.release();
|
||||
remoteVideo.release();
|
||||
super.onDestroy();
|
||||
@@ -146,6 +161,21 @@ public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.C
|
||||
session.publishCamera(roomId);
|
||||
}
|
||||
|
||||
private void publishScreen() {
|
||||
int roomId = parseInt(((EditText) findViewById(R.id.edit_sfu_room_id)).getText().toString(), 0);
|
||||
if (roomId <= 0) {
|
||||
Toast.makeText(this, R.string.sfu_lab_room_required, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (!SfuAuthStore.hasSession(this)) {
|
||||
Toast.makeText(this, R.string.sfu_lab_need_login, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
pendingScreenRoomId = roomId;
|
||||
MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||
screenCaptureLauncher.launch(mgr.createScreenCaptureIntent());
|
||||
}
|
||||
|
||||
private void subscribe() {
|
||||
int roomId = parseInt(((EditText) findViewById(R.id.edit_sfu_room_id)).getText().toString(), 0);
|
||||
int feedId = parseInt(((EditText) findViewById(R.id.edit_sfu_feed_id)).getText().toString(), 0);
|
||||
@@ -164,6 +194,7 @@ public class SfuLabActivity extends AppCompatActivity implements SfuRtcSession.C
|
||||
if (session != null) {
|
||||
session.stop();
|
||||
}
|
||||
SfuLabForegroundService.stop(this);
|
||||
}
|
||||
|
||||
private static int parseInt(String raw, int fallback) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.foxx.androidcast.sfu;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.foxx.androidcast.CastNotifications;
|
||||
import com.foxx.androidcast.R;
|
||||
|
||||
/**
|
||||
* Foreground service holder for SFU lab MediaProjection screen capture (Android 10+).
|
||||
*/
|
||||
public final class SfuLabForegroundService extends Service {
|
||||
private static volatile boolean running;
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, SfuLabForegroundService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent);
|
||||
} else {
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stop(Context context) {
|
||||
context.stopService(new Intent(context, SfuLabForegroundService.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
CastNotifications.createChannels(this);
|
||||
running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
startForeground(
|
||||
CastNotifications.ID_SENDER + 200,
|
||||
CastNotifications.sender(
|
||||
this,
|
||||
getString(R.string.sfu_lab_screen_active),
|
||||
SfuLabForegroundService.class));
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
running = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.foxx.androidcast.sfu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -17,14 +18,17 @@ import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.RtpReceiver;
|
||||
import org.webrtc.ScreenCapturerAndroid;
|
||||
import org.webrtc.SdpObserver;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import android.media.projection.MediaProjection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -50,7 +54,7 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
private JanusWebSocketClient janusWs;
|
||||
private VideoSource videoSource;
|
||||
private VideoTrack localVideo;
|
||||
private CameraVideoCapturer capturer;
|
||||
private VideoCapturer capturer;
|
||||
private SurfaceTextureHelper textureHelper;
|
||||
private SurfaceViewRenderer localRenderer;
|
||||
private SurfaceViewRenderer remoteRenderer;
|
||||
@@ -93,28 +97,12 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
}
|
||||
|
||||
public void publishCamera(int roomId) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ensureFactory(appContext);
|
||||
SfuSignalingClient api = new SfuSignalingClient(appContext);
|
||||
startCameraCapture();
|
||||
peerConnection = createPeerConnection();
|
||||
if (localVideo != null) {
|
||||
peerConnection.addTrack(localVideo, List.of("stream"));
|
||||
}
|
||||
AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
|
||||
AudioTrack audioTrack = factory.createAudioTrack("audio0", audioSource);
|
||||
peerConnection.addTrack(audioTrack, List.of("stream"));
|
||||
SessionDescription offer = createOffer(peerConnection);
|
||||
peerConnection.setLocalDescription(simpleSdpObserver("local"), offer);
|
||||
JSONObject join = api.joinPublisher(roomId, offer.type.canonicalForm(), offer.description);
|
||||
applyJoinResponse(join);
|
||||
log("Publisher joined room " + roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "publish", e);
|
||||
callback.onError(e.getMessage() != null ? e.getMessage() : "publish failed");
|
||||
}
|
||||
});
|
||||
executor.execute(() -> runPublish(roomId, this::startCameraCapture));
|
||||
}
|
||||
|
||||
/** M2: publish device screen via MediaProjection (matches browser getDisplayMedia lab path). */
|
||||
public void publishScreenShare(int roomId, Intent projectionData) {
|
||||
executor.execute(() -> runPublish(roomId, () -> startScreenCapture(projectionData)));
|
||||
}
|
||||
|
||||
public void subscribe(int roomId, int feedId) {
|
||||
@@ -122,7 +110,7 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
try {
|
||||
ensureFactory(appContext);
|
||||
SfuSignalingClient api = new SfuSignalingClient(appContext);
|
||||
peerConnection = createPeerConnection();
|
||||
peerConnection = createPeerConnection(loadIceServers(api));
|
||||
peerConnection.addTransceiver(org.webrtc.MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO,
|
||||
new org.webrtc.RtpTransceiver.RtpTransceiverInit(
|
||||
org.webrtc.RtpTransceiver.RtpTransceiverDirection.RECV_ONLY));
|
||||
@@ -147,19 +135,7 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
janusWs.close();
|
||||
janusWs = null;
|
||||
}
|
||||
if (capturer != null) {
|
||||
try {
|
||||
capturer.stopCapture();
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
capturer.dispose();
|
||||
capturer = null;
|
||||
}
|
||||
if (textureHelper != null) {
|
||||
textureHelper.dispose();
|
||||
textureHelper = null;
|
||||
}
|
||||
disposeCapturer();
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
videoSource = null;
|
||||
@@ -175,6 +151,29 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
});
|
||||
}
|
||||
|
||||
private void runPublish(int roomId, Runnable startCapture) {
|
||||
try {
|
||||
ensureFactory(appContext);
|
||||
SfuSignalingClient api = new SfuSignalingClient(appContext);
|
||||
startCapture.run();
|
||||
peerConnection = createPeerConnection(loadIceServers(api));
|
||||
if (localVideo != null) {
|
||||
peerConnection.addTrack(localVideo, List.of("stream"));
|
||||
}
|
||||
AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
|
||||
AudioTrack audioTrack = factory.createAudioTrack("audio0", audioSource);
|
||||
peerConnection.addTrack(audioTrack, List.of("stream"));
|
||||
SessionDescription offer = createOffer(peerConnection);
|
||||
peerConnection.setLocalDescription(simpleSdpObserver("local"), offer);
|
||||
JSONObject join = api.joinPublisher(roomId, offer.type.canonicalForm(), offer.description);
|
||||
applyJoinResponse(join);
|
||||
log("Publisher joined room " + roomId);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "publish", e);
|
||||
callback.onError(e.getMessage() != null ? e.getMessage() : "publish failed");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyJoinResponse(JSONObject join) throws org.json.JSONException {
|
||||
long sessionId = join.getLong("session_id");
|
||||
long handleId = join.getLong("handle_id");
|
||||
@@ -207,10 +206,16 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
});
|
||||
}
|
||||
|
||||
private PeerConnection createPeerConnection() {
|
||||
List<PeerConnection.IceServer> ice = new ArrayList<>();
|
||||
ice.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
|
||||
ice.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer());
|
||||
private List<PeerConnection.IceServer> loadIceServers(SfuSignalingClient api) {
|
||||
try {
|
||||
return SfuIceServers.fromHealth(api.health());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "ICE from health failed, using defaults", e);
|
||||
return SfuIceServers.defaults();
|
||||
}
|
||||
}
|
||||
|
||||
private PeerConnection createPeerConnection(List<PeerConnection.IceServer> ice) {
|
||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(ice);
|
||||
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
||||
PeerConnection pc = factory.createPeerConnection(config, this);
|
||||
@@ -236,16 +241,50 @@ public final class SfuRtcSession implements PeerConnection.Observer {
|
||||
throw new IllegalStateException("No camera");
|
||||
}
|
||||
capturer = enumerator.createCapturer(device, null);
|
||||
startVideoCapture(false, 1280, 720, 30);
|
||||
}
|
||||
|
||||
private void startScreenCapture(Intent projectionData) {
|
||||
if (projectionData == null) {
|
||||
throw new IllegalStateException("Screen capture consent missing");
|
||||
}
|
||||
capturer = new ScreenCapturerAndroid(projectionData, new MediaProjection.Callback() {
|
||||
@Override
|
||||
public void onStop() {
|
||||
log("MediaProjection stopped");
|
||||
stop();
|
||||
}
|
||||
});
|
||||
startVideoCapture(true, 1280, 720, 15);
|
||||
}
|
||||
|
||||
private void startVideoCapture(boolean screencast, int width, int height, int fps) {
|
||||
textureHelper = SurfaceTextureHelper.create("SfuCap", eglBase.getEglBaseContext());
|
||||
videoSource = factory.createVideoSource(capturer.isScreencast());
|
||||
videoSource = factory.createVideoSource(screencast);
|
||||
capturer.initialize(textureHelper, appContext, videoSource.getCapturerObserver());
|
||||
capturer.startCapture(1280, 720, 30);
|
||||
capturer.startCapture(width, height, fps);
|
||||
localVideo = factory.createVideoTrack("video0", videoSource);
|
||||
if (localRenderer != null) {
|
||||
localVideo.addSink(localRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
private void disposeCapturer() {
|
||||
if (capturer != null) {
|
||||
try {
|
||||
capturer.stopCapture();
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
capturer.dispose();
|
||||
capturer = null;
|
||||
}
|
||||
if (textureHelper != null) {
|
||||
textureHelper.dispose();
|
||||
textureHelper = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SessionDescription createOffer(PeerConnection pc) {
|
||||
final SessionDescription[] holder = new SessionDescription[1];
|
||||
final Exception[] error = new Exception[1];
|
||||
|
||||
@@ -61,11 +61,17 @@
|
||||
android:id="@+id/btn_sfu_publish"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/sfu_lab_publish" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_sfu_publish_screen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/sfu_lab_publish_screen" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_sfu_subscribe"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
<string name="dev_wired_display_hint">Wired HDMI uses the OS display stack, not the cast protocol. If no second screen appears in system Display settings, adapters cannot be fixed in-app. See docs/USB_HDMI_CAST.md.</string>
|
||||
<string name="dev_external_capture_policy">External display capture policy (future)</string>
|
||||
<string name="dev_usb_tether_transport">Show USB-tether transport in cast settings (stub, roadmap E)</string>
|
||||
<string name="dev_sfu_relay">Enable Janus SFU relay lab (libwebrtc M1)</string>
|
||||
<string name="dev_sfu_relay">Enable Janus SFU relay lab (libwebrtc M1/M2)</string>
|
||||
<string name="dev_open_sfu_lab">Open SFU lab</string>
|
||||
<string name="sfu_lab_title">SFU lab</string>
|
||||
<string name="sfu_lab_unavailable">SFU lab disabled — enable in developer settings</string>
|
||||
@@ -276,6 +276,9 @@
|
||||
<string name="sfu_lab_feed_id">Publisher feed id</string>
|
||||
<string name="sfu_lab_create_room">Create room</string>
|
||||
<string name="sfu_lab_publish">Publish camera</string>
|
||||
<string name="sfu_lab_publish_screen">Publish screen share</string>
|
||||
<string name="sfu_lab_screen_active">SFU screen share active</string>
|
||||
<string name="sfu_lab_screen_denied">Screen capture permission denied</string>
|
||||
<string name="sfu_lab_subscribe">Subscribe</string>
|
||||
<string name="sfu_lab_stop">Stop</string>
|
||||
<string name="sfu_lab_local">Local preview</string>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.foxx.androidcast.sfu;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.webrtc.PeerConnection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SfuIceServersTest {
|
||||
@Test
|
||||
public void defaults_includeGoogleStun() {
|
||||
List<PeerConnection.IceServer> ice = SfuIceServers.defaults();
|
||||
assertEquals(2, ice.size());
|
||||
assertTrue(ice.get(0).urls.get(0).contains("stun:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromHealth_parsesTurnEntry() throws Exception {
|
||||
JSONObject health = new JSONObject("""
|
||||
{"sfu":{"ice_servers":[
|
||||
{"urls":"stun:stun.l.google.com:19302"},
|
||||
{"urls":"turn:10.7.16.237:3478?transport=udp","username":"acturn","credential":"secret"}
|
||||
]}}
|
||||
""");
|
||||
List<PeerConnection.IceServer> ice = SfuIceServers.fromHealth(health);
|
||||
assertEquals(2, ice.size());
|
||||
assertTrue(ice.get(1).urls.get(0).startsWith("turn:"));
|
||||
assertEquals("acturn", ice.get(1).username);
|
||||
assertEquals("secret", ice.get(1).password);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromHealth_emptyFallsBackToDefaults() throws Exception {
|
||||
JSONObject health = new JSONObject("{\"sfu\":{}}");
|
||||
List<PeerConnection.IceServer> ice = SfuIceServers.fromHealth(health);
|
||||
assertFalse(ice.isEmpty());
|
||||
assertTrue(ice.get(0).urls.get(0).contains("stun:"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user