Flutter相机录制时实时切换摄像头功能实现求助
录制中实时切换摄像头并合并视频的解决方案
核心思路是录制多段视频片段,切换摄像头时无缝衔接录制,最后合并所有片段为完整视频——原生camera库不支持录制中直接切换摄像头,必须通过分段录制+后期合并实现。
步骤1:添加依赖
在pubspec.yaml中添加视频合并所需的FFmpeg库:
dependencies: camera: ^0.10.5+5 ffmpeg_kit_flutter: ^5.1.0 lottie: ^2.6.0 video_player: ^2.8.6 flutter_svg: ^2.0.9
步骤2:修改完整代码
import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:video_player/video_player.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:ffmpeg_kit_flutter/ffmpeg_kit.dart'; import 'package:path_provider/path_provider.dart'; class CameraaPreview extends StatefulWidget { final List<CameraDescription> cameras; final bool permissionStatus; const CameraaPreview({ super.key, required this.cameras, required this.permissionStatus, }); @override State<CameraaPreview> createState() => _CameraaPreviewState(); } class _CameraaPreviewState extends State<CameraaPreview> with SingleTickerProviderStateMixin { late CameraController _cameraController; late AnimationController _lottieController; Future<void>? _initializeFutureController; VideoPlayerController? _controller; XFile? image; File? _finalVideo; bool _havePic = false; bool _haveVideo = false; bool _isRecording = false; bool _flashOpen = false; late Timer _timer; bool isBackCameraOpen = true; int _recordedSeconds = 0; List<File> _recordedSegments = []; // 保存所有录制片段 @override void initState() { _initializeCameraController(); _lottieController = AnimationController( vsync: this, duration: const Duration(seconds: 1), ); super.initState(); } @override void dispose() { _cameraController.dispose(); _controller?.dispose(); _lottieController.dispose(); _timer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; const double navBarHeight = kBottomNavigationBarHeight; final avatarSize = size.width * 0.0999; final avataPosition = (navBarHeight - avatarSize) / 0.35; return _havePic ? _showImage(image!) : _haveVideo ? _showVideo(_finalVideo!) : FutureBuilder( future: _initializeFutureController, builder: (context, snapshot) { return Stack( children: <Widget>[ if (_cameraController.value.isInitialized) AspectRatio( aspectRatio: 1.09 / _cameraController.value.aspectRatio, child: CameraPreview(_cameraController), ), // 录制按钮 Positioned( bottom: avataPosition / 9, left: size.width / 2.9 - avatarSize / 2, child: GestureDetector( onLongPressStart: (_) { _lottieController.forward(); _lottieController.repeat( reverse: true, period: const Duration(seconds: 1), ); _startRecording(); }, onLongPressEnd: (_) async { _lottieController.stop(); _lottieController.reset(); await _stopAndMergeRecording(); }, onTap: () => _clickPicture(), child: Lottie.asset( "assets/recordingButton.json", controller: _lottieController, height: 155, width: 155, fit: BoxFit.cover, ), ), ), // 摄像头切换按钮 Positioned( top: 65, right: size.width * 0.05, child: GestureDetector( onTap: () => _toggleCamera(), child: SvgPicture.asset( "assets/camerarotate.svg", color: Colors.white, height: 25, ), ), ), if (_isRecording) Positioned( top: 20, left: size.width / 2.25, child: Text( '$_recordedSeconds s', style: const TextStyle( color: Colors.red, fontSize: 24, ), ), ), ], ); }, ); } void _clickPicture() async { if (_cameraController.value.isInitialized) { await _cameraController.setFlashMode( _flashOpen ? FlashMode.always : FlashMode.off, ); image = await _cameraController.takePicture(); setState(() => _havePic = true); } } void _startRecording() async { if (_cameraController.value.isInitialized && !_isRecording) { await _cameraController.setFlashMode( _flashOpen ? FlashMode.always : FlashMode.off, ); await _cameraController.startVideoRecording(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { setState(() => _recordedSeconds += 1); }); setState(() => _isRecording = true); } } Future<void> _stopCurrentSegment() async { if (_cameraController.value.isRecordingVideo) { _timer.cancel(); final segment = await _cameraController.stopVideoRecording(); _recordedSegments.add(File(segment.path)); setState(() => _isRecording = false); } } Future<void> _toggleCamera() async { if (_isRecording) { // 录制中切换:先停止当前片段,切换摄像头后立即开始录制 await _stopCurrentSegment(); await _switchCamera(); _startRecording(); } else { // 非录制状态直接切换 await _switchCamera(); } } Future<void> _switchCamera() async { await _cameraController.dispose(); final newCamera = isBackCameraOpen ? widget.cameras[1] : widget.cameras[0]; _cameraController = CameraController( newCamera, ResolutionPreset.ultraHigh, enableAudio: true, imageFormatGroup: ImageFormatGroup.jpeg, ); setState(() { _initializeFutureController = _cameraController.initialize(); isBackCameraOpen = !isBackCameraOpen; }); await _initializeFutureController; } Future<void> _stopAndMergeRecording() async { if (_isRecording) { await _stopCurrentSegment(); } if (_recordedSegments.isEmpty) return; // 生成合并后的视频文件路径 final tempDir = await getTemporaryDirectory(); _finalVideo = File('${tempDir.path}/final_video.mp4'); // 生成FFmpeg命令:合并多个视频片段 final segmentListPath = '${tempDir.path}/segments.txt'; final segmentFile = File(segmentListPath); final content = _recordedSegments .map((file) => "file '${file.path}'") .join('\n'); await segmentFile.writeAsString(content); // 执行FFmpeg合并命令 await FFmpegKit.execute( "-f concat -safe 0 -i $segmentListPath -c copy ${_finalVideo!.path}", ); // 初始化播放控制器 _controller = VideoPlayerController.file(_finalVideo!) ..initialize().then((_) { setState(() => _haveVideo = true); _controller?.setLooping(true); _controller?.play(); }); // 清理临时片段文件 for (var file in _recordedSegments) { await file.delete(); } await segmentFile.delete(); _recordedSegments.clear(); _recordedSeconds = 0; } Widget _showImage(XFile image) { return Stack( children: [ SizedBox.expand( child: Image.file( File(image.path), fit: BoxFit.cover, ), ), Positioned( top: 55, left: size.width * 0.05, child: GestureDetector( onTap: () => setState(() => _havePic = false), child: SvgPicture.asset( "assets/cancel.svg", color: Colors.white, height: 25, ), ), ), ], ); } Future<void> _initializeCameraController() async { _cameraController = CameraController( widget.cameras[0], ResolutionPreset.ultraHigh, enableAudio: true, imageFormatGroup: ImageFormatGroup.jpeg, ); setState(() { _initializeFutureController = _cameraController.initialize(); }); } Widget _showVideo(File video) { return Stack( children: [ SizedBox.expand( child: VideoPlayer(_controller!), ), Positioned( top: 55, left: size.width * 0.05, child: GestureDetector( onTap: () { setState(() => _haveVideo = false); _controller?.pause(); }, child: SvgPicture.asset( "assets/cancel.svg", color: Colors.white, height: 25, ), ), ), ], ); } }
关键改动说明
- 分段录制管理:新增
_recordedSegments列表保存每次切换摄像头前后的视频片段,切换时先停止当前片段录制,再切换摄像头并启动新录制。 - 摄像头切换逻辑:重写
_toggleCamera方法,区分录制中/非录制状态的切换逻辑,保证录制无缝衔接。 - 视频合并:使用FFmpeg将多个片段合并为单个视频,通过生成片段列表文件的方式执行合并命令,最后清理临时文件。
- 状态修正:调整了变量命名、播放控制器初始化逻辑,避免空指针异常。
内容的提问来源于stack exchange,提问作者RIZWAN ALI
相关产品推荐
相关产品推荐

