shogo4405 / HaishinKit.dart

Camera and Microphone streaming library via RTMP for Flutter.
https://pub.dev/packages/haishin_kit
BSD 3-Clause "New" or "Revised" License
31 stars 19 forks source link

StreamTextureView not showing camera preview on iOS #63

Open dylandamsma opened 1 month ago

dylandamsma commented 1 month ago

Describe the bug

The StreamViewTexture widget is not returning a preview on iOS. The RtmpStream#registerTexture method in RTMPStreamHandler.swift seems to be incorrect.

To Reproduce

  1. Use StreamViewTexture as per example
  2. attachVideo as per example
  3. See no preview of camera
  4. Start streaming (works)

Expected behavior

  1. StreamViewTexture(_stream) should return the preview from camera on Flutter

Version

Latest from Main

Smartphone info.

Additional context

import 'dart:async';

import 'package:audio_session/audio_session.dart';
import 'package:baseline/resources/constants/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:haishin_kit/video_settings.dart';
import 'package:keep_screen_on/keep_screen_on.dart';
import 'package:nylo_framework/nylo_framework.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:haishin_kit/audio_source.dart';
import 'package:haishin_kit/stream_view_texture.dart';
import 'package:haishin_kit/rtmp_connection.dart';
import 'package:haishin_kit/rtmp_stream.dart';
import 'package:haishin_kit/video_source.dart';

class HaishinInterfacePage extends NyStatefulWidget {
  static const path = '/stream-interface';

  HaishinInterfacePage({super.key})
      : super(path, child: () => _HaishinInterfacePageState());
}

class _HaishinInterfacePageState extends NyState<HaishinInterfacePage> {
  RtmpConnection? _connection;
  RtmpStream? _stream;
  bool _recording = false;
  CameraPosition currentPosition = CameraPosition.back;

  @override
  void initState() {
    super.initState();
    NyLogger.info("HaishinInterfacePage initState");

    KeepScreenOn.turnOn();

    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeRight,
      DeviceOrientation.landscapeLeft,
    ]);

    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);

    setupStream();
  }

  void setupStream() async {
    try {
      await Permission.camera.request();
      await Permission.microphone.request();

      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) {
        NyLogger.info('Event received: $event');
        if (event != null) {
          var data = event["data"];
          if (data != null) {
            NyLogger.info('Event data: $data');
            var code = data["code"];
            if (code != null) {
              NyLogger.info('Event code: $code');
              switch (code) {
                case 'NetConnection.Connect.Success':
                  _stream?.videoSettings.width = 1920;
                  _stream?.videoSettings.height = 1080;
                  _stream?.videoSettings.bitrate = 4000 * 1000; // Bitrate HD
                  _stream?.videoSettings.profileLevel = ProfileLevel.h264High52;
                  _stream?.videoSettings.frameInterval = 1;
                  _stream?.publish('8d63-yapg-0h78-rhxf-e3u5');
                  setState(() {
                    _recording = true;
                  });
                  break;
              }
            } else {
              NyLogger.error('Code is null in event data');
            }
          } else {
            NyLogger.error('Event data is null');
          }
        } else {
          NyLogger.error('Event is null');
        }
      });

      RtmpStream stream = await RtmpStream.create(connection);
      stream.attachAudio(AudioSource());
      stream.attachVideo(VideoSource(position: currentPosition));

      if (!mounted) return;

      setState(() {
        NyLogger.info('Connection created');
        _connection = connection;
        _stream = stream;
      });
    } catch (e) {
      NyLogger.error("Initialization error: $e");
    }
  }

  @override
  void dispose() {
    _stopStream();
    KeepScreenOn.turnOff();
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    super.dispose();
  }

  void _startStream() async {
    if (_recording) {
      _connection?.close();
      setState(() {
        _recording = false;
      });
    } else {
      _connection?.connect("rtmp://a.rtmp.youtube.com/live2");
    }
  }

  void _stopStream() async {
    _connection?.close();
  }

  @override
  Widget view(BuildContext context) {
    return Scaffold(
      backgroundColor: BColors.gray900,
      body: Stack(
        children: [
          SizedBox(
            width: double.infinity,
            height: double.infinity,
            child: StreamViewTexture(_stream),
          ),
          Positioned(
            left: 36,
            right: 36,
            top: 24,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Flexible(
                  child: Image.asset(
                    'public/assets/images/baseline-light.png',
                    width: 120,
                  ),
                ),
                ElevatedButton(
                  onPressed: () async {
                    NyLogger.info('Pressed Start Stream');
                    _startStream();
                  },
                  style: ElevatedButton.styleFrom(
                    elevation: 0,
                    padding:
                        const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(4),
                    ),
                  ),
                  child: const Text(
                    "Toggle Stream",
                    style: TextStyle(color: Colors.white, fontSize: 16),
                    textAlign: TextAlign.center,
                  ),
                ),
              ],
            ),
          ),
          Positioned(
            left: 36,
            right: 36,
            bottom: 24,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Flexible(
                  child: Container(
                    padding:
                        const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    decoration: BoxDecoration(
                      color: Colors.black.withOpacity(0.5),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: const Text(
                      "Score will be displayed here on stream",
                      style: TextStyle(color: Colors.white, fontSize: 16),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
                ElevatedButton(
                  onPressed: () {
                    // Add your instructions logic here
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: BColors.gray700,
                    elevation: 0,
                    padding:
                        const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(4),
                    ),
                  ),
                  child: const Text(
                    "Instructions",
                    style: TextStyle(color: Colors.white, fontSize: 16),
                    textAlign: TextAlign.center,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Screenshots

image image

Relevant log output

No response

shogo4405 commented 1 month ago

I couldn't figure out how to test it locally. I would like a minimal project that can be built. It works with this code: https://github.com/shogo4405/HaishinKit.dart/blob/main/example/lib/main.dart#L34-L160, so I believe the issue lies in the combination with the code you provided.