Unity3D Android插件录制摄像头视频失败问题求助
嘿,我仔细看了你的问题和代码,你遇到的两个核心问题——窗口权限错误和MediaRecorder停止时的非法状态,其实都是因为几个常见的Android摄像头录制坑没踩对,我给你梳理下解决办法,还有修改后的代码参考:
一、先解决WindowManager$BadTokenException(窗口权限被拒)
你之前的错误是因为Service作为后台组件,没有权限创建前台窗口,而你依赖的SurfaceView是需要绑定到Activity窗口的,在Service里直接用就会触发权限问题。解决思路有两个:
- 改用
SurfaceTexture替代SurfaceView:不需要显示预览窗口,用虚拟纹理给摄像头提供预览目标,完全避开窗口创建的问题 - 把Service升级为前台服务:Android 8.0+对后台组件限制严格,前台服务可以获得必要的权限,需要在Manifest里声明权限并启动前台通知
我推荐第一种方案,因为录制摄像头不一定需要显示预览,更适合Unity插件的场景。
二、解决MediaRecorder.stop()的IllegalStateException和0KB文件
这个异常几乎都是因为MediaRecorder的配置流程错误、参数无效,或者资源释放顺序不对导致的,你的代码里有几个明显的问题:
- 视频尺寸设置无效:你写了
mMediaRecorder.setVideoSize(1, 1);,这会导致MediaRecorder无法正常录制 - 配置顺序错误:MediaRecorder的API有严格的调用顺序,错一步就会触发非法状态
- SurfaceHolder状态未就绪:你在Activity里创建的SurfaceView,到Service里使用时可能还没初始化完成,导致预览Surface无效
- 资源释放顺序混乱:停止录制时的步骤顺序错了,应该先停MediaRecorder再处理摄像头
修改后的完整代码参考
1. 重构CameraService(核心修改)
public class CameraService extends Service { private static final String TAG = "RecorderService"; private Camera mServiceCamera; private boolean mRecordingStatus; private MediaRecorder mMediaRecorder; private SurfaceTexture mSurfaceTexture; // 用虚拟纹理替代SurfaceView @Override public void onCreate() { super.onCreate(); mRecordingStatus = false; // 启动前台服务(Android 8.0+必须,避免后台限制) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel("recording_channel", "Camera Recording", NotificationManager.IMPORTANCE_LOW); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } Notification notification = new NotificationCompat.Builder(this, "recording_channel") .setContentTitle("Camera Recording") .setContentText("Recording in background") .setSmallIcon(R.mipmap.ic_launcher) .build(); startForeground(1, notification); // 查找前置摄像头 int frontCameraId = -1; for (int i = 0; i < Camera.getNumberOfCameras(); i++) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(i, info); if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { frontCameraId = i; break; } } if (frontCameraId == -1) { Toast.makeText(this, "Front camera not found", Toast.LENGTH_SHORT).show(); stopSelf(); return; } mServiceCamera = Camera.open(frontCameraId); if (!startRecording()) { stopSelf(); } } @Override public IBinder onBind(Intent intent) { return null; } @Override public void onDestroy() { stopRecording(); mRecordingStatus = false; stopForeground(true); super.onDestroy(); } private boolean startRecording() { try { Toast.makeText(getBaseContext(), "Recording Started", Toast.LENGTH_SHORT).show(); // 配置摄像头预览参数 Camera.Parameters params = mServiceCamera.getParameters(); Camera.Size previewSize = getOptimalPreviewSize(params.getSupportedPreviewSizes(), 1280, 720); params.setPreviewSize(previewSize.width, previewSize.height); mServiceCamera.setParameters(params); // 创建虚拟SurfaceTexture,不需要显示预览 mSurfaceTexture = new SurfaceTexture(0); mServiceCamera.setPreviewTexture(mSurfaceTexture); mServiceCamera.startPreview(); // 解锁摄像头给MediaRecorder使用 mServiceCamera.unlock(); // 严格按照顺序配置MediaRecorder mMediaRecorder = new MediaRecorder(); mMediaRecorder.setCamera(mServiceCamera); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置和预览一致的视频尺寸 mMediaRecorder.setVideoSize(previewSize.width, previewSize.height); mMediaRecorder.setVideoFrameRate(30); mMediaRecorder.setVideoEncodingBitRate(2 * 1024 * 1024); // 2Mbps码率,保证视频质量 // 保存到公共视频目录,避免存储权限问题 String outputPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) + "/camera_recording.mp4"; mMediaRecorder.setOutputFile(outputPath); mMediaRecorder.prepare(); mMediaRecorder.start(); mRecordingStatus = true; return true; } catch (IllegalStateException e) { Log.e(TAG, "Failed to start recording (IllegalState): ", e); cleanupResources(); return false; } catch (IOException e) { Log.e(TAG, "Failed to start recording (IO): ", e); cleanupResources(); return false; } } private void stopRecording() { Toast.makeText(getBaseContext(), "Recording Stopped", Toast.LENGTH_SHORT).show(); // 先处理MediaRecorder,再处理摄像头 try { if (mMediaRecorder != null) { mMediaRecorder.stop(); mMediaRecorder.reset(); mMediaRecorder.release(); mMediaRecorder = null; } } catch (IllegalStateException e) { Log.e(TAG, "Error stopping recorder: ", e); } finally { if (mServiceCamera != null) { mServiceCamera.stopPreview(); mServiceCamera.lock(); mServiceCamera.release(); mServiceCamera = null; } if (mSurfaceTexture != null) { mSurfaceTexture.release(); mSurfaceTexture = null; } } } // 辅助方法:获取最接近目标分辨率的摄像头预览尺寸 private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int targetWidth, int targetHeight) { final double ASPECT_TOLERANCE = 0.1; double targetRatio = (double) targetWidth / targetHeight; if (sizes == null) return null; Camera.Size optimalSize = null; double minDiff = Double.MAX_VALUE; for (Camera.Size size : sizes) { double ratio = (double) size.width / size.height; if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } // 如果找不到匹配比例的,选最接近高度的 if (optimalSize == null) { minDiff = Double.MAX_VALUE; for (Camera.Size size : sizes) { if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } } return optimalSize; } // 清理资源的通用方法 private void cleanupResources() { if (mMediaRecorder != null) { mMediaRecorder.release(); mMediaRecorder = null; } if (mServiceCamera != null) { mServiceCamera.release(); mServiceCamera = null; } if (mSurfaceTexture != null) { mSurfaceTexture.release(); mSurfaceTexture = null; } } }
2. 简化CaptureManager(移除无用的SurfaceView代码)
public class CaptureManager extends UnityPlayerActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 启动摄像头录制服务 Intent intent = new Intent(CaptureManager.this, CameraService.class); startService(intent); // 1分钟后自动停止录制 new Handler(Looper.getMainLooper()).postDelayed(() -> { Intent stopIntent = new Intent(CaptureManager.this, CameraService.class); stopService(stopIntent); }, 60000); } }
3. 修正AndroidManifest配置
别忘了添加必要的权限和服务声明:
<!-- 基础权限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Android 8.0+前台服务权限 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- 摄像头特性声明 --> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.front" /> <!-- 服务声明,指定前台服务类型 --> <service android:name=".CameraService" android:foregroundServiceType="camera|microphone" />
关键修改点说明
- 用SurfaceTexture替代SurfaceView:完全避免了创建窗口的需求,从根源解决Window权限问题
- 严格遵循MediaRecorder配置顺序:这个是Android官方明确要求的,顺序错了必出异常
- 设置有效的视频尺寸:和摄像头预览尺寸保持一致,避免录制失败
- 前台服务适配:符合Android 8.0+的后台限制,不会被系统杀死
- 优化资源释放顺序:先停止MediaRecorder,再释放摄像头,避免非法状态
最后提醒下,Android 10+以上建议用MediaStore来保存文件,不需要WRITE_EXTERNAL_STORAGE权限,如果你需要适配更高版本,可以修改文件保存的逻辑。
内容的提问来源于stack exchange,提问作者pookie
相关产品推荐
相关产品推荐

