From 60ba25bdb4ee615ccbe82c9ae573f0d4f5ce5202 Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Wed, 3 May 2023 16:24:38 +0800 Subject: [PATCH] =?UTF-8?q?[+]=20=E6=B0=B4=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- taoyao-client-android/README.md | 5 +- .../acgist/taoyao/client/MainActivity.java | 1 + .../client/src/main/res/values/settings.xml | 2 + .../com/acgist/taoyao/media/MediaManager.java | 57 ++++++-- .../media/audio/AudioChangerProcesser.java | 10 -- .../{AudioMixer.java => MixerProcesser.java} | 4 +- .../taoyao/media/client/PhotographClient.java | 1 + .../taoyao/media/client/RecordClient.java | 6 + .../taoyao/media/client/SessionClient.java | 2 + .../taoyao/media/video/AiProcesser.java | 22 +-- .../taoyao/media/video/BeautyProcesser.java | 29 ---- .../taoyao/media/video/VideoProcesser.java | 33 +++++ .../media/video/WatermarkProcesser.java | 125 ++++++++++++++++-- .../java/com/acgist/taoyao/rtp/RtpTest.java | 2 +- 15 files changed, 219 insertions(+), 86 deletions(-) delete mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioChangerProcesser.java rename taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/{AudioMixer.java => MixerProcesser.java} (78%) delete mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/BeautyProcesser.java create mode 100644 taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/VideoProcesser.java diff --git a/README.md b/README.md index 897ff1d..5ab8611 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,8 @@ |控制|支持|完成|部分控制信令| |拍照|支持|完成|拍照| |录像|支持|完成|录制| -|变声|支持|暂未实现|变声器| -|水印|支持|暂未实现|视频水印| -|美颜|支持|暂未实现|视频美颜| -|AI识别|支持|暂未实现|视频AI识别| +|混音|支持|暂未实现|多路混音| +|水印|支持|完成|视频水印| > 注意:Web终端不支持同时进入多个视频房间,安卓终端支持同时进入多个视频房间。 diff --git a/taoyao-client-android/README.md b/taoyao-client-android/README.md index 6c5d658..559efd8 100644 --- a/taoyao-client-android/README.md +++ b/taoyao-client-android/README.md @@ -18,9 +18,8 @@ ## 视频旋转 -1. 应用旋转(横屏|竖屏) -2. 代码旋转 -3. 镜头物理旋转 +1. 应用旋转:横屏竖屏 +2. 物理旋转:旋转镜头 ## 学习资料 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 c3b434e..14b55e0 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 @@ -130,6 +130,7 @@ public class MainActivity extends AppCompatActivity implements Serializable { resources.getInteger(R.integer.iFrameInterval), resources.getString(R.string.storagePathImage), resources.getString(R.string.storagePathVideo), + resources.getString(R.string.watermark), VideoSourceType.valueOf(resources.getString(R.string.videoSourceType)) ); final Display display = this.getWindow().getContext().getDisplay(); 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 ed60c05..45473aa 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 @@ -44,4 +44,6 @@ 1 1 + + "'TAOYAO' yyyy-MM-dd HH:mm:ss" \ No newline at end of file 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 8ed0ab1..c4c1da4 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 @@ -14,7 +14,10 @@ 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.VideoProcesser; +import com.acgist.taoyao.media.video.WatermarkProcesser; +import org.apache.commons.lang3.StringUtils; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.Camera2Enumerator; @@ -27,7 +30,6 @@ import org.webrtc.EglBase; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; import org.webrtc.PeerConnectionFactory; -import org.webrtc.RendererCommon; import org.webrtc.ScreenCapturerAndroid; import org.webrtc.SurfaceTextureHelper; import org.webrtc.SurfaceViewRenderer; @@ -88,6 +90,10 @@ public final class MediaManager { * 关键帧频率 */ private int iFrameInterval; + /** + * 水印 + */ + private String watermark; /** * 视频来源类型 */ @@ -148,6 +154,10 @@ public final class MediaManager { * PeerConnectionFactory */ private PeerConnectionFactory peerConnectionFactory; + /** + * 视频处理 + */ + private VideoProcesser videoProcesser; static { // // 设置采样 @@ -190,14 +200,14 @@ public final class MediaManager { /** * @param mainHandler Handler - * @param context 上下文 + * @param context 上下文 */ public void initContext( Handler mainHandler, Context context, int imageQuantity, String audioQuantity, String videoQuantity, int channelCount, int iFrameInterval, String imagePath, String videoPath, - VideoSourceType videoSourceType + String watermark, VideoSourceType videoSourceType ) { this.mainHandler = mainHandler; this.context = context; @@ -208,6 +218,7 @@ public final class MediaManager { this.iFrameInterval = iFrameInterval; this.imagePath = imagePath; this.videoPath = videoPath; + this.watermark = watermark; this.videoSourceType = videoSourceType; } @@ -320,6 +331,7 @@ public final class MediaManager { }); this.initAudio(); this.initVideo(); + this.initWatermark(); } private JavaAudioDeviceModule javaAudioDeviceModule() { @@ -419,7 +431,7 @@ public final class MediaManager { // 忽略其他摄像头 } } - this.initVideoTrack(); + this.initVideoSource(); } private void initSharePromise() { @@ -433,13 +445,13 @@ public final class MediaManager { */ public void initScreen(Intent intent) { this.videoCapturer = new ScreenCapturerAndroid(intent, new ScreenCallback()); - this.initVideoTrack(); + this.initVideoSource(); } /** * 加载视频 */ - private void initVideoTrack() { + private void initVideoSource() { // 加载视频 this.surfaceTextureHelper = SurfaceTextureHelper.create("MediaVideoThread", this.eglContext); // this.surfaceTextureHelper.setTextureSize(); @@ -456,12 +468,21 @@ public final class MediaManager { // this.shareVideoSource.setVideoProcessor(); } + private void initWatermark() { + if(StringUtils.isNotEmpty(this.watermark)) { + final MediaVideoProperties mediaVideoProperties = this.mediaProperties.getVideos().get(this.videoQuantity); + if(this.videoProcesser == null) { + this.videoProcesser = new WatermarkProcesser(this.watermark, mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight()); + } else { + this.videoProcesser = new WatermarkProcesser(this.watermark, mediaVideoProperties.getWidth(), mediaVideoProperties.getHeight(), this.videoProcesser); + } + } + } + /** * 更新配置 * - * @param mediaProperties 媒体配置 - * @param mediaAudioProperties 音频配置 - * @param mediaVideoProperties 视频配置 + * @param mediaProperties 媒体配置 */ public void updateMediaConfig(MediaProperties mediaProperties) { this.mediaProperties = mediaProperties; @@ -707,6 +728,10 @@ public final class MediaManager { this.peerConnectionFactory.dispose(); this.peerConnectionFactory = null; } + if(this.videoProcesser != null) { + this.videoProcesser.close(); + this.videoProcesser = null; + } } /** @@ -741,8 +766,18 @@ public final class MediaManager { @Override public void onFrameCaptured(VideoFrame videoFrame) { // 注意:VideoFrame必须释放,多线程环境需要调用retain和release方法。 - this.mainObserver.onFrameCaptured(videoFrame); - this.shareObserver.onFrameCaptured(videoFrame); + if(MediaManager.this.videoProcesser == null) { + this.mainObserver.onFrameCaptured(videoFrame); + this.shareObserver.onFrameCaptured(videoFrame); + } else { + final VideoFrame.I420Buffer i420Buffer = videoFrame.getBuffer().toI420(); + MediaManager.this.videoProcesser.process(i420Buffer); + final VideoFrame processVideoFrame = new VideoFrame(i420Buffer.cropAndScale(0, 0, i420Buffer.getWidth(), i420Buffer.getHeight(), i420Buffer.getWidth(), i420Buffer.getHeight()), videoFrame.getRotation(), videoFrame.getTimestampNs()); + i420Buffer.release(); + this.mainObserver.onFrameCaptured(processVideoFrame); + this.shareObserver.onFrameCaptured(processVideoFrame); + processVideoFrame.release(); + } } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioChangerProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioChangerProcesser.java deleted file mode 100644 index d2e765f..0000000 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioChangerProcesser.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.acgist.taoyao.media.audio; - -/** - * 变声处理器 - * - * @author acgist - */ -public class AudioChangerProcesser { - -} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioMixer.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java similarity index 78% rename from taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioMixer.java rename to taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java index 2b6af8f..b8f73c8 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/AudioMixer.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java @@ -1,13 +1,13 @@ package com.acgist.taoyao.media.audio; /** - * 混音 + * 混音处理器 * * WebRtcAudioTrack#AudioTrackThread :远程音频 * WebRtcAudioRecord#AudioRecordThread:本地音频 * * @author acgist */ -public class AudioMixer { +public class MixerProcesser { } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java index d0ad0d7..adc85f1 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/PhotographClient.java @@ -135,6 +135,7 @@ public class PhotographClient implements VideoSink { // matrix.setRotate(90); // final Bitmap matrixBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false); bitmap.compress(Bitmap.CompressFormat.JPEG, this.quantity, output); + bitmap.recycle(); } catch (Exception e) { Log.e(PhotographClient.class.getSimpleName(), "拍照异常", e); } finally { diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java index 85e33b3..28d0367 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java @@ -32,7 +32,13 @@ import java.time.LocalDateTime; */ public class RecordClient extends Client implements VideoSink, JavaAudioDeviceModule.SamplesReadyCallback { + /** + * 等待时间(毫秒) + */ private static final long WAIT_TIME_MS = 50; + /** + * 等待时间(纳秒) + */ private static final long WAIT_TIME_US = WAIT_TIME_MS * 1000; /** 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 d515390..4878590 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 @@ -377,6 +377,7 @@ public class SessionClient extends Client { public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { Log.d(SessionClient.class.getSimpleName(), "PCIce连接状态改变:" + iceConnectionState); SessionClient.this.logState(); + // disconnected:暂时连接不上可能自我恢复 } @Override @@ -432,6 +433,7 @@ public class SessionClient extends Client { public void onRenegotiationNeeded() { Log.d(SessionClient.class.getSimpleName(), "重新协商媒体:" + SessionClient.this.sessionId); if(SessionClient.this.peerConnection.connectionState() == PeerConnection.PeerConnectionState.CONNECTED) { +// SessionClient.this.peerConnection.restartIce(); // TODO:重新协商 // SessionClient.this.offer(); } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/AiProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/AiProcesser.java index 1dc85e5..9bc4ba1 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/AiProcesser.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/AiProcesser.java @@ -1,30 +1,18 @@ package com.acgist.taoyao.media.video; import org.webrtc.VideoFrame; -import org.webrtc.VideoProcessor; -import org.webrtc.VideoSink; /** - * AI处理器 + * AI识别处理器 + * + * 建议不要每帧识别,如果没有识别出来结果可以复用识别结果。 * * @author acgist */ -public class AiProcesser implements VideoProcessor { +public class AiProcesser extends VideoProcesser { @Override - public void setSink(VideoSink videoSink) { - } - - @Override - public void onCapturerStarted(boolean status) { - } - - @Override - public void onCapturerStopped() { - } - - @Override - public void onFrameCaptured(VideoFrame videoFrame) { + protected void doProcess(VideoFrame.I420Buffer i420Buffer) { } } diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/BeautyProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/BeautyProcesser.java deleted file mode 100644 index 61887e3..0000000 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/BeautyProcesser.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.acgist.taoyao.media.video; - -import org.webrtc.VideoFrame; -import org.webrtc.VideoProcessor; -import org.webrtc.VideoSink; - -/** - * 美颜处理器 - * - * @author acgist - */ -public class BeautyProcesser implements VideoProcessor { - - @Override - public void setSink(VideoSink videoSink) { - } - - @Override - public void onCapturerStarted(boolean status) { - } - - @Override - public void onCapturerStopped() { - } - - @Override - public void onFrameCaptured(VideoFrame videoFrame) { - } -} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/VideoProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/VideoProcesser.java new file mode 100644 index 0000000..d25dfa5 --- /dev/null +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/VideoProcesser.java @@ -0,0 +1,33 @@ +package com.acgist.taoyao.media.video; + +import org.webrtc.VideoFrame; + +import java.io.Closeable; + +/** + * 视频处理器 + */ +public abstract class VideoProcesser implements Closeable { + + protected VideoProcesser next; + + public void process(VideoFrame.I420Buffer i420Buffer) { + this.doProcess(i420Buffer); + if(this.next == null) { + // 忽略 + } else { + this.next.process(i420Buffer); + } + } + + protected abstract void doProcess(VideoFrame.I420Buffer i420Buffer); + + public void close() { + if(this.next == null) { + // 忽略 + } else { + this.next.close(); + } + } + +} diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/WatermarkProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/WatermarkProcesser.java index 25fd9c6..2ffd9ba 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/WatermarkProcesser.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/video/WatermarkProcesser.java @@ -1,30 +1,137 @@ package com.acgist.taoyao.media.video; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; + import org.webrtc.VideoFrame; -import org.webrtc.VideoProcessor; -import org.webrtc.VideoSink; + +import java.nio.ByteBuffer; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Timer; +import java.util.TimerTask; /** * 水印处理器 * + * 性能优化: + * 没有水印:20~25波动 + * 没有定时:26~32波动 + * 定时水印:28~32 + * * @author acgist */ -public class WatermarkProcesser implements VideoProcessor { +public class WatermarkProcesser extends VideoProcesser { - @Override - public void setSink(VideoSink videoSink) { + private static final WatermarkMatrix[] MATRICES = new WatermarkMatrix[256]; + + private final String format; + private final int width; + private final int height; + private final Timer timer; + private final WatermarkMatrix[] watermark; + + public WatermarkProcesser(String format, int width, int height) { + this.format = format; + this.width = width; + this.height = height; + final String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern(format)); + this.watermark = new WatermarkMatrix[date.length()]; + this.timer = new Timer("Watermark-Timer", true); + this.init(); + } + + public WatermarkProcesser(String format, int width, int height, VideoProcesser videoProcesser) { + this(format, width, height); + this.next = videoProcesser; + } + + private void init() { + final String source = "-: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final char[] chars = source.toCharArray(); + for (char value : chars) { + this.build(value); + } + this.timer.schedule(new TimerTask() { + @Override + public void run() { + int index = 0; + final char[] chars = LocalDateTime.now().format(DateTimeFormatter.ofPattern(WatermarkProcesser.this.format)).toCharArray(); + for (char value : chars) { + WatermarkProcesser.this.watermark[index] = MATRICES[value]; + index++; + } + } + }, 1000, 1000); + } + + private void build(char source) { + // TODO:优化复用bitmap + final String target = Character.toString(source); + final Paint paint = new Paint(); + paint.setColor(Color.WHITE); + paint.setDither(true); + paint.setTextSize(40.0F); + paint.setTextAlign(Paint.Align.LEFT); + paint.setFilterBitmap(true); + final Paint.FontMetricsInt box = paint.getFontMetricsInt(); + final int width = (int) paint.measureText(target); + final int height = box.descent - box.ascent; + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + canvas.drawText(target, 0, box.leading - box.ascent, paint); + canvas.save(); + final boolean[][] matrix = new boolean[width][height]; + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + matrix[i][j] = bitmap.getColor(i, j).toArgb() != 0; + } + } + MATRICES[source] = new WatermarkMatrix(width, height, matrix); + bitmap.recycle(); } @Override - public void onCapturerStarted(boolean status) { + protected void doProcess(VideoFrame.I420Buffer i420Buffer) { + int widthPos = 0; + int heightPos = 0; + final ByteBuffer buffer = i420Buffer.getDataY(); + for (WatermarkMatrix matrix : watermark) { + if(matrix == null) { + continue; + } + for (int height = 0; height < matrix.height; height++) { + for (int width = 0; width < matrix.width; width++) { + if(matrix.matrix[width][height]) { + buffer.put(this.width * height + width + widthPos, (byte) 0); + } + } + } + widthPos += matrix.width; + heightPos += matrix.height; + } } @Override - public void onCapturerStopped() { + public void close() { + super.close(); + this.timer.cancel(); } - @Override - public void onFrameCaptured(VideoFrame videoFrame) { + static class WatermarkMatrix { + + int width; + int height; + boolean[][] matrix; + + public WatermarkMatrix(int width, int height, boolean[][] matrix) { + this.width = width; + this.height = height; + this.matrix = matrix; + } + } } diff --git a/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java index 87edd73..f5c3e2e 100644 --- a/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java +++ b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java @@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class RtpTest { - + @Test void testSocket() throws Exception { final Socket socket = new Socket();