diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java index 7ee5127..66dc6a6 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java @@ -9,14 +9,19 @@ import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.view.Display; +import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.acgist.taoyao.client.databinding.ActivityMainBinding; +import com.acgist.taoyao.config.Config; +import com.acgist.taoyao.media.MediaManager; import java.io.Serializable; @@ -25,7 +30,7 @@ import java.io.Serializable; * * @author acgist */ -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements Serializable { private MainHandler mainHandler; private ActivityMainBinding binding; @@ -96,6 +101,9 @@ public class MainActivity extends AppCompatActivity { if(!allGranted) { Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show(); } + MediaManager.getInstance().init(this.getApplicationContext()); + MediaManager.getInstance().initAudio(); + MediaManager.getInstance().initVideo(); } /** @@ -111,7 +119,7 @@ public class MainActivity extends AppCompatActivity { Log.i(MainActivity.class.getSimpleName(), "拉起媒体服务"); final Intent intent = new Intent(this, MediaService.class); if(this.mainHandler == null) { - this.mainHandler = new MainHandler(); + this.mainHandler = new MainHandler(this); } intent.setAction(MediaService.Action.CONNECT.name()); intent.putExtra("mainHandler", this.mainHandler); @@ -136,14 +144,52 @@ public class MainActivity extends AppCompatActivity { * * @author acgist */ - private static class MainHandler extends Handler implements Serializable { + public static class MainHandler extends Handler implements Serializable { + + private final MainActivity mainActivity; + + public MainHandler(MainActivity mainActivity) { + this.mainActivity = mainActivity; + } @Override public void handleMessage(@NonNull Message message) { super.handleMessage(message); - Log.d(MainActivity.class.getSimpleName(), "Handler消息:" + message.what + " - " + message.obj); + Log.d(MainHandler.class.getSimpleName(), "Handler消息:" + message.what + " - " + message.obj); + switch(message.what) { + case Config.WHAT_SCREEN_CAPTURE -> this.mainActivity.screenCapture(message); + case Config.WHAT_NEW_CLIENT_VIDEO -> this.mainActivity.newClientVideo(message); + } } } + /** + * 屏幕捕获 + * + * @param message 消息 + */ + private void screenCapture(Message message) { + final ActivityResultLauncher activityResultLauncher = this.registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if(result.getResultCode() == RESULT_OK) { + Log.i(MediaManager.class.getSimpleName(), "开始屏幕捕获"); + message.getCallback().run(); + } else { + Log.w(MainActivity.class.getSimpleName(), "屏幕捕获失败:" + result.getResultCode()); + } + } + ); + activityResultLauncher.launch((Intent) message.obj); + } + + /** + * 新建用户视频 + * + * @param message 消息 + */ + private void newClientVideo(Message message) { + } + } \ No newline at end of file diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java index a4d2a06..0867b8d 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java @@ -27,6 +27,7 @@ public class MediaService extends Service { static { System.loadLibrary("taoyao"); + System.loadLibrary("jingle_peerconnection_so"); } /** diff --git a/taoyao-client-android/taoyao/media/build.gradle b/taoyao-client-android/taoyao/media/build.gradle index c433d3f..cc60b1f 100644 --- a/taoyao-client-android/taoyao/media/build.gradle +++ b/taoyao-client-android/taoyao/media/build.gradle @@ -24,7 +24,8 @@ android { "-DLIBWEBRTC_BINARY_PATH=" + WEBRTC_LIB_PATH, "-DMEDIASOUPCLIENT_BUILD_TESTS=OFF", "-DMEDIASOUPCLIENT_LOG_TRACE=OFF", - "-DMEDIASOUPCLIENT_LOG_DEV=OFF" + "-DMEDIASOUPCLIENT_LOG_DEV=OFF", + "-DANDROID_STL=c++_shared" } } } @@ -44,10 +45,15 @@ android { version '3.22.1' } } + sourceSets { + main { + jniLibs.srcDirs = ["libs"] + } + } } dependencies { - implementation fileTree(dir: 'libs', include: ['*.a', '*.so', '*.jar']) + implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.apache.commons:commons-collections4:4.4' implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' diff --git a/taoyao-client-android/taoyao/media/libs/arm64-v8a/libjingle_peerconnection_so.so b/taoyao-client-android/taoyao/media/libs/arm64-v8a/libjingle_peerconnection_so.so new file mode 100644 index 0000000..40f3f68 Binary files /dev/null and b/taoyao-client-android/taoyao/media/libs/arm64-v8a/libjingle_peerconnection_so.so differ diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/config/Config.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/config/Config.java new file mode 100644 index 0000000..a40debc --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/config/Config.java @@ -0,0 +1,19 @@ +package com.acgist.taoyao.config; + +/** + * 配置 + * + * @author acgist + */ +public class Config { + + /** + * 屏幕捕获 + */ + public static final int WHAT_SCREEN_CAPTURE = 1000; + /** + * 新建用户视频 + */ + public static final int WHAT_NEW_CLIENT_VIDEO = 1001; + +} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/LocalClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/LocalClient.java index afafe57..ad1f1a9 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/LocalClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/LocalClient.java @@ -1,12 +1,16 @@ package com.acgist.taoyao.media; +import java.util.logging.Handler; + /** - * 房间终端 - * 使用NDK + Mediasoup实现多人会话 + * 房间本地终端 * * @author acgist */ -public class LocalClient { +public class LocalClient extends RoomClient { + public LocalClient(String name, String clientId, Handler handler) { + super(name, clientId, handler); + } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java index ce3ed18..cfec6de 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java @@ -1,8 +1,15 @@ package com.acgist.taoyao.media; import android.content.Context; +import android.content.Intent; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.Message; import android.util.Log; +import com.acgist.taoyao.config.Config; + import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.Camera2Enumerator; @@ -12,6 +19,7 @@ import org.webrtc.EglBase; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; import org.webrtc.PeerConnectionFactory; +import org.webrtc.ScreenCapturerAndroid; import org.webrtc.SurfaceTextureHelper; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoCapturer; @@ -36,6 +44,8 @@ public class MediaManager { */ public enum Type { + // 文件共享 + FILE, // 后置摄像头 BACK, // 前置摄像头 @@ -43,12 +53,33 @@ public class MediaManager { // 屏幕共享 SCREEN; + /** + * @return 是否摄像头 + */ + public boolean isCamera() { + return this == BACK || this == FRONT; + } + + + } + + private static final MediaManager INSTANCE = new MediaManager(); + + private MediaManager() { + } + + public static final MediaManager getInstance() { + return INSTANCE; } /** * 视频类型 */ private Type type; + /** + * Handler + */ + private Handler handler; /** * 上下文 */ @@ -58,21 +89,16 @@ public class MediaManager { */ private MediaStream mediaStream; /** - * 屏幕捕获 + * 视频捕获 + * FileVideoCapturer + * CameraVideoCapturer + * ScreenCapturerAndroid */ private VideoCapturer videoCapturer; - /** - * 摄像头捕获 - */ - private CameraVideoCapturer cameraVideoCapturer; /** * Peer连接工厂 */ private PeerConnectionFactory peerConnectionFactory; - /** - * 预览 - */ - private SurfaceViewRenderer previewView; static { // 设置采样 @@ -85,8 +111,17 @@ public class MediaManager { /** * 加载媒体流 + * + * @param context 上下文 */ - public void init() { + public void init(Context context) { + this.type = Type.BACK; + this.context = context; + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(this.context) + .setEnableInternalTracer(true) + .createInitializationOptions() + ); this.peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); this.mediaStream = this.peerConnectionFactory.createLocalMediaStream("ARDAMS"); } @@ -104,11 +139,10 @@ public class MediaManager { } this.type = type; Log.i(MediaManager.class.getSimpleName(), "设置视频来源:" + type); - if(this.type == Type.SCREEN || type == Type.SCREEN) { - this.initVideo(); - } else { + if(this.type.isCamera() && type.isCamera()) { // TODO:测试是否需要完全重置 - this.cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { + final CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) this.videoCapturer; + cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { @Override public void onCameraSwitchDone(boolean success) { } @@ -116,13 +150,15 @@ public class MediaManager { public void onCameraSwitchError(String message) { } }); + } else { + this.initVideo(); } } /** * 加载音频 */ - private void initAudio() { + public void initAudio() { // 关闭音频 this.closeAudioTrack(); // 加载音频 @@ -137,57 +173,68 @@ public class MediaManager { /** * 加载视频 */ - private void initVideo() { + public void initVideo() { this.closeVideoTrack(); - if(this.cameraVideoCapturer != null) { - this.cameraVideoCapturer.dispose(); + if(this.videoCapturer != null) { + this.videoCapturer.dispose(); } - final CameraEnumerator cameraEnumerator = new Camera2Enumerator(this.context); - final String[] names = cameraEnumerator.getDeviceNames(); - for(String name : names) { - if(this.type == Type.FRONT && cameraEnumerator.isFrontFacing(name)) { - Log.i(MediaManager.class.getSimpleName(), "加载前置摄像头:" + name); - this.cameraVideoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); - } else if(this.type == Type.BACK && cameraEnumerator.isBackFacing(name)) { - Log.i(MediaManager.class.getSimpleName(), "加载后置摄像头:" + name); - this.cameraVideoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); - } else { - Log.i(MediaManager.class.getSimpleName(), "忽略摄像头:" + name); + if(this.type.isCamera()) { + final CameraEnumerator cameraEnumerator = new Camera2Enumerator(this.context); + final String[] names = cameraEnumerator.getDeviceNames(); + for(String name : names) { + if(this.type == Type.FRONT && cameraEnumerator.isFrontFacing(name)) { + Log.i(MediaManager.class.getSimpleName(), "加载视频(前置摄像头):" + name); + this.videoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); + } else if(this.type == Type.BACK && cameraEnumerator.isBackFacing(name)) { + Log.i(MediaManager.class.getSimpleName(), "加载视频(后置摄像头):" + name); + this.videoCapturer = cameraEnumerator.createCapturer(name, new MediaCameraEventsHandler()); + } else { + Log.d(MediaManager.class.getSimpleName(), "忽略摄像头:" + name); + } } + } else if(this.type == Type.FILE) { + Log.i(MediaManager.class.getSimpleName(), "加载视频(文件)"); + } else if(this.type == Type.SCREEN) { + Log.i(MediaManager.class.getSimpleName(), "加载视频(录屏)"); + final MediaProjectionManager mediaProjectionManager = this.context.getSystemService(MediaProjectionManager.class); + final Intent intent = mediaProjectionManager.createScreenCaptureIntent(); + final Message message = Message.obtain(this.handler, () -> { + this.videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() { + @Override + public void onStop() { + super.onStop(); + Log.i(MediaManager.class.getSimpleName(), "停止屏幕捕获"); + } + }); + }); + message.obj = intent; + message.what = Config.WHAT_SCREEN_CAPTURE; + this.handler.sendMessage(message); +// this.handler.dispatchMessage(message); } + this.initVideoTrack(); } /** * 加载视频 */ private void initVideoTrack() { - SurfaceTextureHelper surfaceTextureHelper = null; // 设置预览 - if(this.previewView != null) { - final EglBase eglBase = EglBase.create(); - surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", eglBase.getEglBaseContext()); - this.previewView.setMirror(true); - this.previewView.setEnableHardwareScaler(true); - } - // 是否捕获屏幕 - final boolean screen = this.type == Type.SCREEN; - final VideoSource videoSource = this.peerConnectionFactory.createVideoSource(screen); - if(screen) { - Log.i(MediaManager.class.getSimpleName(), "捕获屏幕"); - } else { - Log.i(MediaManager.class.getSimpleName(), "捕获摄像头"); - this.cameraVideoCapturer.initialize(surfaceTextureHelper, this.context, videoSource.getCapturerObserver()); - this.cameraVideoCapturer.startCapture(640, 480, 30); - } + final SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this.context); + surfaceViewRenderer.setMirror(true); + surfaceViewRenderer.setEnableHardwareScaler(true); + // 加载视频 + final EglBase eglBase = EglBase.create(); + final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", eglBase.getEglBaseContext()); + final VideoSource videoSource = this.peerConnectionFactory.createVideoSource(this.videoCapturer.isScreencast()); + this.videoCapturer.initialize(surfaceTextureHelper, this.context, videoSource.getCapturerObserver()); + this.videoCapturer.startCapture(640, 480, 30); final VideoTrack videoTrack = this.peerConnectionFactory.createVideoTrack("ARDAMSv0", videoSource); videoTrack.setEnabled(true); this.mediaStream.addTrack(videoTrack); Log.i(MediaManager.class.getSimpleName(), "加载视频:" + videoTrack.id()); } - private void initPreview() { - } - public void pauseAudio() { synchronized (this.mediaStream.audioTracks) { this.mediaStream.audioTracks.forEach(a -> a.setEnabled(false)); @@ -264,14 +311,17 @@ public class MediaManager { * 释放资源 */ public void close() { - if(this.cameraVideoCapturer != null) { - this.cameraVideoCapturer.dispose(); + if(this.videoCapturer != null) { + this.videoCapturer.dispose(); + this.videoCapturer = null; } if(this.mediaStream != null) { this.mediaStream.dispose(); + this.mediaStream = null; } if(this.peerConnectionFactory != null) { this.peerConnectionFactory.dispose(); + this.peerConnectionFactory = null; } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RemoteClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RemoteClient.java index 37161c3..34ecc9f 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RemoteClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RemoteClient.java @@ -1,4 +1,9 @@ package com.acgist.taoyao.media; +/** + * 房间远程终端 + * + * @author acgist + */ public class RemoteClient { } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RoomClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RoomClient.java new file mode 100644 index 0000000..9216fe4 --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/RoomClient.java @@ -0,0 +1,37 @@ +package com.acgist.taoyao.media; + +import org.webrtc.AudioTrack; +import org.webrtc.MediaStream; +import org.webrtc.VideoTrack; + +import java.util.logging.Handler; + +/** + * 房间终端 + * 使用NDK + Mediasoup实现多人会话 + * + * @author acgist + */ +public class RoomClient { + + protected final String name; + protected final String clientId; + protected final Handler handler; + protected AudioTrack audioTrack; + protected VideoTrack videoTrack; + protected MediaStream mediaStream; + + public RoomClient(String name, String clientId, Handler handler) { + this.name = name; + this.clientId = clientId; + this.handler = handler; + } + + /** + * 打开预览 + */ + private void preview() { + + } + +}