2026-01-13 11:36:24 +05:30

782 lines
25 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:onufitness/constants/api_enum_constant.dart';
import 'package:onufitness/constants/data_constant.dart';
import 'package:onufitness/controller/get_agora_token_controller.dart';
import 'package:onufitness/screens/streamming/controllers/get_api_live_streams_controller.dart';
import 'package:onufitness/services/local_storage_services/shared_services.dart';
import 'package:onufitness/services/logger_service.dart';
import 'package:onufitness/utils/custom_sneakbar.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:agora_chat_sdk/agora_chat_sdk.dart';
// Live Stream Controller-----------------------------------------------------------------------
class LiveStreamController extends GetxController {
final logger = LoggerService();
late RtcEngine _engine;
get engine => _engine;
// Timer related variables---------------------------------------------------------------------
Timer? _streamTimer;
static const int _streamDurationMinutes = streamEndsTime;
RxInt remainingSeconds = (_streamDurationMinutes * 60).obs;
RxString timeDisplay =
"${_streamDurationMinutes.toString().padLeft(2, '0')}:00".obs;
// Observable variables------------------------------------------------------------------------
RxBool localUserJoined = false.obs;
RxInt remoteUid = 0.obs;
RxBool isHost = false.obs;
RxBool isJoined = false.obs;
RxList<LiveChatMessage> chatMessages = <LiveChatMessage>[].obs;
RxBool showChat = true.obs;
RxBool chatRoomJoined = false.obs;
RxInt memberCount = 0.obs;
// Controllers---------------------------------------------------------------------------------
final TextEditingController commentController = TextEditingController();
final ScrollController chatScrollController = ScrollController();
// Agora Configuration -------------------------------------------------------------------------
static const appId = agoraAppId;
static const String chatAppKey = agoraChatAppKey;
// Chat Room Configuration---------------------------------------------------------------------
RxString chatRoomId = "".obs;
RxString token = "".obs;
RxString channel = "".obs;
// User info------------------------------------------------------------------------------------
RxString hostName = "".obs;
RxString hostProfilePic = "".obs;
RxString currentUserId = "".obs;
RxString currentUserName = "".obs;
RxString currentUserProfilePic = "".obs;
// Add flag to track if engine needs reinitialization
bool _engineNeedsReinit = false;
//------------------------------------------------------------------------------------------------
@override
void onInit() {
super.onInit();
_initializeChatSDK();
_initializeAgoraVideoSDK();
}
@override
void onClose() {
stopStreamTimer();
_cleanupEverything();
commentController.dispose();
chatScrollController.dispose();
super.onClose();
}
// Start the 10-minute countdown timer.................................................................
void _startStreamTimer() {
stopStreamTimer();
remainingSeconds.value = _streamDurationMinutes * 60;
updateTimeDisplay();
_streamTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingSeconds.value > 0) {
remainingSeconds.value--;
updateTimeDisplay();
// Show warnings at specific intervals
if (remainingSeconds.value == 300) {
showTimeWarning("5 minutes remaining before spark auto-ends");
} else if (remainingSeconds.value == 180) {
showTimeWarning("3 minutes remaining before spark auto-ends");
} else if (remainingSeconds.value == 60) {
showTimeWarning("1 minute remaining before spark auto-ends");
} else if (remainingSeconds.value == 30) {
showTimeWarning("30 seconds remaining before spark auto-ends");
}
} else {
timer.cancel();
autoEndStream();
}
});
}
// Stop the stream timer.......................................................................................
void stopStreamTimer() {
_streamTimer?.cancel();
_streamTimer = null;
}
// Update the time display format (MM:SS)......................................................................
void updateTimeDisplay() {
int minutes = remainingSeconds.value ~/ 60;
int seconds = remainingSeconds.value % 60;
timeDisplay.value =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
// Show time warning to users..................................................................................
void showTimeWarning(String message) {
customSnackbar(title: 'Spark Timer', message: message, duration: 3);
// Add system message to chat
addChatMessage(
LiveChatMessage(
userId: "system",
username: "System",
message: "$message",
timestamp: DateTime.now(),
isSystemMessage: true,
),
);
}
// Auto-end stream when timer expires..................................................................................
Future<void> autoEndStream() async {
try {
// Show final warning
customSnackbar(
title: 'Spark Ended',
message: 'Spark automatically ended after 10 minutes',
duration: 5,
);
// Add system message to chat
addChatMessage(
LiveChatMessage(
userId: "system",
username: "System",
message:
"⏰ Spark automatically ended after 10 minutes. Thank you for watching!",
timestamp: DateTime.now(),
isSystemMessage: true,
),
);
// Wait a moment for the message to be sent
await Future.delayed(const Duration(seconds: 2));
// End the stream
await endStream();
// Make API call to end stream on backend
if (!Get.isRegistered<GetLiveStreamsController>()) {
Get.put(GetLiveStreamsController());
}
final apiController = Get.find<GetLiveStreamsController>();
await apiController.endLiveStream(streamID: channel.value);
} catch (e, stackTrace) {
logger.error(
"Failed to end stream or make API call to end stream on backend",
error: e,
stackTrace: stackTrace,
);
}
}
// Initialize Agora Chat SDK-------------------------------------------------------------------------
Future<void> _initializeChatSDK() async {
try {
ChatOptions options = ChatOptions(
appKey: chatAppKey,
deleteMessagesAsExitChatRoom: false,
);
await ChatClient.getInstance.init(options);
await loginChatUser();
_setupChatEventHandlers();
} catch (e) {
Get.snackbar('Chat Error', 'Failed to initialize chat: $e');
}
}
// Chat Login------------------------------------------------------------------------------------------------------------
Future<bool> loginChatUser() async {
try {
final loginDetails = SharedServices.getLoginDetails();
if (loginDetails?.data?.userId == null) {
return false;
}
final tokenSuccess = SharedServices.getAgoraUserAndRtmTokens();
if (tokenSuccess?.data == null ||
tokenSuccess?.data?.agoraUserToken == null) {
return false;
}
final userToken = tokenSuccess!.data!.agoraUserToken.toString();
final userId = loginDetails!.data!.userId.toString();
final agoraUserId = userId.replaceAll("-", "");
await ChatClient.getInstance.loginWithToken(agoraUserId, userToken);
return true;
} on ChatError catch (e) {
return false;
}
}
// Setup chat room event handlers-------------------------------------------------------------------------
void _setupChatEventHandlers() {
ChatClient.getInstance.chatRoomManager.addEventHandler(
"chat_event_handeller",
ChatRoomEventHandler(
// When a new member joins the chat room
onMemberJoinedFromChatRoom: (roomId, participant, ext) async {
await updateMemberCount();
},
// When a member leaves the chat room
onMemberExitedFromChatRoom: (roomId, roomName, participant) async {
await updateMemberCount();
},
// When chat room is destroyed - UPDATED WITH STREAM ENDED MESSAGE
onChatRoomDestroyed: (roomId, roomName) {
addChatMessage(
LiveChatMessage(
userId: "system",
username: "System",
message: "🔴 The Spark has ended. Thank you for watching!",
timestamp: DateTime.now(),
isSystemMessage: true,
),
);
chatRoomJoined.value = false;
customSnackbar(
title: 'Live Stream ended',
message: 'The Spark has ended. Thank you for watching!',
duration: 2,
);
},
// When member count changes
onSpecificationChanged: (room) async {
await updateMemberCount();
},
),
);
// Setup message event handler for receiving messages
ChatClient.getInstance.chatManager.addEventHandler(
"receive_message_handler",
ChatEventHandler(
onMessagesReceived: (messages) {
for (var message in messages) {
if (message.chatType == ChatType.ChatRoom &&
message.conversationId == chatRoomId.value) {
_handleReceivedMessage(message);
}
}
},
),
);
}
// Handle received chat messages-------------------------------------------------------------------------
void _handleReceivedMessage(ChatMessage message) {
String messageText = "";
String senderName = message.attributes!['fullName'] ?? "Unknown";
if (message.body.type == MessageType.TXT) {
ChatTextMessageBody body = message.body as ChatTextMessageBody;
messageText = body.content;
}
addChatMessage(
LiveChatMessage(
userId: message.from ?? "unknown",
username: senderName,
message: messageText,
timestamp: DateTime.fromMillisecondsSinceEpoch(message.serverTime),
isSystemMessage: false,
),
);
}
// Update member count-------------------------------------------------------------------------
Future<void> updateMemberCount() async {
try {
// ChatRoom? room = await ChatClient.getInstance.chatRoomManager
// .getChatRoomWithId(chatRoomId.value);
ChatRoom? room = await ChatClient.getInstance.chatRoomManager
.fetchChatRoomInfoFromServer(chatRoomId.value);
memberCount.value = room.memberCount ?? 0;
} catch (e, stackTrace) {
logger.error(
"Failed to update member count from chat room",
error: e,
stackTrace: stackTrace,
);
}
}
// Initialize the Agora RTC engine instance-------------------------------------------------------------------------
Future<void> _initializeAgoraVideoSDK() async {
try {
_engine = createAgoraRtcEngine();
await _engine.initialize(
const RtcEngineContext(
appId: appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
),
);
_setupEventHandlers();
await _requestPermissions();
_engineNeedsReinit = false;
} catch (e) {
Get.snackbar('Error', 'Failed to initialize Agora SDK: $e');
}
}
// Request microphone and camera permissions-------------------------------------------------------------------------
Future<void> _requestPermissions() async {
if (Platform.isAndroid) {
await [Permission.microphone, Permission.camera].request();
}
}
// Register event handlers for Agora RTC-------------------------------------------------------------------------
void _setupEventHandlers() {
_engine.registerEventHandler(
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) async {
localUserJoined.value = true;
isJoined.value = true;
// Start the 10-minute timer when successfully joined (only for host)
if (isHost.value) {
_startStreamTimer();
}
// Join chat room after successfully joining video channel
await joinChatRoom();
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
this.remoteUid.value = remoteUid;
},
onUserOffline: (
RtcConnection connection,
int remoteUid,
UserOfflineReasonType reason,
) {
this.remoteUid.value = 0;
},
onError: (ErrorCodeType err, String msg) {},
),
);
}
// Setup local video.....................................................................
Future<void> _setupLocalVideo() async {
await _engine.enableVideo();
await _engine.startPreview();
}
// Join channel as host.....................................................................
Future joinAsHost() async {
try {
// Reinitialize engine if needed
if (_engineNeedsReinit) {
await _initializeAgoraVideoSDK();
}
isHost.value = true;
await _setupLocalVideo();
if (!Get.isRegistered<AgoraTokenController>()) {
Get.put(AgoraTokenController());
}
final agoraTokenController = Get.find<AgoraTokenController>();
// Generate a random 32-bit integer
final random = Random();
final tempId = random.nextInt(4294967296); // 2^32
await agoraTokenController
.getRTCtoken(
channelName: channel.value,
role: ApiEnum.SUBSCRIBER,
tempId: tempId.toString(),
)
.then((value) {
if (value) {
String tokenValue = agoraTokenController.rTCtoken.value;
token.value = tokenValue;
}
});
// Enabling the chat..................................................................
chatRoomJoined.value = true;
//......................................................................................
await _engine.joinChannel(
token: token.value,
channelId: channel.value,
options: const ChannelMediaOptions(
autoSubscribeVideo: true,
autoSubscribeAudio: true,
publishCameraTrack: true,
publishMicrophoneTrack: true,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
audienceLatencyLevel:
AudienceLatencyLevelType.audienceLatencyLevelUltraLowLatency,
),
uid: tempId,
);
} catch (e, stackTrace) {
logger.error(
"Failed to join channel as broadcaster",
error: e,
stackTrace: stackTrace,
);
}
}
// Join channel as audience.....................................................................
Future joinAsAudience() async {
// Reinitialize engine if needed
if (_engineNeedsReinit) {
await _initializeAgoraVideoSDK();
}
if (!Get.isRegistered<AgoraTokenController>()) {
Get.put(AgoraTokenController());
}
final agoraTokenController = Get.find<AgoraTokenController>();
// Generate a random 32-bit integer
final random = Random();
final tempId = random.nextInt(4294967296); // 2^32
await agoraTokenController
.getRTCtoken(
channelName: channel.value,
role: ApiEnum.SUBSCRIBER,
tempId: tempId.toString(),
)
.then((value) {
if (value) {
String tokenValue = agoraTokenController.rTCtoken.value;
token.value = tokenValue;
}
});
try {
isHost.value = false;
// Enabling the chat..................................................................
chatRoomJoined.value = true;
//......................................................................................
await _engine.joinChannel(
token: token.value,
channelId: channel.value,
options: const ChannelMediaOptions(
autoSubscribeVideo: true,
autoSubscribeAudio: true,
publishCameraTrack: false,
publishMicrophoneTrack: false,
clientRoleType: ClientRoleType.clientRoleAudience,
audienceLatencyLevel:
AudienceLatencyLevelType.audienceLatencyLevelUltraLowLatency,
),
uid: tempId,
);
} catch (e, stackTrace) {
logger.error(
"Failed to join channel as audience",
error: e,
stackTrace: stackTrace,
);
}
}
// End stream (host only)------------------------------------------------------------------------
Future<void> endStream() async {
try {
// Stop the timer first
stopStreamTimer();
// Step 1: Destroy chat room if host (this will kick out all members)
if (isHost.value && chatRoomJoined.value) {
try {
await ChatClient.getInstance.chatRoomManager.destroyChatRoom(
chatRoomId.value,
);
} catch (e) {
// If destroy fails, at least leave the room
await _leaveChatRoom();
}
} else {
// If not host, just leave the chat room
await _leaveChatRoom();
}
// Step 2: Clean up video engine
await _cleanupAgoraEngine();
// Step 3: Reset all state variables
_resetStreamState();
// Step 4: Navigate back
Get.back();
} catch (e, stackTrace) {
logger.error(
"Failed to end stream and cleanup",
error: e,
stackTrace: stackTrace,
);
}
}
// Leave stream(For audience) --------------------------------------------------------------------------
Future<void> leaveStream() async {
try {
// Stop timer if running (shouldn't be for audience, but just in case)
stopStreamTimer();
// Send system message..............................
await sendJoinedAndLeaveMessage(false);
// Leave chat room first..............................
await _leaveChatRoom();
// Clean up video engine..............................
await _cleanupAgoraEngine();
// Reset state..............................
_resetStreamState();
// Navigate back..............................
Get.back();
} catch (e, stackTrace) {
logger.error(
"Failed to leave stream and cleanup",
error: e,
stackTrace: stackTrace,
);
}
}
// Reset stream state-------------------------------------------------------------------------
void _resetStreamState() {
// Reset observable variables
localUserJoined.value = false;
isJoined.value = false;
remoteUid.value = 0;
chatRoomJoined.value = false;
memberCount.value = 0;
// Reset timer state
remainingSeconds.value = _streamDurationMinutes * 60;
timeDisplay.value =
"${_streamDurationMinutes.toString().padLeft(2, '0')}:00";
// Clear chat messages
chatMessages.clear();
// Clear controllers
commentController.clear();
// Reset tokens and channel info
token.value = "";
// Don't reset channel.value as it might be needed for rejoining
}
// Cleanup Agora engine -------------------------------------------------------------------------
Future<void> _cleanupAgoraEngine() async {
try {
if (isJoined.value) {
await _engine.leaveChannel();
}
await _engine.release();
_engineNeedsReinit = true; // Mark that engine needs reinitialization
} catch (e) {
_engineNeedsReinit = true; // Still mark for reinit even if cleanup failed
}
}
// Complete cleanup - for onClose-------------------------------------------------------------------------
Future<void> _cleanupEverything() async {
try {
// First cleanup chat
await _leaveChatRoom();
// Then cleanup video engine
if (!_engineNeedsReinit) {
await _cleanupAgoraEngine();
}
// Remove all event handlers
ChatClient.getInstance.chatRoomManager.removeEventHandler(
"chat_event_handeller",
);
ChatClient.getInstance.chatManager.removeEventHandler(
"receive_message_handler",
);
} catch (e, stackTrace) {
logger.error(
"Failed to cleanup Agora engine or remove event handlers",
error: e,
stackTrace: stackTrace,
);
}
}
// Join chat room -------------------------------------------------------------------------
Future<void> joinChatRoom() async {
try {
await ChatClient.getInstance.chatRoomManager.joinChatRoom(
chatRoomId.value,
leaveOtherRooms: false,
ext: currentUserId.value,
);
chatRoomJoined.value = true;
await updateMemberCount();
// Add welcome message
addChatMessage(
LiveChatMessage(
userId: "system",
username: "System",
message:
"Welcome to the live chat! Stream will auto-end after 10 minutes.",
timestamp: DateTime.now(),
isSystemMessage: true,
),
);
Future.delayed(Duration(milliseconds: 500), () async {
await sendJoinedAndLeaveMessage(true);
});
} catch (e, stackTrace) {
logger.error(
"Failed to join chat room or send welcome message",
error: e,
stackTrace: stackTrace,
);
}
}
// Leave chat room - IMPROVED VERSION----------------------------------------------------------------------------------------
Future<void> _leaveChatRoom() async {
try {
if (chatRoomJoined.value && chatRoomId.value.isNotEmpty) {
await ChatClient.getInstance.chatRoomManager.leaveChatRoom(
chatRoomId.value,
);
chatRoomJoined.value = false;
}
} catch (e) {
// Don't show error to user as this might happen during normal cleanup
}
}
// Send comment to chat room-------------------------------------------------------------------------
void sendComment() {
if (commentController.text.trim().isNotEmpty && chatRoomJoined.value) {
_sendChatMessage(commentController.text);
commentController.clear();
} else if (!chatRoomJoined.value) {
Get.snackbar('Chat Error', 'Not connected to chat room');
}
}
// Send message to chat room-------------------------------------------------------------------------
Future<void> _sendChatMessage(String messageText) async {
try {
// Create text message
ChatMessage message = ChatMessage.createTxtSendMessage(
targetId: chatRoomId.value,
content: messageText,
chatType: ChatType.ChatRoom,
);
message.attributes = {
"fullName": currentUserName.value,
"profilePicture": currentUserProfilePic.value,
};
// Send message
await ChatClient.getInstance.chatManager.sendMessage(message);
// Add to local chat immediately for better UX
addChatMessage(
LiveChatMessage(
userId: currentUserId.value,
username: 'You',
message: messageText,
timestamp: DateTime.now(),
isSystemMessage: false,
),
);
} catch (e) {
Get.snackbar('Chat Error', 'Failed to send message: $e');
}
}
//.... Show System message When user Join the room and Left the Room................................................
Future<void> sendJoinedAndLeaveMessage(bool isJoined) async {
try {
// Create text message
ChatMessage message = ChatMessage.createTxtSendMessage(
targetId: chatRoomId.value,
content:
isJoined
? "${currentUserName.value} Joined the chat"
: "${currentUserName.value} left the chat",
chatType: ChatType.ChatRoom,
);
message.attributes = {
"fullName": "System",
"profilePicture": "",
"isSystemMessage": true,
};
// Send message
await ChatClient.getInstance.chatManager.sendMessage(message);
} catch (e) {
Get.snackbar('Chat Error', 'Failed to send message: $e');
}
}
// Add chat message-------------------------------------------------------------------------
void addChatMessage(LiveChatMessage message) {
chatMessages.add(message);
// Auto scroll to bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (chatScrollController.hasClients) {
chatScrollController.animateTo(
chatScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
// Toggle chat visibility-------------------------------------------------------------------------
void toggleChat() {
showChat.value = !showChat.value;
}
}
// Live Chat Message Model-------------------------------------------------------------------------
class LiveChatMessage {
final String userId;
final String username;
final String message;
final DateTime timestamp;
final bool isSystemMessage;
LiveChatMessage({
required this.userId,
required this.username,
required this.message,
required this.timestamp,
this.isSystemMessage = false,
});
}