You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity3D Android插件录制摄像头视频失败问题求助

嘿,我仔细看了你的问题和代码,你遇到的两个核心问题——窗口权限错误和MediaRecorder停止时的非法状态,其实都是因为几个常见的Android摄像头录制坑没踩对,我给你梳理下解决办法,还有修改后的代码参考:

一、先解决WindowManager$BadTokenException(窗口权限被拒)

你之前的错误是因为Service作为后台组件,没有权限创建前台窗口,而你依赖的SurfaceView是需要绑定到Activity窗口的,在Service里直接用就会触发权限问题。解决思路有两个:

  • 改用SurfaceTexture替代SurfaceView:不需要显示预览窗口,用虚拟纹理给摄像头提供预览目标,完全避开窗口创建的问题
  • 把Service升级为前台服务:Android 8.0+对后台组件限制严格,前台服务可以获得必要的权限,需要在Manifest里声明权限并启动前台通知

我推荐第一种方案,因为录制摄像头不一定需要显示预览,更适合Unity插件的场景。

二、解决MediaRecorder.stop()的IllegalStateException和0KB文件

这个异常几乎都是因为MediaRecorder的配置流程错误、参数无效,或者资源释放顺序不对导致的,你的代码里有几个明显的问题:

  1. 视频尺寸设置无效:你写了mMediaRecorder.setVideoSize(1, 1);,这会导致MediaRecorder无法正常录制
  2. 配置顺序错误:MediaRecorder的API有严格的调用顺序,错一步就会触发非法状态
  3. SurfaceHolder状态未就绪:你在Activity里创建的SurfaceView,到Service里使用时可能还没初始化完成,导致预览Surface无效
  4. 资源释放顺序混乱:停止录制时的步骤顺序错了,应该先停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" />

关键修改点说明

  1. 用SurfaceTexture替代SurfaceView:完全避免了创建窗口的需求,从根源解决Window权限问题
  2. 严格遵循MediaRecorder配置顺序:这个是Android官方明确要求的,顺序错了必出异常
  3. 设置有效的视频尺寸:和摄像头预览尺寸保持一致,避免录制失败
  4. 前台服务适配:符合Android 8.0+的后台限制,不会被系统杀死
  5. 优化资源释放顺序:先停止MediaRecorder,再释放摄像头,避免非法状态

最后提醒下,Android 10+以上建议用MediaStore来保存文件,不需要WRITE_EXTERNAL_STORAGE权限,如果你需要适配更高版本,可以修改文件保存的逻辑。

内容的提问来源于stack exchange,提问作者pookie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 17:53:02