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
910 stars 199 forks source link

Streaming in Haishin Kit and Recording with CamerAwesome #386

Closed imajared closed 9 months ago

imajared commented 10 months ago

I am trying to utilise 2 plugins here at the same time here so I realise that this may not be possible.

On the tap of a button I'm starting an rtmp stream and starting the recordingstate in camerawesome. What happens is that the RTMP stream goes through but the preview screen freezes and recording only records a few frames before it outputs a video.

Expected results

I am just hoping there is something I am missing that will allow me to continue the recording.

Actual results

Frozen preview screen until screen is disposed and reinitialised.

Your flutter version

Flutter 3.13.0

My code

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:camerawesome/pigeon.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:haishin_kit/rtmp_connection.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wakelock/wakelock.dart';
import 'package:http/http.dart' as http;

//haishin kit
import 'package:audio_session/audio_session.dart';
import 'package:haishin_kit/audio_settings.dart';
import 'package:haishin_kit/audio_source.dart';
import 'package:haishin_kit/net_stream_drawable_texture.dart';
import 'package:haishin_kit/rtmp_connection.dart';
import 'package:haishin_kit/rtmp_stream.dart';
import 'package:haishin_kit/video_settings.dart';
import 'package:haishin_kit/video_source.dart';

import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';

import 'package:camerawesome/camerawesome_plugin.dart';

class LivestreamCamera extends StatefulWidget {
  @override
  final String ingestURL;
  final String id;
  final String template;

  const LivestreamCamera(
      {required this.ingestURL, required this.template, required this.id});

  _LivestreamCameraHomeState createState() {
    return _LivestreamCameraHomeState();
  }
}

class _LivestreamCameraHomeState extends State<LivestreamCamera> {

  bool streaming = false;
  bool enableAudio = true;
  RtmpConnection? _connection;
  RtmpStream? _stream;
  CameraPosition currentPosition = CameraPosition.back;

  // stats
  String livestream_status = '';
  String livestream_window = '';
  String thermalState = '';
  String batteryTemp = '';

  // Camera Awesome State
  late CameraState state;

  late Timer livestreamTimer;

  var uuid = Uuid();

