Android - WebRTC:视频通话实时绘图
Android - WebRTC: Video call live drawing
我正在尝试在 Android(移动)应用程序上使用 WebRTC 制作一个简单的功能。
该应用程序现在可以进行简单的视频通话:将两个设备相互连接并让它们听到和看到。
我想要实现的是在通话期间进行一些现场绘图。简而言之:用户 1 呼叫用户 2,呼叫接通,然后用户 1 单击将冻结视频帧的绘制按钮并允许他在此冻结的帧上绘制。显然,用户 2 应该在他的 phone 上实时看到这幅画。
现在我可以冻结框架(通过调用 videoCapture.stopCapture())并使用自定义 SurfaceViewRenderer 在其上绘制。问题是 User2 看不到绘图,只能看到冻结的框架。
首先,我尝试创建一个包含绘图 canvas 和用于绘制的冻结帧的新视频轨道,但我没有成功。
使用 peerConnectionFactory.createVideoTrack("ARDAMSv1_" + rand, videoSource);
创建视频轨道时
我应该指定轨道的视频源,但源只能是 VideoSource
并且此 VideoSource 只能使用直接链接到设备相机的 VideoCapturer
创建(没有任何绘图当然是)。这就解释了为什么用户 2 在他的设备上看不到任何绘图。
我的问题是:如何创建一个 VideoCapturer,它可以流式传输相机流(冻结帧)和带有绘图的 canvas?
所以我尝试将自己的 VideoCapturer 实现为:
1) 捕获视图(例如包含绘图和冻结帧的布局)并将其流式传输到 VideoSource
或者 2) 捕获相机视图,但在流式传输之前将绘图添加到帧中。
我无法完成这项工作,因为我不知道如何操作 I420Frame 对象以在其上绘制并 return 使用正确的回调对其进行绘制。
也许我对这种方法完全错误,需要做一些完全不同的事情,我愿意接受任何建议。
PS:我正在使用 Android API 25 和 WebRTC 1.0.19742。我不想使用任何付费第三方 SDK/lib.
有谁知道如何继续实现从一个 android 应用程序到另一个 android 应用程序的简单 WebRTC 实时绘图?
我正在开发一个类似的应用程序,所以我分享 在 webrtc 流上绘制部分:
- 您需要做的是获取stream to canvas。
- 然后在canvas上有绘图应用程序编辑,我看一个William Malone项目。 (如果要导入图片,再透明!)
- 最后(我猜你错过了什么)是 stream from canvas 就像你对任何 WebRTC 一样。
我特意为大家准备的一个小demohere(本地webrtc,查看日志)
PS:我使用 getDisplayMedia,而不是 userMedia,因为我的网络摄像头是 kaput...
几周前我们又回到了那个功能,我设法找到了方法。
我扩展了我自己的 CameraCapturer class 以在渲染前控制相机帧。然后我创建了自己的 CanvasView 以便能够在上面绘图。
从那里我所做的是将两个位图合并在一起(相机视图 + 我的 canvas 与绘图),然后我在缓冲区上使用 OpenGL 绘制它并将其显示在 SurfaceView 上。
如果有人感兴趣,我可能会 post 一些代码。
@Override
public void startCapture(int width, int height, int fps) {
Log.d("InitialsClass", "startCapture");
surTexture.stopListening();
cameraHeight = 480;
cameraWidth = 640;
int horizontalSpacing = 16;
int verticalSpacing = 20;
int x = horizontalSpacing;
int y = cameraHeight - verticalSpacing;
cameraBitmap = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888);
YuvFrame frame = new YuvFrame(null, PROCESSING_NONE, appContext);
surTexture.startListening(new VideoSink() {
@Override
public void onFrame(VideoFrame videoFrame) {
frame.fromVideoFrame(videoFrame, PROCESSING_NONE);
}
});
if (captureThread == null || !captureThread.isInterrupted()) {
captureThread = new Thread(() -> {
try {
if (matrix == null) {
matrix = new Matrix();
}
long start = System.nanoTime();
capturerObs.onCapturerStarted(true);
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
YuvConverter yuvConverter = new YuvConverter();
WindowManager windowManager = (WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
// Set filtering
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
//The bitmap is drawn on the GPU at this point.
TextureBufferImpl buffer = new TextureBufferImpl(cameraWidth, cameraHeight - 3, VideoFrame.TextureBuffer.Type.RGB, textures[0], matrix, surTexture.getHandler(), yuvConverter, null);
Resources resources = appContext.getResources();
float scale = resources.getDisplayMetrics().density;
Log.d("InitialsClass before", "camera start capturer width- " + cameraWidth + " height- " + cameraHeight);
while (!Thread.currentThread().isInterrupted()) {
ByteBuffer gBuffer = frame.getByteBuffer();
if (gBuffer != null) {
Log.d("InitialsClass ", "gBuffer not null");
cameraBitmap.copyPixelsFromBuffer(gBuffer);
}
if (cameraBitmap != null) {
if (canvas == null) {
canvas = new Canvas();
}
if (appContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT)
rotationDegree = -90;
else {
assert windowManager != null;
if (windowManager.getDefaultDisplay().getRotation() == Surface.ROTATION_0) {
// clockwise
rotationDegree = 0;
} else if (windowManager.getDefaultDisplay().getRotation() == Surface.ROTATION_90) {
// anti-clockwise
rotationDegree = -180;
}
}
canvas.save(); //save the position of the canvas
canvas.rotate(rotationDegree, (cameraBitmap.getWidth() / 2), (cameraBitmap.getHeight() / 2)); //rotate the canvas
canvas.drawBitmap(cameraBitmap, 0, 0, null); //draw the image on the rotated canvas
canvas.restore(); // restore the canvas position.
matrix.setScale(-1, 1);
matrix.postTranslate(/*weakBitmap.get().getWidth()*/ cameraBitmap.getWidth(), 0);
matrix.setScale(1, -1);
matrix.postTranslate(0, /*weakBitmap.get().getHeight()*/ cameraBitmap.getHeight());
canvas.setMatrix(matrix);
if (textPaint == null) {
textPaint = new TextPaint();
}
textPaint.setColor(Color.WHITE);
textPaint.setTypeface(Typeface.create(typeFace, Typeface.BOLD));
textPaint.setTextSize((int) (11 * scale));
if (textBounds == null) {
textBounds = new Rect();
}
textPaint.getTextBounds(userName, 0, userName.length(), textBounds);
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setAntiAlias(true);
canvas.drawText(userName, x, y, textPaint);
if (paint == null) {
paint = new Paint();
}
if (isLocalCandidate) {
paint.setColor(Color.GREEN);
} else {
paint.setColor(Color.TRANSPARENT);
}
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(0, 8, cameraWidth - 8, cameraHeight - 8, paint);
if (surTexture != null && surTexture.getHandler() != null && surTexture.getHandler().getLooper().getThread().isAlive()) {
surTexture.getHandler().post(() -> {
// Load the bitmap into the bound texture.
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, /*weakBitmap.get()*/ cameraBitmap, 0);
//We transfer it to the VideoFrame
VideoFrame.I420Buffer i420Buf = yuvConverter.convert(buffer);
long frameTime = System.nanoTime() - start;
VideoFrame videoFrame = new VideoFrame(i420Buf, 0, frameTime);
capturerObs.onFrameCaptured(videoFrame);
});
}
}
Thread.sleep(100);
}
} catch (InterruptedException ex) {
Log.d("InitialsClass camera", ex.toString());
Thread.currentThread().interrupt();
return;
}
});
}
captureThread.start();
}
@Anael。看看吧。
我正在尝试在 Android(移动)应用程序上使用 WebRTC 制作一个简单的功能。 该应用程序现在可以进行简单的视频通话:将两个设备相互连接并让它们听到和看到。 我想要实现的是在通话期间进行一些现场绘图。简而言之:用户 1 呼叫用户 2,呼叫接通,然后用户 1 单击将冻结视频帧的绘制按钮并允许他在此冻结的帧上绘制。显然,用户 2 应该在他的 phone 上实时看到这幅画。 现在我可以冻结框架(通过调用 videoCapture.stopCapture())并使用自定义 SurfaceViewRenderer 在其上绘制。问题是 User2 看不到绘图,只能看到冻结的框架。
首先,我尝试创建一个包含绘图 canvas 和用于绘制的冻结帧的新视频轨道,但我没有成功。
使用 peerConnectionFactory.createVideoTrack("ARDAMSv1_" + rand, videoSource);
创建视频轨道时
我应该指定轨道的视频源,但源只能是 VideoSource
并且此 VideoSource 只能使用直接链接到设备相机的 VideoCapturer
创建(没有任何绘图当然是)。这就解释了为什么用户 2 在他的设备上看不到任何绘图。
我的问题是:如何创建一个 VideoCapturer,它可以流式传输相机流(冻结帧)和带有绘图的 canvas?
所以我尝试将自己的 VideoCapturer 实现为:
1) 捕获视图(例如包含绘图和冻结帧的布局)并将其流式传输到 VideoSource
或者 2) 捕获相机视图,但在流式传输之前将绘图添加到帧中。
我无法完成这项工作,因为我不知道如何操作 I420Frame 对象以在其上绘制并 return 使用正确的回调对其进行绘制。
也许我对这种方法完全错误,需要做一些完全不同的事情,我愿意接受任何建议。 PS:我正在使用 Android API 25 和 WebRTC 1.0.19742。我不想使用任何付费第三方 SDK/lib.
有谁知道如何继续实现从一个 android 应用程序到另一个 android 应用程序的简单 WebRTC 实时绘图?
我正在开发一个类似的应用程序,所以我分享 在 webrtc 流上绘制部分:
- 您需要做的是获取stream to canvas。
- 然后在canvas上有绘图应用程序编辑,我看一个William Malone项目。 (如果要导入图片,再透明!)
- 最后(我猜你错过了什么)是 stream from canvas 就像你对任何 WebRTC 一样。
我特意为大家准备的一个小demohere(本地webrtc,查看日志)
PS:我使用 getDisplayMedia,而不是 userMedia,因为我的网络摄像头是 kaput...
几周前我们又回到了那个功能,我设法找到了方法。
我扩展了我自己的 CameraCapturer class 以在渲染前控制相机帧。然后我创建了自己的 CanvasView 以便能够在上面绘图。
从那里我所做的是将两个位图合并在一起(相机视图 + 我的 canvas 与绘图),然后我在缓冲区上使用 OpenGL 绘制它并将其显示在 SurfaceView 上。
如果有人感兴趣,我可能会 post 一些代码。
@Override
public void startCapture(int width, int height, int fps) {
Log.d("InitialsClass", "startCapture");
surTexture.stopListening();
cameraHeight = 480;
cameraWidth = 640;
int horizontalSpacing = 16;
int verticalSpacing = 20;
int x = horizontalSpacing;
int y = cameraHeight - verticalSpacing;
cameraBitmap = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888);
YuvFrame frame = new YuvFrame(null, PROCESSING_NONE, appContext);
surTexture.startListening(new VideoSink() {
@Override
public void onFrame(VideoFrame videoFrame) {
frame.fromVideoFrame(videoFrame, PROCESSING_NONE);
}
});
if (captureThread == null || !captureThread.isInterrupted()) {
captureThread = new Thread(() -> {
try {
if (matrix == null) {
matrix = new Matrix();
}
long start = System.nanoTime();
capturerObs.onCapturerStarted(true);
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
YuvConverter yuvConverter = new YuvConverter();
WindowManager windowManager = (WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
// Set filtering
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
//The bitmap is drawn on the GPU at this point.
TextureBufferImpl buffer = new TextureBufferImpl(cameraWidth, cameraHeight - 3, VideoFrame.TextureBuffer.Type.RGB, textures[0], matrix, surTexture.getHandler(), yuvConverter, null);
Resources resources = appContext.getResources();
float scale = resources.getDisplayMetrics().density;
Log.d("InitialsClass before", "camera start capturer width- " + cameraWidth + " height- " + cameraHeight);
while (!Thread.currentThread().isInterrupted()) {
ByteBuffer gBuffer = frame.getByteBuffer();
if (gBuffer != null) {
Log.d("InitialsClass ", "gBuffer not null");
cameraBitmap.copyPixelsFromBuffer(gBuffer);
}
if (cameraBitmap != null) {
if (canvas == null) {
canvas = new Canvas();
}
if (appContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT)
rotationDegree = -90;
else {
assert windowManager != null;
if (windowManager.getDefaultDisplay().getRotation() == Surface.ROTATION_0) {
// clockwise
rotationDegree = 0;
} else if (windowManager.getDefaultDisplay().getRotation() == Surface.ROTATION_90) {
// anti-clockwise
rotationDegree = -180;
}
}
canvas.save(); //save the position of the canvas
canvas.rotate(rotationDegree, (cameraBitmap.getWidth() / 2), (cameraBitmap.getHeight() / 2)); //rotate the canvas
canvas.drawBitmap(cameraBitmap, 0, 0, null); //draw the image on the rotated canvas
canvas.restore(); // restore the canvas position.
matrix.setScale(-1, 1);
matrix.postTranslate(/*weakBitmap.get().getWidth()*/ cameraBitmap.getWidth(), 0);
matrix.setScale(1, -1);
matrix.postTranslate(0, /*weakBitmap.get().getHeight()*/ cameraBitmap.getHeight());
canvas.setMatrix(matrix);
if (textPaint == null) {
textPaint = new TextPaint();
}
textPaint.setColor(Color.WHITE);
textPaint.setTypeface(Typeface.create(typeFace, Typeface.BOLD));
textPaint.setTextSize((int) (11 * scale));
if (textBounds == null) {
textBounds = new Rect();
}
textPaint.getTextBounds(userName, 0, userName.length(), textBounds);
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.setAntiAlias(true);
canvas.drawText(userName, x, y, textPaint);
if (paint == null) {
paint = new Paint();
}
if (isLocalCandidate) {
paint.setColor(Color.GREEN);
} else {
paint.setColor(Color.TRANSPARENT);
}
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(0, 8, cameraWidth - 8, cameraHeight - 8, paint);
if (surTexture != null && surTexture.getHandler() != null && surTexture.getHandler().getLooper().getThread().isAlive()) {
surTexture.getHandler().post(() -> {
// Load the bitmap into the bound texture.
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, /*weakBitmap.get()*/ cameraBitmap, 0);
//We transfer it to the VideoFrame
VideoFrame.I420Buffer i420Buf = yuvConverter.convert(buffer);
long frameTime = System.nanoTime() - start;
VideoFrame videoFrame = new VideoFrame(i420Buf, 0, frameTime);
capturerObs.onFrameCaptured(videoFrame);
});
}
}
Thread.sleep(100);
}
} catch (InterruptedException ex) {
Log.d("InitialsClass camera", ex.toString());
Thread.currentThread().interrupt();
return;
}
});
}
captureThread.start();
}
@Anael。看看吧。