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

Flutter使用camera包录制视频时,如何实现录制中切换摄像头?

Flutter录制视频时切换摄像头的实现方案

由于官方camera包不支持录制过程中实时切换摄像头,目前可行的方案是分段录制视频片段,录制完成后合并所有片段为单个视频。以下是具体实现步骤和代码示例:

核心思路

  1. 每次切换摄像头前,先停止当前录制并保存临时视频片段
  2. 切换摄像头并初始化新的控制器后,继续录制新的片段
  3. 所有录制完成后,使用FFmpeg将所有片段按顺序合并为一个完整视频

依赖准备

在pubspec.yaml中添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  camera: ^0.10.0+1
  video_player: ^2.6.1
  flutter_ffmpeg: ^0.4.0

代码实现

1. 初始化变量与摄像头

import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ffmpeg/flutter_ffmpeg.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_player/video_player.dart';

class CameraRecordPage extends StatefulWidget {
  const CameraRecordPage({super.key});

  @override
  State<CameraRecordPage> createState() => _CameraRecordPageState();
}

class _CameraRecordPageState extends State<CameraRecordPage> {
  late CameraController _cameraController;
  List<CameraDescription> _cameras = [];
  bool _isRecording = false;
  List<String> _videoSegments = [];
  String _finalVideoPath = '';
  VideoPlayerController? _finalVideoController;

  @override
  void initState() {
    super.initState();
    _initCameras();
  }

  Future<void> _initCameras() async {
    _cameras = await availableCameras();
    await _initCameraController(_cameras.firstWhere((cam) => cam.lensDirection == CameraLensDirection.back));
  }

  Future<void> _initCameraController(CameraDescription camera) async {
    if (_cameraController.value.isInitialized) {
      await _cameraController.dispose();
    }
    _cameraController = CameraController(
      camera,
      ResolutionPreset.high,
      enableAudio: true,
    );

    try {
      await _cameraController.initialize();
      setState(() {});
    } catch (e) {
      print('摄像头初始化失败: $e');
    }
  }

2. 录制与摄像头切换逻辑

Future<void> _toggleRecording() async {
    if (_isRecording) {
      // 停止当前录制,保存片段
      final path = await _cameraController.stopVideoRecording();
      _videoSegments.add(path.path);
      setState(() => _isRecording = false);
    } else {
      // 启动新的录制
      await _cameraController.startVideoRecording();
      setState(() => _isRecording = true);
    }
  }

  Future<void> _switchCamera() async {
    if (_isRecording) {
      // 录制中切换,先保存当前片段
      final path = await _cameraController.stopVideoRecording();
      _videoSegments.add(path.path);
    }

    // 切换前后摄像头
    final currentLens = _cameraController.description.lensDirection;
    final targetCamera = _cameras.firstWhere(
      (cam) => cam.lensDirection == (currentLens == CameraLensDirection.back 
          ? CameraLensDirection.front 
          : CameraLensDirection.back)
    );

    await _initCameraController(targetCamera);

    if (_isRecording) {
      // 恢复录制
      await _cameraController.startVideoRecording();
    }
  }

3. 视频合并与预览

Future<void> _mergeVideos() async {
    if (_videoSegments.isEmpty) return;

    // 生成FFmpeg需要的片段列表文件
    final tempDir = await getTemporaryDirectory();
    final segmentsFile = File('${tempDir.path}/segments.txt');
    await segmentsFile.writeAsString(_videoSegments.map((path) => "file '$path'").join('\n'));

    // 生成最终视频存储路径
    final appDir = await getApplicationDocumentsDirectory();
    _finalVideoPath = '${appDir.path}/final_${DateTime.now().millisecondsSinceEpoch}.mp4';

    // 执行FFmpeg合并命令(采用concat协议,不重新编码,速度快)
    final ffmpeg = FlutterFFmpeg();
    final result = await ffmpeg.executeWithArguments([
      '-f', 'concat',
      '-safe', '0',
      '-i', segmentsFile.path,
      '-c', 'copy',
      _finalVideoPath
    ]);

    if (result == 0) {
      print('视频合并完成: $_finalVideoPath');
      // 初始化预览控制器
      _finalVideoController = VideoPlayerController.file(File(_finalVideoPath))
        ..initialize().then((_) => setState(() {}));
    } else {
      print('视频合并失败,错误码: $result');
    }

    // 清理临时文件
    for (final path in _videoSegments) {
      await File(path).delete();
    }
    await segmentsFile.delete();
    _videoSegments.clear();
  }

4. UI布局

@override
  Widget build(BuildContext context) {
    if (!_cameraController.value.isInitialized) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      body: Stack(
        children: [
          // 摄像头预览
          CameraPreview(_cameraController),
          // 底部操作栏
          Positioned(
            bottom: 30,
            left: 0,
            right: 0,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // 录制/停止按钮
                IconButton(
                  icon: Icon(
                    _isRecording ? Icons.stop_circle : Icons.circle,
                    color: Colors.red,
                    size: 60,
                  ),
                  onPressed: _toggleRecording,
                ),
                const SizedBox(width: 40),
                // 切换摄像头按钮
                IconButton(
                  icon: const Icon(Icons.flip_camera_ios, color: Colors.white, size: 30),
                  onPressed: _switchCamera,
                ),
                const SizedBox(width: 40),
                // 合并视频按钮(仅非录制状态且有片段时显示)
                if (!_isRecording && _videoSegments.isNotEmpty)
                  ElevatedButton(
                    onPressed: _mergeVideos,
                    child: const Text('合并视频'),
                  ),
              ],
            ),
          ),
          // 合并后的视频预览
          if (_finalVideoController?.value.isInitialized ?? false)
            Positioned(
              top: 20,
              right: 20,
              child: SizedBox(
                width: 100,
                height: 150,
                child: VideoPlayer(_finalVideoController!),
              ),
            ),
        ],
      ),
      floatingActionButton: _finalVideoController?.value.isInitialized ?? false
          ? FloatingActionButton(
              onPressed: () {
                setState(() {
                  _finalVideoController!.value.isPlaying
                      ? _finalVideoController!.pause()
                      : _finalVideoController!.play();
                });
              },
              child: Icon(
                _finalVideoController!.value.isPlaying ? Icons.pause : Icons.play_arrow,
              ),
            )
          : null,
    );
  }

  @override
  void dispose() {
    _cameraController.dispose();
    _finalVideoController?.dispose();
    super.dispose();
  }
}

注意事项

  • 权限配置:需要在Android的AndroidManifest.xml和iOS的Info.plist中添加摄像头、麦克风、存储权限
  • FFmpeg版本:如果需要支持更多视频格式,可选择flutter_ffmpeg的full版本(具体版本可根据实际情况调整)
  • 临时文件管理:合并完成后务必删除临时片段文件,避免占用设备存储空间

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 13:54:51