  @override
  void initState() {
    Wakelock.enable();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeLeft,
    ]);
    initPlatformState();
    super.initState();
    getLivestreamStats();
    livestreamTimer = Timer.periodic(Duration(seconds: 3), (timer) {
      getLivestreamStats();
    });
  }

  @override
  void dispose() async {
    Wakelock.disable();
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.landscapeLeft, DeviceOrientation.portraitUp]);
    livestreamTimer.cancel();
    _stream?.dispose();
    _connection?.dispose();
    super.dispose();
  }

  Future<void> initPlatformState() async {
    await Permission.camera.request();
    await Permission.microphone.request();

    // Set up AVAudioSession for iOS.
    final session = await AudioSession.instance;
    await session.configure(const AudioSessionConfiguration(
      avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
      avAudioSessionCategoryOptions:
          AVAudioSessionCategoryOptions.allowBluetooth,
    ));

    RtmpConnection connection = await RtmpConnection.create();
    connection.eventChannel.receiveBroadcastStream().listen((event) {
      switch (event["data"]["code"]) {
        case 'NetConnection.Connect.Success':
            _stream?.publish("live");
          break;
        default:
          showInSnackBar(context, 'Error streaming to ${widget.ingestURL}');
          break;
      }
    });

    RtmpStream stream = await RtmpStream.create(connection);
    stream.audioSettings = AudioSettings(bitrate: 192 * 1024);
    stream.videoSettings = VideoSettings(
      width: (widget.template == '1080p30') ? 1920 : 1280,
      height: (widget.template == '1080p30') ? 1080 : 720,
      bitrate: (widget.template == '1080p30') ? 4000 * 1000 : 2000 * 1000,
    );
    stream.attachAudio(AudioSource());
    stream.attachVideo(VideoSource(position: currentPosition));

    setState(() {
      _connection = connection;
      _stream = stream;
    });
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        onWillPop: _onWillPop,
        child: CameraAwesomeBuilder.custom(
          builder: (cameraState, previewSize, previewRect) {
            return cameraState.when(
                onPreparingCamera: (state) => const Center(
                      child: CircularProgressIndicator(),
                    ),
                onVideoMode: (state) => RecordVideoUI(state),
                onVideoRecordingMode: (state) => RecordVideoUI(state)
            );

          },
          saveConfig: SaveConfig.video(pathBuilder: () async {
            final Directory extDir = await getApplicationDocumentsDirectory();
            final String dirPath = '${extDir.path}/streams';
            await Directory(dirPath).create(recursive: true);
            String filePath = '$dirPath/${uuid.v4()}.mp4';
            return filePath;
          }),
          sensor: Sensors.back,
          aspectRatio: CameraAspectRatios.ratio_16_9,
        ));
  }

  Future<bool> _onWillPop() async {
    // This dialog will exit your app on saying yes
    return await showDialog(
          context: context,
          builder: (context) => new AlertDialog(
            title: Text('Warning'),
            content: Text('Unable to leave screen while streaming'),
            actions: [
              TextButton(
                  onPressed: () {
                    Navigator.pop(context);
                  },
                  child: Text('OK')),
            ],
          ),
        ) ??
        false;
  }

  void getLivestreamStats() async {
    final prefs = await SharedPreferences.getInstance();

    var livestream = await http.get(
        Uri.parse('https://api.naoca.com/v1/livestreams/' + widget.id),
        headers: <String, String>{
          'accept': 'application/json',
          'token': (prefs.getString('token') ?? ''),
        });

    var starts_at = DateFormat('HH:mm a').format(
        DateTime.parse(jsonDecode(livestream.body)['data']['starts_at'])
            .toLocal());

    var ends_at = DateFormat('HH:mm a').format(
        DateTime.parse(jsonDecode(livestream.body)['data']['ends_at'])
            .toLocal());

    setState(() {
      livestream_status = jsonDecode(livestream.body)['data']['status']
          .toString()
          .replaceAll('_', ' ')
          .toTitleCase();
      livestream_window = starts_at + ' - ' + ends_at;
    });
  }

  Future<bool> startStreaming() async {
    try {
      _connection?.connect("rtmp://192.168.1.119/streams/live");
      streaming = true;
      showInSnackBar(context, 'Video Streaming and Recording Started');
      return true;
    } catch (e) {
      showInSnackBar(context, e.toString());
      return false;
    }
  }

  Future<void> stopStreaming() async {
    try {
      _connection?.close();
      _stream?.close();
      streaming = false;
      showInSnackBar(context, 'Video Streaming and Recording Stopped');
    } catch (e) {
      showInSnackBar(context, e.toString());
      return null;
    }
  }

  void showInSnackBar(context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
  }

   StatefulWidget RecordVideoUI(state) {
    return Scaffold(
      backgroundColor: Colors.transparent,
      body: SafeArea(
        child: RotatedBox(
          quarterTurns: -1,
          child: Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Column(children: [
                Material(
                  color: Colors.transparent,
                  child: IconButton(
                    icon: Icon(
                      Icons.arrow_back,
                      color: Colors.white,
                    ),
                    onPressed: () {
                      if (streaming) {
                        showDialog(
                            context: context,
                            builder: (context) {
                              return AlertDialog(
                                title: Text('Warning'),
                                content: Text(
                                    'Unable to leave screen while streaming'),
                                actions: [
                                  TextButton(
                                      onPressed: () {
                                        Navigator.pop(context);
                                      },
                                      child: Text('OK')),
                                ],
                              );
                            });
                      } else {
                        Navigator.pop(context);
                      }
                    },
                  ),
                ),
              ]),
              Column(
                children: [
                  Row(
                    children: [
                      Material(
                          child: Container(
                            width: 150.0,
                            child: Text(
                              livestream_status,
                              textAlign: TextAlign.center,
                            ),
                            color: Colors.white,
                            padding: EdgeInsets.all(5.0),
                          )),
                      Material(
                          child: SizedBox(
                            width: 50.0,
                          )),
                      Material(
                          child: Container(
                            width: 150.0,
                            child: Text(
                              livestream_window,
                              textAlign: TextAlign.center,
                            ),
                            color: Colors.white,
                            padding: EdgeInsets.all(5.0),
                          ))
                    ],
                  )
                ],
              ),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.max,
                children: [
                  Material(
                    color: Colors.transparent,
                    child: Stack(
                      alignment: Alignment.center,
                      children: [
                        IconButton(
                          icon: Icon(
                            Icons.circle,
                            color: streaming ? Colors.red : Colors.white,
                            size: 45.0,
                          ),
                          onPressed: () async {

                            streaming
                                ? stopStreaming()
                                : startStreaming();

                            setState(() { });

                            state.when(
                              onVideoMode: (videoState) => videoState.startRecording(),
                              onVideoRecordingMode: (videoState) => videoState.stopRecording(),
                            );

                            setState(() { });

                          },
                        ),
                      ],
                    ),
                  )
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

}

extension StringCasingExtension on String {
  String toCapitalized() =>
      length > 0 ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
  String toTitleCase() => replaceAll(RegExp(' +'), ' ')
      .split(' ')
      .map((str) => str.toCapitalized())
      .join(' ');
}
g-apparence commented 10 months ago

Yes I don't think you can combine those two. The native camera stream seems not be listenable by multiple source. You can make some research on this but I'm pretty sure that's the problem here.

g-apparence commented 9 months ago

I close this as it's not a bug and cannot be really solved like that. Feel free to open another issue if you really think camerAwesome is the problem here.