[+] 水印

This commit is contained in:
acgist
2023-05-03 16:24:38 +08:00
parent a52c62a46a
commit 60ba25bdb4
15 changed files with 219 additions and 86 deletions

View File

@@ -18,9 +18,8 @@
## 视频旋转
1. 应用旋转横屏|竖屏
2. 代码旋转
3. 镜头物理旋转
1. 应用旋转横屏竖屏
2. 物理旋转:旋转镜头
## 学习资料

View File

@@ -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();

View File

@@ -44,4 +44,6 @@
<integer name="channelCount">1</integer>
<!-- 视频关键帧频率 -->
<integer name="iFrameInterval">1</integer>
<!-- 水印 -->
<string name="watermark">"'TAOYAO' yyyy-MM-dd HH:mm:ss"</string>
</resources>

View File

@@ -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();
}
}
}

View File

@@ -1,10 +0,0 @@
package com.acgist.taoyao.media.audio;
/**
* 变声处理器
*
* @author acgist
*/
public class AudioChangerProcesser {
}

View File

@@ -1,13 +1,13 @@
package com.acgist.taoyao.media.audio;
/**
* 混音
* 混音处理器
*
* WebRtcAudioTrack#AudioTrackThread 远程音频
* WebRtcAudioRecord#AudioRecordThread本地音频
*
* @author acgist
*/
public class AudioMixer {
public class MixerProcesser {
}

View File

@@ -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 {

View File

@@ -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;
/**

View File

@@ -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();
}

View File

@@ -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) {
}
}

View File

@@ -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) {
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}