fix: 修复自编码模式分辨率切换及画面拉伸问题
- 重启编码器以支持动态分辨率切换 - 同步调整解码端 SurfaceView 尺寸以保持宽高比 - 修复 MediaCodec Image 未及时关闭导致的潜在崩溃
This commit is contained in:
@@ -355,6 +355,9 @@ public class ScreenCaptureService extends Service {
|
|||||||
// 关键:分辨率切换后,如果是自编码模式,同样需要触发扰动确保产生首帧,避免静态画面黑屏。
|
// 关键:分辨率切换后,如果是自编码模式,同样需要触发扰动确保产生首帧,避免静态画面黑屏。
|
||||||
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
|
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
|
||||||
updateNotification("正在切换分辨率...");
|
updateNotification("正在切换分辨率...");
|
||||||
|
// 自编码器(MediaCodec)不支持动态切换分辨率,必须重启编码器实例。
|
||||||
|
stopSelfCodecEncoder();
|
||||||
|
startSelfCodecEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")");
|
Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")");
|
||||||
|
|||||||
@@ -147,8 +147,16 @@ public class SelfCodecEncoder {
|
|||||||
* 当编码器输入缓冲不足时会丢弃该帧(编码器跟不上),保证不堆积、不阻塞采集线程。
|
* 当编码器输入缓冲不足时会丢弃该帧(编码器跟不上),保证不堆积、不阻塞采集线程。
|
||||||
*/
|
*/
|
||||||
public void feedFrame(VideoFrame frame) {
|
public void feedFrame(VideoFrame frame) {
|
||||||
|
synchronized (this) {
|
||||||
MediaCodec mc = encoder;
|
MediaCodec mc = encoder;
|
||||||
if (!running || mc == null) return;
|
if (!running || mc == null) return;
|
||||||
|
|
||||||
|
// 检查分辨率:自编码器 MediaCodec 尺寸是固定的,若采集分辨率已切换则丢弃本帧,
|
||||||
|
// 等待外部(ScreenCaptureService)重启本编码器实例。
|
||||||
|
if (frame.getRotatedWidth() != width || frame.getRotatedHeight() != height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Integer idx;
|
Integer idx;
|
||||||
synchronized (inputLock) {
|
synchronized (inputLock) {
|
||||||
idx = freeInputIndices.poll();
|
idx = freeInputIndices.poll();
|
||||||
@@ -166,6 +174,7 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void encodeFrame(MediaCodec mc, int index, VideoFrame frame) {
|
private void encodeFrame(MediaCodec mc, int index, VideoFrame frame) {
|
||||||
long ptsUs = System.nanoTime() / 1000;
|
long ptsUs = System.nanoTime() / 1000;
|
||||||
@@ -218,14 +227,15 @@ public class SelfCodecEncoder {
|
|||||||
|
|
||||||
/** 把 NV12 按编码器输入 Image 的 stride 写入输入缓冲并入队。 */
|
/** 把 NV12 按编码器输入 Image 的 stride 写入输入缓冲并入队。 */
|
||||||
private void writeNv12(MediaCodec mc, int index, byte[] nv12, int w, int h, long ptsUs) {
|
private void writeNv12(MediaCodec mc, int index, byte[] nv12, int w, int h, long ptsUs) {
|
||||||
Image image = mc.getInputImage(index);
|
// 使用 try-with-resources 确保 Image 及时关闭。
|
||||||
|
// 根据 MediaCodec 文档,Image 必须在 queueInputBuffer 之前关闭,否则可能导致 native 崩溃或阻塞。
|
||||||
|
try (Image image = mc.getInputImage(index)) {
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
mc.queueInputBuffer(index, 0, 0, ptsUs, 0);
|
mc.queueInputBuffer(index, 0, 0, ptsUs, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
Image.Plane[] planes = image.getPlanes();
|
Image.Plane[] planes = image.getPlanes();
|
||||||
// Y 平面写入,添加边界检查防止部分设备 buffer limit 偏移导致的 Crash
|
// Y 平面写入
|
||||||
Image.Plane yPlane = planes[0];
|
Image.Plane yPlane = planes[0];
|
||||||
ByteBuffer yBuf = yPlane.getBuffer();
|
ByteBuffer yBuf = yPlane.getBuffer();
|
||||||
int yRow = yPlane.getRowStride();
|
int yRow = yPlane.getRowStride();
|
||||||
@@ -259,7 +269,6 @@ public class SelfCodecEncoder {
|
|||||||
for (int r = 0; r < ch; r++) {
|
for (int r = 0; r < ch; r++) {
|
||||||
int dstRow = r * row;
|
int dstRow = r * row;
|
||||||
for (int c = 0; c < cw; c++) {
|
for (int c = 0; c < cw; c++) {
|
||||||
// nv12 数组中 UV 是交错存储的
|
|
||||||
int s = uvSrcBase + (r * cw + c) * 2 + (p - 1);
|
int s = uvSrcBase + (r * cw + c) * 2 + (p - 1);
|
||||||
int dst = dstRow + c;
|
int dst = dstRow + c;
|
||||||
if (dst < limit) {
|
if (dst < limit) {
|
||||||
@@ -282,7 +291,6 @@ public class SelfCodecEncoder {
|
|||||||
for (int c = 0; c < cw; c++) {
|
for (int c = 0; c < cw; c++) {
|
||||||
int s = sRow + c * 2;
|
int s = sRow + c * 2;
|
||||||
int d = dRow + c * uvPix;
|
int d = dRow + c * uvPix;
|
||||||
// 针对 Android 14+ 或部分特殊设备,UV buffer 的 limit 可能刚好少一个字节
|
|
||||||
if (d < uvLimit) {
|
if (d < uvLimit) {
|
||||||
uvBuf.put(d, nv12[s]);
|
uvBuf.put(d, nv12[s]);
|
||||||
}
|
}
|
||||||
@@ -292,7 +300,8 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 显式关闭 image 确保在 queueInputBuffer 前释放 native 句柄
|
||||||
|
image.close();
|
||||||
mc.queueInputBuffer(index, 0, w * h * 3 / 2, ptsUs, 0);
|
mc.queueInputBuffer(index, 0, w * h * 3 / 2, ptsUs, 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "writeNv12 failed", e);
|
Log.e(TAG, "writeNv12 failed", e);
|
||||||
@@ -463,6 +472,7 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void release() {
|
public void release() {
|
||||||
|
synchronized (this) {
|
||||||
running = false;
|
running = false;
|
||||||
freeInputIndices.clear();
|
freeInputIndices.clear();
|
||||||
if (encoder != null) {
|
if (encoder != null) {
|
||||||
@@ -478,4 +488,5 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
nv12Buffer = null;
|
nv12Buffer = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.widget.RadioGroup;
|
|||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
|
import android.view.Gravity;
|
||||||
import com.ttstd.controller.webrtc.SelfCodecDecoder;
|
import com.ttstd.controller.webrtc.SelfCodecDecoder;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
@@ -231,6 +232,57 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自编码模式:按比例调整 selfCodecSurface 与触摸层尺寸。
|
||||||
|
* MediaCodec 渲染到 SurfaceView 时会把帧拉伸填满整个 Surface 显示区域,
|
||||||
|
* 因此必须让 Surface 的宽高比与视频一致,否则画面会被拉伸变形。
|
||||||
|
*/
|
||||||
|
private void adjustSelfCodecSize(int videoWidth, int videoHeight) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
if (videoWidth == 0 || videoHeight == 0) return;
|
||||||
|
|
||||||
|
int containerWidth = controlPanel.getWidth();
|
||||||
|
int containerHeight = controlPanel.getHeight();
|
||||||
|
|
||||||
|
if (containerWidth == 0 || containerHeight == 0) {
|
||||||
|
// 容器尚未测量完成,稍后重试
|
||||||
|
controlPanel.postDelayed(() -> adjustSelfCodecSize(videoWidth, videoHeight), 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float videoAspectRatio = (float) videoWidth / videoHeight;
|
||||||
|
float containerAspectRatio = (float) containerWidth / containerHeight;
|
||||||
|
|
||||||
|
int newWidth, newHeight;
|
||||||
|
if (videoAspectRatio > containerAspectRatio) {
|
||||||
|
// 视频更宽,以容器宽度为准
|
||||||
|
newWidth = containerWidth;
|
||||||
|
newHeight = (int) (containerWidth / videoAspectRatio);
|
||||||
|
} else {
|
||||||
|
// 视频更高,以容器高度为准
|
||||||
|
newWidth = (int) (containerHeight * videoAspectRatio);
|
||||||
|
newHeight = containerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整自编码渲染 Surface 的尺寸(等比居中),避免画面被拉伸变形
|
||||||
|
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) selfCodecSurface.getLayoutParams();
|
||||||
|
lp.width = newWidth;
|
||||||
|
lp.height = newHeight;
|
||||||
|
lp.gravity = Gravity.CENTER;
|
||||||
|
selfCodecSurface.setLayoutParams(lp);
|
||||||
|
|
||||||
|
// 同步触摸层,使其与视频区域完全重合,保证触控坐标一致
|
||||||
|
FrameLayout.LayoutParams overlayLp = (FrameLayout.LayoutParams) touchOverlay.getLayoutParams();
|
||||||
|
overlayLp.width = newWidth;
|
||||||
|
overlayLp.height = newHeight;
|
||||||
|
overlayLp.gravity = Gravity.CENTER;
|
||||||
|
touchOverlay.setLayoutParams(overlayLp);
|
||||||
|
|
||||||
|
Log.d(TAG, String.format("Adjusted self-codec view size: %dx%d (video: %dx%d, container: %dx%d)",
|
||||||
|
newWidth, newHeight, videoWidth, videoHeight, containerWidth, containerHeight));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 弹出鉴权输入对话框:选择免密连接、动态验证码或固定密码,输入后发起连接。 */
|
/** 弹出鉴权输入对话框:选择免密连接、动态验证码或固定密码,输入后发起连接。 */
|
||||||
private void showAuthDialog() {
|
private void showAuthDialog() {
|
||||||
String serverUrl = etServerUrl.getText().toString().trim();
|
String serverUrl = etServerUrl.getText().toString().trim();
|
||||||
@@ -653,6 +705,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
tvStreamMode = findViewById(R.id.tv_stream_mode);
|
tvStreamMode = findViewById(R.id.tv_stream_mode);
|
||||||
selfCodecSurface = findViewById(R.id.self_codec_surface);
|
selfCodecSurface = findViewById(R.id.self_codec_surface);
|
||||||
selfCodecDecoder = new SelfCodecDecoder();
|
selfCodecDecoder = new SelfCodecDecoder();
|
||||||
|
selfCodecDecoder.setOnResolutionUpdateListener((w, h) -> adjustSelfCodecSize(w, h));
|
||||||
surfaceCallback = new SurfaceHolder.Callback() {
|
surfaceCallback = new SurfaceHolder.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
@@ -688,6 +741,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
selfCodecSurface.setVisibility(View.VISIBLE);
|
selfCodecSurface.setVisibility(View.VISIBLE);
|
||||||
remoteVideoView.setVisibility(View.GONE);
|
remoteVideoView.setVisibility(View.GONE);
|
||||||
selfCodecDecoder.setEnabled(true);
|
selfCodecDecoder.setEnabled(true);
|
||||||
|
if (selfCodecDecoder.getVideoWidth() > 0) {
|
||||||
|
adjustSelfCodecSize(selfCodecDecoder.getVideoWidth(), selfCodecDecoder.getVideoHeight());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selfCodecSurface.setVisibility(View.GONE);
|
selfCodecSurface.setVisibility(View.GONE);
|
||||||
remoteVideoView.setVisibility(View.VISIBLE);
|
remoteVideoView.setVisibility(View.VISIBLE);
|
||||||
@@ -706,6 +762,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
selfCodecSurface.setVisibility(View.VISIBLE);
|
selfCodecSurface.setVisibility(View.VISIBLE);
|
||||||
remoteVideoView.setVisibility(View.GONE);
|
remoteVideoView.setVisibility(View.GONE);
|
||||||
selfCodecDecoder.setEnabled(true);
|
selfCodecDecoder.setEnabled(true);
|
||||||
|
if (selfCodecDecoder.getVideoWidth() > 0) {
|
||||||
|
adjustSelfCodecSize(selfCodecDecoder.getVideoWidth(), selfCodecDecoder.getVideoHeight());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selfCodecSurface.setVisibility(View.GONE);
|
selfCodecSurface.setVisibility(View.GONE);
|
||||||
remoteVideoView.setVisibility(View.VISIBLE);
|
remoteVideoView.setVisibility(View.VISIBLE);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public class SelfCodecDecoder {
|
|||||||
private int videoWidth = PLACEHOLDER_W;
|
private int videoWidth = PLACEHOLDER_W;
|
||||||
private int videoHeight = PLACEHOLDER_H;
|
private int videoHeight = PLACEHOLDER_H;
|
||||||
|
|
||||||
|
// 分辨率变化回调(通知 UI 调整渲染 Surface 尺寸以保持宽高比,避免画面被拉伸变形)
|
||||||
|
private OnResolutionUpdateListener resolutionListener;
|
||||||
|
|
||||||
// 待喂入的参数集(SPS/PPS 等),以 CODEC_CONFIG 形式输入解码器
|
// 待喂入的参数集(SPS/PPS 等),以 CODEC_CONFIG 形式输入解码器
|
||||||
private final ArrayDeque<byte[]> pendingConfigs = new ArrayDeque<>();
|
private final ArrayDeque<byte[]> pendingConfigs = new ArrayDeque<>();
|
||||||
// 待解码帧
|
// 待解码帧
|
||||||
@@ -86,6 +89,9 @@ public class SelfCodecDecoder {
|
|||||||
// 分辨率变化时需重置解码器以重新 configure
|
// 分辨率变化时需重置解码器以重新 configure
|
||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
}
|
}
|
||||||
|
if (resolutionListener != null) {
|
||||||
|
resolutionListener.onResolutionUpdated(w, h);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,4 +339,21 @@ public class SelfCodecDecoder {
|
|||||||
releaseDecoder();
|
releaseDecoder();
|
||||||
surface = null;
|
surface = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getVideoWidth() {
|
||||||
|
return videoWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVideoHeight() {
|
||||||
|
return videoHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnResolutionUpdateListener(OnResolutionUpdateListener l) {
|
||||||
|
this.resolutionListener = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分辨率变化监听:当被控端上报新的视频宽高时回调,供 UI 调整渲染视图比例。 */
|
||||||
|
public interface OnResolutionUpdateListener {
|
||||||
|
void onResolutionUpdated(int width, int height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user