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 流上绘制部分:

  1. 您需要做的是获取stream to canvas
  2. 然后在canvas上有绘图应用程序编辑,我看一个William Malone项目。 (如果要导入图片,再透明!)
  3. 最后(我猜你错过了什么)是 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。看看吧。