HaishinKit.dart
HaishinKit.dart copied to clipboard
StreamTextureView not showing camera preview on iOS
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
- Use StreamViewTexture as per example
- attachVideo as per example
- See no preview of camera
- Start streaming (works)
Expected behavior
- StreamViewTexture(_stream) should return the preview from camera on Flutter
Version
Latest from Main
Smartphone info.
- iOS 18 (iPhone 15 pro max)
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
Relevant log output
No response