From f9de241e092080f3ce60d207ac4289017c5467fc Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Thu, 13 Nov 2025 15:01:02 +0800 Subject: [PATCH] =?UTF-8?q?[+]=20=E5=85=B1=E4=BA=AB=E5=8E=9F=E7=94=9FWebRT?= =?UTF-8?q?C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + taoyao-client-android/README.md | 2 + .../client/src/main/res/values/settings.xml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 +- taoyao-client-android/taoyao/media/README.md | 16 ++ .../com/acgist/taoyao/media/MediaManager.java | 23 ++ .../acgist/taoyao/media/VideoSourceType.java | 8 +- .../taoyao/media/client/SessionClient.java | 2 + .../media/video/ShareVideoCapturer.java | 216 ++++++++++++++++++ 9 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 taoyao-client-android/taoyao/media/README.md create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/ShareVideoCapturer.java diff --git a/.gitignore b/.gitignore index 381236c..948f3a4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ build target node_modules +.idea + .vscode package-lock.json diff --git a/taoyao-client-android/README.md b/taoyao-client-android/README.md index 658c746..b263125 100644 --- a/taoyao-client-android/README.md +++ b/taoyao-client-android/README.md @@ -15,6 +15,8 @@ * [libmediasoupclient文档](https://mediasoup.org/documentation/v3/libmediasoupclient) * [libmediasoupclient接口](https://mediasoup.org/documentation/v3/libmediasoupclient/api) +> 分支:`3.4.2` + ## 项目配置 可以自己编译`WebRTC`依赖或者下载已有依赖,项目导入以后拷贝`libmediasoupclient`源码还有`WebRTC`头文件和库文件到`deps`目录。 diff --git a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml index 2dad546..034470e 100644 --- a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml +++ b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml @@ -48,6 +48,6 @@ "'TAOYAO' yyyy-MM-dd HH:mm:ss" - + BACK diff --git a/taoyao-client-android/taoyao/gradle/wrapper/gradle-wrapper.properties b/taoyao-client-android/taoyao/gradle/wrapper/gradle-wrapper.properties index 3b794c2..bfe84cf 100644 --- a/taoyao-client-android/taoyao/gradle/wrapper/gradle-wrapper.properties +++ b/taoyao-client-android/taoyao/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,5 @@ -#Mon Mar 20 09:51:37 CST 2023 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-7.5-bin.zip -#distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip -#distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.5-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists diff --git a/taoyao-client-android/taoyao/media/README.md b/taoyao-client-android/taoyao/media/README.md new file mode 100644 index 0000000..3109a4d --- /dev/null +++ b/taoyao-client-android/taoyao/media/README.md @@ -0,0 +1,16 @@ +# 目录结构 + +``` +│ +├─deps +│ ├─libmediasoupclient +│ │ libmediasoupclient源码 +│ └─webrtc +│ ├─lib +│ │ └─arm64-v8a +│ │ libwebrtc.a +│ │ WebRTC静态库 +│ └─src +│ ├─api +│ │ WebRTC头文件 +``` 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 4fa775e..54f9432 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 @@ -16,6 +16,7 @@ import com.acgist.taoyao.media.config.MediaProperties; import com.acgist.taoyao.media.config.MediaVideoProperties; import com.acgist.taoyao.media.config.WebrtcProperties; import com.acgist.taoyao.media.signal.ITaoyao; +import com.acgist.taoyao.media.video.ShareVideoCapturer; import com.acgist.taoyao.media.video.VideoProcesser; import com.acgist.taoyao.media.video.WatermarkProcesser; @@ -491,6 +492,8 @@ public final class MediaManager { this.initCameraCapturer(); } else if (this.videoSourceType == VideoSourceType.SCREEN) { this.initScreenCapturerPromise(); + } else if(this.videoSourceType == VideoSourceType.SHARE) { + this.initShareCapturer(); } else { // 其他来源 } @@ -544,6 +547,14 @@ public final class MediaManager { } } + /** + * 加载本地共享 + */ + private void initShareCapturer() { + this.videoCapturer = new ShareVideoCapturer(); + this.initVideoSource(); + } + /** * 加载屏幕采集 * @@ -1050,6 +1061,18 @@ public final class MediaManager { } + public void addShare(MediaStream mediaStream) { + if(this.videoSourceType == VideoSourceType.SHARE) { + ((ShareVideoCapturer) this.videoCapturer).addSource(mediaStream); + } + } + + public void removeShare(MediaStream mediaStream) { + if(this.videoSourceType == VideoSourceType.SHARE) { + ((ShareVideoCapturer) this.videoCapturer).removeSource(mediaStream); + } + } + /** * 加载MediasoupClient */ diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java index 188eecb..0b094c4 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/VideoSourceType.java @@ -22,7 +22,13 @@ public enum VideoSourceType { /** * 屏幕共享:ScreenCapturerAndroid */ - SCREEN; + SCREEN, + /** + * 共享本地:ShareVideoCapturer + * + * 注意:这个模式只是用来测试很多功能没有兼容 + */ + SHARE; /** * @return 是否是摄像头 diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java index 03d30b6..304241b 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/SessionClient.java @@ -492,6 +492,7 @@ public class SessionClient extends Client { SessionClient.this.remoteMediaStream = mediaStream; SessionClient.this.playAudio(); SessionClient.this.playVideo(); + SessionClient.this.mediaManager.addShare(mediaStream); } @Override @@ -499,6 +500,7 @@ public class SessionClient extends Client { Log.i(SessionClient.class.getSimpleName(), "删除远程媒体:" + SessionClient.this.clientId); mediaStream.dispose(); SessionClient.this.remoteMediaStream = null; + SessionClient.this.mediaManager.removeShare(mediaStream); } @Override diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/ShareVideoCapturer.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/ShareVideoCapturer.java new file mode 100644 index 0000000..0047312 --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/ShareVideoCapturer.java @@ -0,0 +1,216 @@ +package com.acgist.taoyao.media.video; + +import android.content.Context; +import android.util.Log; + +import org.webrtc.CapturerObserver; +import org.webrtc.JavaI420Buffer; +import org.webrtc.MediaStream; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoFrame; +import org.webrtc.VideoSink; +import org.webrtc.VideoTrack; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 注意:只是功能验证,没有实现资源释放。 + */ +public class ShareVideoCapturer implements VideoCapturer { + + private byte[] bytes = new byte[1024 * 1024]; + private boolean running = false; + private CapturerObserver capturerObserver; + private final Map frames = new HashMap<>(); + + @Override + public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, CapturerObserver capturerObserver) { + this.capturerObserver = capturerObserver; + } + + @Override + public void startCapture(int width, int height, int framerate) { + this.running = true; + final Thread thread = new Thread(() -> { + final int col = 2; + final int row = 2; + final int stride = width; + final int width_ = width / col; + final int height_ = height / row; + Log.d(ShareVideoCapturer.class.getSimpleName(), "原始宽度:" + width); + Log.d(ShareVideoCapturer.class.getSimpleName(), "原始高度:" + height); + Log.d(ShareVideoCapturer.class.getSimpleName(), "目标宽度:" + width_); + Log.d(ShareVideoCapturer.class.getSimpleName(), "目标高度:" + height_); + final JavaI420Buffer buffer = JavaI420Buffer.wrap( + width, height, + ByteBuffer.allocateDirect(width * height), stride, + ByteBuffer.allocateDirect(width * height / 2), stride, + ByteBuffer.allocateDirect(width * height / 2), stride, + null + ); + this.clearBuffer(buffer); + final List buffers = new ArrayList<>(); + while(ShareVideoCapturer.this.running) { + synchronized (ShareVideoCapturer.this.frames) { + do { + try { + // 25帧 + ShareVideoCapturer.this.frames.wait(1000 / 25); + } catch (Exception e) { + Log.e(ShareVideoCapturer.class.getSimpleName(), "等待异常", e); + } + } while(ShareVideoCapturer.this.frames.isEmpty()); + ShareVideoCapturer.this.frames.forEach((k, v) -> { + final VideoFrame.Buffer c = v.getBuffer(); + final VideoFrame.Buffer o = c.cropAndScale(0, 0, c.getWidth(), c.getHeight(), width_, height_); + final VideoFrame.I420Buffer x = o.toI420(); + // 如果每个都是独立传输 +// buffer.getDataY().put(x.getDataY()); +// buffer.getDataU().put(x.getDataU()); +// buffer.getDataV().put(x.getDataV()); + buffers.add(x); + o.release(); + v.release(); + }); + for (int i = 0; i < buffers.size(); i++) { + final int row_ = i / col; + final int col_ = i % col; + final int dstX = col_ * width_; + final int dstY = row_ * height_; + ShareVideoCapturer.this.copyBuffer(buffers.get(i), buffer, dstX, dstY); + } + buffers.clear(); + ShareVideoCapturer.this.frames.clear(); + final VideoFrame frame = new VideoFrame( + buffer, + 0, + System.nanoTime() + ); + ShareVideoCapturer.this.capturerObserver.onFrameCaptured(frame); + } + } + buffer.release(); + synchronized (ShareVideoCapturer.this.frames) { + ShareVideoCapturer.this.frames.forEach((k, v) -> { + v.release(); + }); + ShareVideoCapturer.this.frames.clear(); + } + }); + thread.setName("SHARE-VIDEO-CAPTURER"); + thread.setDaemon(true); + thread.start(); + } + + private void clearBuffer(VideoFrame.I420Buffer buffer) { + this.clearBuffer(buffer.getDataY()); + this.clearBuffer(buffer.getDataU()); + this.clearBuffer(buffer.getDataV()); + } + + private void clearBuffer(ByteBuffer buffer) { + while(buffer.hasRemaining()) { + buffer.put((byte) 128); + } + } + + private void copyBuffer( + VideoFrame.I420Buffer src, + VideoFrame.I420Buffer dst, + int dstX, + int dstY + ) { + final int width = src.getWidth(); + final int height = src.getHeight(); + // 复制Y平面 + this.copyPlane( + src.getDataY(), src.getStrideY(), + dst.getDataY(), dst.getStrideY(), + width, height, dstX, dstY + ); + // 复制U平面 + final int uvWidth = (width + 1) / 2; + final int uvHeight = (height + 1) / 2; + copyPlane( + src.getDataU(), src.getStrideU(), + dst.getDataU(), dst.getStrideU(), + uvWidth, uvHeight, dstX / 2, dstY / 2 + ); + // 复制V平面 + this.copyPlane( + src.getDataV(), src.getStrideV(), + dst.getDataV(), dst.getStrideV(), + uvWidth, uvHeight, dstX / 2, dstY / 2 + ); + } + + private void copyPlane( + ByteBuffer src, int srcStride, + ByteBuffer dst, int dstStride, + int width, int height, int dstX, int dstY + ) { + for (int y = 0; y < height; y++) { + final int srcPos = y * srcStride; + final int dstPos = (dstY + y) * dstStride + dstX; + src.position(srcPos); + src.get(this.bytes, 0, width); + dst.position(dstPos); + dst.put(this.bytes, 0, width); + } + } + + @Override + public void stopCapture() throws InterruptedException { + this.running = false; + } + + @Override + public void changeCaptureFormat(int width, int height, int framerate) { + } + + @Override + public void dispose() { + this.running = false; + } + + @Override + public boolean isScreencast() { + return false; + } + + public void addSource(MediaStream mediaStream) { + mediaStream.videoTracks.forEach(track -> { + track.addSink(new VideoSink() { + @Override + public void onFrame(VideoFrame frame) { + synchronized (ShareVideoCapturer.this.frames) { + if(ShareVideoCapturer.this.running) { + frame.retain(); + final VideoFrame old = ShareVideoCapturer.this.frames.put(track, frame); + if(old != null) { + old.release(); + } + } + } + } + }); + }); + } + + public void removeSource(MediaStream mediaStream) { + mediaStream.videoTracks.forEach(track -> { + synchronized (ShareVideoCapturer.this.frames) { + final VideoFrame old = ShareVideoCapturer.this.frames.remove(track); + if(old != null) { + old.release(); + } + } + }); + } + +}