在基于 WebRTC Mesh 模型 Android 设备 与 Android 设备间 P2P 投屏通信遇到过下列问题
- 首开慢,B端黑了好一会才展示出视频帧,这个时长可能展示为数百毫秒甚至更高
- 建立通信之后黑屏,如果设备桌面是静止状态需要手动去在采集端设备进行一些屏幕变化的操作,渲染端才展示画面
- 渲染端的渲染尺寸如果发生变化(surface),例如全屏且小窗(悬浮窗)模式,画面未及时刷新
上述问题应该如何去排查,我们应该先对 A-B 之间是如何进行通信传输的得有个概念
这里我们默认为设备已经在上层信令完成了offer/answer/sdp/ice等通信(下图),因为这不是排查上述问题的几个重点
我们直接来以 Androider 的视角捋一捋通信建立后的传输过程:
上图是个人对于WebRTC P2P 投屏传输粗浅的理解绘制,如有不正确得地方还请斧正,通过上图的流程可以得出,可能出现黑屏的环节有 3 处:
- 采集端:采集端有没有正常采集到画面,采集端的生产者频率是否在正常生产,是否一直在采集(loop)。
- 渲染端:有没有可能视频帧已经传输过来了,但是 surface 还未 created?,消费者的 loop onFrame 是否在正常消费。
- 传输管道:传输管道或者 native 层出现问题。
上述三个环节中,出现问题几率最小的是传输管道环节,是已经被 WebRTC 封装好了,一般只有在采集和渲染的应用两侧容易出现问题,我们主要分析这两个环节。
Capture
通过上面的流程图总结下采集端传输流程:
- 首先,捕获到的屏幕内容经过编码器编码成视频帧。
- 编码后的视频帧被封装成 VideoFrame 对象。
- VideoFrame 对象被添加到 VideoSource 中。
- VideoSource 会将视频帧发送到与之关联的 VideoTrack 中。
- VideoTrack 将视频帧传输到远程对等端。
java
public interface CapturerObserver {
/** Notify if the capturer have been started successfully or not. */
void onCapturerStarted(boolean success);
/** Notify that the capturer has been stopped. */
void onCapturerStopped();
/** Delivers a captured frame. */
void onFrameCaptured(VideoFrame frame);
}
需要看观察核心类 ScreenCapturerAndroid 是否正常工作
- startCapture
java
@Override
// TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression.
@SuppressWarnings("NoSynchronizedMethodCheck")
public synchronized void startCapture(
final int width, final int height, final int ignoredFramerate) {
checkNotDisposed();
this.width = width;
this.height = height;
mediaProjection = mediaProjectionManager.getMediaProjection(
Activity.RESULT_OK, mediaProjectionPermissionResultData);
// Let MediaProjection callback use the SurfaceTextureHelper thread.
mediaProjection.registerCallback(mediaProjectionCallback, surfaceTextureHelper.getHandler());
createVirtualDisplay();
capturerObserver.onCapturerStarted(true);
surfaceTextureHelper.startListening(ScreenCapturerAndroid.this);
}
- onFrame
typescript
// This is called on the internal looper thread of {@Code SurfaceTextureHelper}.
@Override
public void onFrame(VideoFrame frame) {
numCapturedFrames++;
capturerObserver.onFrameCaptured(frame);
}
进行 debug 调试或者增加日志在 startCapture 和 onFrame 方法中看看打印是否正常,以及 onFrame loop 的频率
后续路径简化
- VideoSource -> onFrameCaptured()
- NativeAndroidVideoTrackSource -> nativeOnFrameCaptured()
至此后续的传输就到 native 层,我们可在每个方法中加上关键日志,观察日志是否正常执行,以及 loop 的执行频率,如果在某个时机发现没有传输或者频率太低我们可以在应用层进行帧补偿。
帧补偿
在 WebRTC Android SurfaceTextureHelper 源码当中提供一个这样的方法:
java
/**
* Forces a frame to be produced. If no new frame is available, the last frame is sent to the
* listener again.
*/
public void forceFrame() {
handler.post(() -> {
hasPendingTexture = true;
tryDeliverTextureFrame();
});
}
强制生产一帧,如果没有可用帧,则用最近的一帧来发送给监听者。如果在某些场景产生了没有画面定位是采集频率问题可以尝试使用这个方法进行帧补偿。
Render
渲染端的排查思路,先需要确定 SurfaceViewRenderer 的 surface 是否创建成功,并且
java
private void createEglSurfaceInternal(Object surface) {
eglSurfaceCreationRunnable.setSurface(surface);
postToRenderThread(eglSurfaceCreationRunnable);
}
surface 成功设置给到 EglRender。
其次 debug 或者日志观察 SurfaceViewRenderer 的 onFrame 方法是否成功执行
java
// Update frame dimensions and report any changes to `rendererEvents`.
private void updateFrameDimensionsAndReportEvents(VideoFrame frame) {
synchronized (layoutLock) {
if (isRenderingPaused) {
return;
}
if (!isFirstFrameRendered) {
isFirstFrameRendered = true;
logD("Reporting first rendered frame.");
if (rendererEvents != null) {
rendererEvents.onFirstFrameRendered();
}
}
if (rotatedFrameWidth != frame.getRotatedWidth()
|| rotatedFrameHeight != frame.getRotatedHeight()
|| frameRotation != frame.getRotation()) {
logD("Reporting frame resolution changed to " + frame.getBuffer().getWidth() + "x"
+ frame.getBuffer().getHeight() + " with rotation " + frame.getRotation());
if (rendererEvents != null) {
rendererEvents.onFrameResolutionChanged(
frame.getBuffer().getWidth(), frame.getBuffer().getHeight(), frame.getRotation());
}
rotatedFrameWidth = frame.getRotatedWidth();
rotatedFrameHeight = frame.getRotatedHeight();
frameRotation = frame.getRotation();
}
}
}
上代码块为渲染刷新的核心方法,WebRTC 源码当中还提供了一些便于排查问题的方法:
java
/**
* Register a callback to be invoked when a new video frame has been received.
*
* @param listener The callback to be invoked. The callback will be invoked on the render thread.
* It should be lightweight and must not call removeFrameListener.
* @param scale The scale of the Bitmap passed to the callback, or 0 if no Bitmap is
* required.
* @param drawer Custom drawer to use for this frame listener.
*/
public void addFrameListener(
EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam) {
eglRenderer.addFrameListener(listener, scale, drawerParam);
}
java
/**
* Callback fired once first frame is rendered.
*/
public void onFirstFrameRendered();
通过上文的关键代码节点,我们增加日志或者去 debug 应该能够看出来渲染端是否正常。
思考一个场景,如果完成了信令握手,A 到 B 已经开始传输了采集画面,这个时候 B 拉起 surface 的容器,这个过程当中 surface 的 created 还未完成,画面帧已经从 A 传输到 B 了。这个时候是不是会黑屏直到 surface 创建成功后的下一帧传输过来?
(延申)如何判断多帧画面一样不进行传输通信
- 视频编解码器特性:视频编解码器(例如VP8、VP9、H.264等)通常会对视频帧进行压缩,以减少数据量并提高传输效率。在这个过程中,编码器会尽量保留画面的主要特征,同时舍弃一些细节。因此,对于静止的画面或者相似的连续画面,编码器生成的压缩后的数据会非常接近,导致两帧之间的差异很小。
- 帧之间的差异性检测:WebRTC会在本地端对视频帧进行分析和比较,以检测连续帧之间的差异。如果两帧之间的差异很小(通常通过比较像素值或者特征值来判断),则可以认为这两帧是相似的,无需进行通信传输。这种本地端的帧差异性检测可以在不需要额外通信开销的情况下进行。
一般来说,开发者可以在接收到新的视频帧后,与上一帧进行比较,判断它们之间的差异。如果差异较小,则可以选择不传输新的帧,而是在接收端使用之前的帧进行渲染。这样可以减少网络传输的数据量,提高传输效率。
Other
文中代码均来自互联网