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

Flutter中Render 0.1.3+1录制Widget功能异常求助

问题:Flutter摄像头录制时将计时器叠加到视频中

我正在开发一款Flutter摄像头应用,使用camera依赖。核心需求是:在摄像头界面显示计时器,并且将计时器与摄像头画面一同录制到视频里,确保视频在相册查看时仍能看到计时器。

由于camera依赖无法实现该需求,我尝试使用render依赖(版本0.1.3+1),通过录制屏幕上的CameraPreview组件和计时器Widget来达成目标,但当前该功能失效。

当前实现代码

// ignore_for_file: use_build_context_synchronously, unnecessary_null_comparison, avoid_print

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_app/providers/countdown_provider.dart';
import 'package:video_app/providers/countup_provider.dart';
import 'package:video_app/providers/tabata_provider.dart';
import 'package:video_app/screens/timer_screen.dart';
import 'package:video_app/screens/video_screen.dart';
import 'package:render/render.dart';


class CameraPage extends StatefulWidget {
  const CameraPage({super.key, required this.timerType});
  final TimerType timerType;

  @override
  CameraPageState createState() => CameraPageState();
}

enum WidgetState {none, loading, loaded, error}

class CameraPageState extends State<CameraPage> {
  bool _isRecording = false;
  WidgetState widgetState = WidgetState.none;
  late CameraController _cameraController;
  Offset? _focusPoint;
  bool _isFlashOn = false;
  late MotionRecorder stopController;
  final RenderController renderController = RenderController(logLevel: LogLevel.debug);
  // final renderController = RenderController();
  
  @override
  void initState() {
    
    _initCamera();
    super.initState();
  }

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

  _initCamera() async {
    widgetState = WidgetState.loading;
    final cameras = await availableCameras();
    final back = cameras.firstWhere((camera) => camera.lensDirection == CameraLensDirection.back);
    _cameraController = CameraController(back, ResolutionPreset.max);
    await _cameraController.initialize();
    //setState(() => _isLoading = false);
    if (_cameraController.value.hasError) {
      widgetState = WidgetState.error;
      if (mounted) setState(() {});
    } else {
      widgetState = WidgetState.loaded;
      if (mounted) setState(() {});
    }
  }

  _recordVideo() async {
    if (_isRecording == false) {
      stopController = renderController.recordMotion();
      // await _cameraController.prepareForVideoRecording();
      // await _cameraController.startVideoRecording();
      setState(() => _isRecording = true);
    } else {
      RenderResult result = await stopController.stop();
      setState(() => _isRecording = false);
      File refile = result.output;
      XFile file = XFile(refile.path);
      // final file = await _cameraController.stopVideoRecording();
      // setState(() => _isRecording = false);
      final route = MaterialPageRoute(
        fullscreenDialog: true,
        builder: (_) => VideoPage(file: file),
      );
      Navigator.push(context, route);
    }
  }

  @override
  Widget build(BuildContext context) {
    final countdownProvider = Provider.of<CountDownProvider>(context);
    final countupProvider = Provider.of<CountupProvider>(context);
    final tabataProvider = Provider.of<TabataProvider>(context);
    switch (widgetState) {
      case WidgetState.none:
      case WidgetState.loading:
        return buildScaffold(context, const Text('Iniciando camara...'));
      case WidgetState.loaded:
        return SafeArea(
          child: Scaffold(
            body: LayoutBuilder(
              builder: (BuildContext context, BoxConstraints constraints) {
                return Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  //

                  Positioned.fill(
                    top: 50,
                    child: AspectRatio(
                      aspectRatio: _cameraController.value.aspectRatio,
                      child: GestureDetector(
                        onTapDown: (TapDownDetails details){
                          final Offset tapPosition = details.localPosition;
                          final Offset relativTapPosition = Offset(
                            tapPosition.dx / constraints.maxWidth,
                            tapPosition.dy / constraints.maxHeight,
                          );
                          _setFocusPoint(relativTapPosition);
                        },
                        child: Render(
                          controller: renderController,
                          child: CameraPreview(_cameraController,
                            child:
                              Positioned(
                                top: 0,
                                right: 10,
                                child: Container(
                                  color: Colors.transparent,
                                  child: Text( widget.timerType != TimerType.tabata 
                                      ? (widget.timerType == TimerType.countdown
                                        ? context.select((CountDownProvider count) => count.timeLeftString)
                                        : context.select((CountupProvider count) => count.timeLeftString)) 
                                      : context.select((TabataProvider count) => count.timeLeftString), 
                                    style: const TextStyle(fontSize: 50, fontWeight: FontWeight.normal, color: Colors.white),),
                                ),
                              ),
                          ),
                        ),
                      ),
                    ),
                  );
          
                  Positioned(
                      bottom: 0,
                      child: Padding(
                        padding: const EdgeInsets.all(25),
                        child: IconButton(
                          icon: Icon(_isRecording ? Icons.stop : Icons.circle, color: _isRecording ? Colors.black : Colors.red), iconSize: 75,
                          onPressed: ()  {
                            switch (widget.timerType){
                                case TimerType.countdown:
                                  countdownProvider.startStopTimer();
                                  break;
                                case TimerType.countup:
                                  countupProvider.startStopTimer();
                                  break;
                                case TimerType.tabata:
                                  tabataProvider.startStopTimer();
                                  break;
                              }
                            _recordVideo();
                          },
                        ),
                      ),
                  ),
                ],
              );
              },
            ),
          ),
        );
      case WidgetState.error:
        return buildScaffold(context, const Text("¡Ooops! Error al cargar la cámara 😩. Reinicia la apliación."));
    }}

    Widget buildScaffold(BuildContext context, Text text) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text("Cámara"),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Center(
                child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: [
                          if(widgetState == WidgetState.loading)
                            const CircularProgressIndicator(),
                          text
                        ],
                      ),
            ),
    );
    }


    
}

参考应用

  • Google Play的“WeTime”
  • Google Play和App Store的“WODProof”

目前未找到其他可行实现方式,诚邀提供解决方案或思路。


内容的提问来源于stack exchange,提问作者Ignacio Lanzzavecchia Cespedes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 03:13:13