diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java index 3a8b610..f5eed56 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java @@ -355,6 +355,9 @@ public class ScreenCaptureService extends Service { // 关键:分辨率切换后,如果是自编码模式,同样需要触发扰动确保产生首帧,避免静态画面黑屏。 if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) { updateNotification("正在切换分辨率..."); + // 自编码器(MediaCodec)不支持动态切换分辨率,必须重启编码器实例。 + stopSelfCodecEncoder(); + startSelfCodecEncoder(); } Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")"); diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/SelfCodecEncoder.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/SelfCodecEncoder.java index 2634dc3..9fabf20 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/SelfCodecEncoder.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/SelfCodecEncoder.java @@ -147,22 +147,31 @@ public class SelfCodecEncoder { * 当编码器输入缓冲不足时会丢弃该帧(编码器跟不上),保证不堆积、不阻塞采集线程。 */ public void feedFrame(VideoFrame frame) { - MediaCodec mc = encoder; - if (!running || mc == null) return; - Integer idx; - synchronized (inputLock) { - idx = freeInputIndices.poll(); - } - if (idx == null) { - return; // 丢掉这一帧,等待下一个空闲输入缓冲 - } - try { - encodeFrame(mc, idx, frame); - } catch (Exception e) { - Log.e(TAG, "encodeFrame failed", e); + synchronized (this) { + MediaCodec mc = encoder; + if (!running || mc == null) return; + + // 检查分辨率:自编码器 MediaCodec 尺寸是固定的,若采集分辨率已切换则丢弃本帧, + // 等待外部(ScreenCaptureService)重启本编码器实例。 + if (frame.getRotatedWidth() != width || frame.getRotatedHeight() != height) { + return; + } + + Integer idx; + synchronized (inputLock) { + idx = freeInputIndices.poll(); + } + if (idx == null) { + return; // 丢掉这一帧,等待下一个空闲输入缓冲 + } try { - mc.queueInputBuffer(idx, 0, 0, System.nanoTime() / 1000, 0); - } catch (Exception ignored) { + encodeFrame(mc, idx, frame); + } catch (Exception e) { + Log.e(TAG, "encodeFrame failed", e); + try { + mc.queueInputBuffer(idx, 0, 0, System.nanoTime() / 1000, 0); + } catch (Exception ignored) { + } } } } @@ -218,14 +227,15 @@ public class SelfCodecEncoder { /** 把 NV12 按编码器输入 Image 的 stride 写入输入缓冲并入队。 */ private void writeNv12(MediaCodec mc, int index, byte[] nv12, int w, int h, long ptsUs) { - Image image = mc.getInputImage(index); - if (image == null) { - mc.queueInputBuffer(index, 0, 0, ptsUs, 0); - return; - } - try { + // 使用 try-with-resources 确保 Image 及时关闭。 + // 根据 MediaCodec 文档,Image 必须在 queueInputBuffer 之前关闭,否则可能导致 native 崩溃或阻塞。 + try (Image image = mc.getInputImage(index)) { + if (image == null) { + mc.queueInputBuffer(index, 0, 0, ptsUs, 0); + return; + } Image.Plane[] planes = image.getPlanes(); - // Y 平面写入,添加边界检查防止部分设备 buffer limit 偏移导致的 Crash + // Y 平面写入 Image.Plane yPlane = planes[0]; ByteBuffer yBuf = yPlane.getBuffer(); int yRow = yPlane.getRowStride(); @@ -259,7 +269,6 @@ public class SelfCodecEncoder { for (int r = 0; r < ch; r++) { int dstRow = r * row; for (int c = 0; c < cw; c++) { - // nv12 数组中 UV 是交错存储的 int s = uvSrcBase + (r * cw + c) * 2 + (p - 1); int dst = dstRow + c; if (dst < limit) { @@ -282,7 +291,6 @@ public class SelfCodecEncoder { for (int c = 0; c < cw; c++) { int s = sRow + c * 2; int d = dRow + c * uvPix; - // 针对 Android 14+ 或部分特殊设备,UV buffer 的 limit 可能刚好少一个字节 if (d < uvLimit) { 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); } catch (Exception e) { Log.e(TAG, "writeNv12 failed", e); @@ -463,19 +472,21 @@ public class SelfCodecEncoder { } public void release() { - running = false; - freeInputIndices.clear(); - if (encoder != null) { - try { - encoder.stop(); - } catch (Exception ignored) { + synchronized (this) { + running = false; + freeInputIndices.clear(); + if (encoder != null) { + try { + encoder.stop(); + } catch (Exception ignored) { + } + try { + encoder.release(); + } catch (Exception ignored) { + } + encoder = null; } - try { - encoder.release(); - } catch (Exception ignored) { - } - encoder = null; + nv12Buffer = null; } - nv12Buffer = null; } } diff --git a/WebRTCController/app/src/main/java/com/ttstd/controller/MainActivity.java b/WebRTCController/app/src/main/java/com/ttstd/controller/MainActivity.java index d41b14d..9cb72ab 100644 --- a/WebRTCController/app/src/main/java/com/ttstd/controller/MainActivity.java +++ b/WebRTCController/app/src/main/java/com/ttstd/controller/MainActivity.java @@ -19,6 +19,7 @@ import android.widget.RadioGroup; import android.widget.Switch; import android.view.SurfaceView; import android.view.SurfaceHolder; +import android.view.Gravity; import com.ttstd.controller.webrtc.SelfCodecDecoder; 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() { String serverUrl = etServerUrl.getText().toString().trim(); @@ -653,6 +705,7 @@ public class MainActivity extends AppCompatActivity { tvStreamMode = findViewById(R.id.tv_stream_mode); selfCodecSurface = findViewById(R.id.self_codec_surface); selfCodecDecoder = new SelfCodecDecoder(); + selfCodecDecoder.setOnResolutionUpdateListener((w, h) -> adjustSelfCodecSize(w, h)); surfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { @@ -688,6 +741,9 @@ public class MainActivity extends AppCompatActivity { selfCodecSurface.setVisibility(View.VISIBLE); remoteVideoView.setVisibility(View.GONE); selfCodecDecoder.setEnabled(true); + if (selfCodecDecoder.getVideoWidth() > 0) { + adjustSelfCodecSize(selfCodecDecoder.getVideoWidth(), selfCodecDecoder.getVideoHeight()); + } } else { selfCodecSurface.setVisibility(View.GONE); remoteVideoView.setVisibility(View.VISIBLE); @@ -706,6 +762,9 @@ public class MainActivity extends AppCompatActivity { selfCodecSurface.setVisibility(View.VISIBLE); remoteVideoView.setVisibility(View.GONE); selfCodecDecoder.setEnabled(true); + if (selfCodecDecoder.getVideoWidth() > 0) { + adjustSelfCodecSize(selfCodecDecoder.getVideoWidth(), selfCodecDecoder.getVideoHeight()); + } } else { selfCodecSurface.setVisibility(View.GONE); remoteVideoView.setVisibility(View.VISIBLE); diff --git a/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/SelfCodecDecoder.java b/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/SelfCodecDecoder.java index d1606e3..f33a5db 100644 --- a/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/SelfCodecDecoder.java +++ b/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/SelfCodecDecoder.java @@ -38,6 +38,9 @@ public class SelfCodecDecoder { private int videoWidth = PLACEHOLDER_W; private int videoHeight = PLACEHOLDER_H; + // 分辨率变化回调(通知 UI 调整渲染 Surface 尺寸以保持宽高比,避免画面被拉伸变形) + private OnResolutionUpdateListener resolutionListener; + // 待喂入的参数集(SPS/PPS 等),以 CODEC_CONFIG 形式输入解码器 private final ArrayDeque pendingConfigs = new ArrayDeque<>(); // 待解码帧 @@ -86,6 +89,9 @@ public class SelfCodecDecoder { // 分辨率变化时需重置解码器以重新 configure releaseDecoder(); } + if (resolutionListener != null) { + resolutionListener.onResolutionUpdated(w, h); + } } } @@ -333,4 +339,21 @@ public class SelfCodecDecoder { releaseDecoder(); 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); + } }