Apparence-io / CamerAwesome

📸 Embedding a camera experience within your own app shouldn't be that hard. A flutter plugin to integrate awesome Android / iOS camera experience.
https://ApparenceKit.dev
MIT License
947 stars 234 forks source link

Video capturing fails right after start (version 1.0.0) #202

Closed KirioXX closed 1 year ago

KirioXX commented 1 year ago

Steps to Reproduce

Build a custom UI and try to start a video. This is my UI:

Screen

import 'package:auto_route/auto_route.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:camerawesome/pigeon.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:app/views/pages/utils/camera/camera_countdown.dart';
import 'package:app/views/pages/utils/camera/camera_layout.dart';
import 'package:uuid/uuid.dart';
import 'package:video_compress/video_compress.dart';

export 'package:camerawesome/camerawesome_plugin.dart' show CaptureMode;

class CameraPageResponse {
  CameraPageResponse({required this.filePath});
  final String filePath;
}

class CameraPage extends StatelessWidget {
  const CameraPage({
    super.key,
    this.captureMode = CaptureMode.photo,
    this.maxVideoDuration,
  });

  final CaptureMode captureMode;
  final Duration? maxVideoDuration;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CameraAwesomeBuilder.custom(
        saveConfig: captureMode == CaptureMode.photo
            ? SaveConfig.photo(
                pathBuilder: () async {
                  final extDir = await getApplicationDocumentsDirectory();
                  return '${extDir.path}/${const Uuid().v4()}.jpg';
                },
              )
            : SaveConfig.video(
                pathBuilder: () async {
                  final extDir = await getApplicationDocumentsDirectory();
                  return '${extDir.path}/${const Uuid().v4()}.mp4';
                },
              ),
        exifPreferences: ExifPreferences(
          saveGPSLocation: true,
        ),
        builder: (cameraState, previewSize, previewRect) {
          return cameraState.when(
            onPreparingCamera: (state) => const Center(
              child: CircularProgressIndicator(),
            ),
            onPhotoMode: (state) => _TakePhotoUI(state),
            onVideoMode: (state) => _RecordVideoUI(state),
            onVideoRecordingMode: (state) => _RecordVideoUI(
              state,
              maxVideoDuration: maxVideoDuration,
            ),
          );
        },
      ),
    );
  }
}

class _TakePhotoUI extends StatefulWidget {
  final PhotoCameraState state;

  const _TakePhotoUI(this.state);

  @override
  State<_TakePhotoUI> createState() => _TakePhotoUIState();
}

class _TakePhotoUIState extends State<_TakePhotoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) {
      if (event != null && event.status == MediaCaptureStatus.success) {
        context.router.pop<CameraPageResponse>(
          CameraPageResponse(filePath: event.filePath),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return AwesomeCameraLayout(state: widget.state);
  }
}

class _RecordVideoUI extends StatefulWidget {
  final CameraState state;
  final Duration? maxVideoDuration;

  const _RecordVideoUI(
    this.state, {
    this.maxVideoDuration,
  });

  @override
  State<_RecordVideoUI> createState() => _RecordVideoUIState();
}

class _RecordVideoUIState extends State<_RecordVideoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) async {
      if (event != null && event.status == MediaCaptureStatus.success) {
        final info = await VideoCompress.compressVideo(
          event.filePath,
          quality: VideoQuality.Res640x480Quality,
          deleteOrigin: false, // It's false by default
        );
        if (info?.path != null) {
          context.router.pop<CameraPageResponse>(
            CameraPageResponse(filePath: info!.path!),
          );
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        AwesomeCameraLayout(state: widget.state),
        if (widget.state is VideoRecordingCameraState &&
            widget.maxVideoDuration != null)
          Positioned(
            bottom: 20,
            right: 10,
            child: CameraCountdown(
              time: widget.maxVideoDuration!,
              callback: () {
                (widget.state as VideoRecordingCameraState).stopRecording();
              },
            ),
          ),
      ],
    );
  }
}

Camera Layout:

import 'package:auto_route/auto_route.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:flutter/material.dart';

class AwesomeCameraLayout extends StatelessWidget {
  final CameraState state;

  const AwesomeCameraLayout({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const SizedBox(height: 16),
        AwesomeTopActions(state: state),
        const Spacer(),
        AwesomeBackground(
          child: Column(children: [
            const SizedBox(height: 12),
            AwesomeCameraModeSelector(state: state),
            AwesomeBottomActions(state: state),
            const SizedBox(height: 32),
          ]),
        ),
      ],
    );
  }
}

class AwesomeTopActions extends StatelessWidget {
  final CameraState state;

  const AwesomeTopActions({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        const AutoLeadingButton(
          color: Colors.white,
        ),
        AwesomeFlashButton(state: state),
        AwesomeAspectRatioButton(state: state),
      ],
    );
  }
}

class AwesomeBottomActions extends StatelessWidget {
  final CameraState state;

  const AwesomeBottomActions({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Flexible(
            flex: 0,
            child: AwesomeCameraSwitchButton(state: state),
          ),
          AwesomeCaptureButton(
            state: state,
          ),
          const Flexible(
            flex: 0,
            child: SizedBox(width: 72, height: 72),
          ),
        ],
      ),
    );
  }
}

class AwesomeBackground extends StatelessWidget {
  final Widget child;

  const AwesomeBackground({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black54,
      child: child,
    );
  }
}

Camera Countdown:

import 'dart:async';

import 'package:flutter/material.dart';

class CameraCountdown extends StatefulWidget {
  const CameraCountdown({
    Key? key,

    /// Sets the inital countimer time
    required this.time,

    /// Triggered when the countown hits 0
    required this.callback,
  }) : super(key: key);

  final Duration time;
  final Function callback;

  @override
  State<CameraCountdown> createState() => _CameraCountdownState();
}

class _CameraCountdownState extends State<CameraCountdown> {
  late Duration _currentTime;
  late final Timer _timer;

  @override
  void initState() {
    super.initState();
    _currentTime = widget.time;
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      final newTime = _currentTime - const Duration(seconds: 1);
      if (newTime == Duration.zero) {
        widget.callback();
        _timer.cancel();
      } else {
        setState(() {
          _currentTime = newTime;
        });
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _timer.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Text(
      '${_currentTime.inSeconds}s',
      style: const TextStyle(
        color: Colors.white,
        fontSize: 30.0,
      ),
    );
  }
}

Expected results

Video capturing starts without exceptions.

Actual results

When I start the video recording I get this error camera stream is in progress, please stop streaming before and when I try to stop the video it fails with a message that the stream is not running.

About your device

Brand Model OS
Apple iPad (7th generation) 16.3

ghost commented 1 year ago

Hello, I can't reproduce the issue, are you sure to not instantiate imageAnalysisConfig anywhere in your app ?

KirioXX commented 1 year ago

Hi @dim-apparence , thanks for the quick response. We do use mobile_scanner for barcode scanning at the moment, could that maybe collide?

ghost commented 1 year ago

So I found the issue, it will be fixed 👍 Thanks for your help.