..
This commit is contained in:
@@ -3,121 +3,180 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bonsoir/bonsoir.dart'; // mDNS 패키지
|
||||
import 'package:bonsoir/bonsoir.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../model/user_info.dart';
|
||||
import '../model/play_packet.dart';
|
||||
import '../manager/global_chat_manager.dart';
|
||||
import '../manager/media_manager.dart'; // [New] 미디어 매니저
|
||||
|
||||
/// 현재 네트워크 상태 (역할)
|
||||
enum NetworkRole { none, host, guest }
|
||||
|
||||
/// P2P 네트워크 통신을 담당하는 싱글톤 매니저
|
||||
class NetworkManager extends ChangeNotifier {
|
||||
// ------------------------------------------------------------------------
|
||||
// 1. Singleton & Initialization
|
||||
// ------------------------------------------------------------------------
|
||||
class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
|
||||
static final NetworkManager _instance = NetworkManager._internal();
|
||||
factory NetworkManager() => _instance;
|
||||
NetworkManager._internal();
|
||||
|
||||
/// 내 정보 (앱 시작 시 initialize 호출 필수)
|
||||
late UserInfo me;
|
||||
|
||||
/// 현재 내 역할
|
||||
NetworkRole role = NetworkRole.none;
|
||||
|
||||
/// 초기화 메서드 (닉네임 설정 및 ID 생성)
|
||||
void initialize({required String nickname}) {
|
||||
// 8자리 랜덤 ID 생성
|
||||
final uuid = const Uuid().v4().substring(0, 8);
|
||||
// 랜덤 컬러 (간단하게 해시코드로 생성 예시)
|
||||
final randomColor = 0xFF000000 | (nickname.hashCode & 0xFFFFFF);
|
||||
|
||||
me = UserInfo(
|
||||
id: uuid,
|
||||
nickname: nickname,
|
||||
colorValue: randomColor,
|
||||
);
|
||||
print('[Network] Initialized User: ${me.nickname} (${me.id})');
|
||||
NetworkManager._internal() {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 2. Variables & Streams
|
||||
// 상태 변수
|
||||
// ------------------------------------------------------------------------
|
||||
late UserInfo me;
|
||||
NetworkRole role = NetworkRole.none;
|
||||
|
||||
String? hostIp;
|
||||
int? hostPort;
|
||||
|
||||
ServerSocket? _serverSocket;
|
||||
Socket? _clientSocket;
|
||||
|
||||
// 소켓
|
||||
ServerSocket? _serverSocket; // (Host용)
|
||||
Socket? _clientSocket; // (Guest용)
|
||||
final List<Socket> _connectedGuests = []; // (Host가 관리하는 게스트 목록)
|
||||
// 소켓과 유저 정보를 1:1 매핑
|
||||
final Map<Socket, UserInfo?> _connectedGuests = {};
|
||||
|
||||
// UI 표시용 게스트 명단
|
||||
final List<UserInfo> guestList = [];
|
||||
|
||||
// mDNS (방 찾기/만들기)
|
||||
BonsoirService? _bonsoirService;
|
||||
BonsoirBroadcast? _bonsoirBroadcast;
|
||||
BonsoirDiscovery? _bonsoirDiscovery;
|
||||
|
||||
// 수신된 데이터를 앱(GameManager)으로 전달하는 스트림
|
||||
// 게임 데이터 스트림
|
||||
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 3. Host Logic (방장)
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// 방 만들기
|
||||
Future<void> startHosting(String roomName) async {
|
||||
stopNetwork(); // 기존 연결 정리
|
||||
role = NetworkRole.host;
|
||||
// 로그 스트림
|
||||
final _logController = StreamController<String>.broadcast();
|
||||
Stream<String> get logStream => _logController.stream;
|
||||
|
||||
try {
|
||||
// A. TCP 서버 소켓 오픈 (Port 0 = 시스템 자동 할당)
|
||||
_serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
int port = _serverSocket!.port;
|
||||
print('[Host] Server opened on port: $port');
|
||||
// 하트비트 & 재접속
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _disconnectWaitTimer;
|
||||
DateTime? _lastPongTime;
|
||||
bool _isReconnecting = false;
|
||||
|
||||
final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4);
|
||||
String myIp = '127.0.0.1';
|
||||
try {
|
||||
// 보통 wlan0 혹은 en0가 와이파이 인터페이스
|
||||
myIp = interfaces.firstWhere((i) => i.name != 'lo').addresses.first.address;
|
||||
} catch (e) {
|
||||
print('IP search failed: $e');
|
||||
static const int HEARTBEAT_INTERVAL_SEC = 3;
|
||||
static const int TIMEOUT_SEC = 10;
|
||||
static const int RECONNECT_WAIT_SEC = 5;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 초기화
|
||||
// ------------------------------------------------------------------------
|
||||
void initialize({required String nickname}) {
|
||||
final uuid = const Uuid().v4().substring(0, 8);
|
||||
final randomColor = 0xFF000000 | (nickname.hashCode & 0xFFFFFF);
|
||||
me = UserInfo(id: uuid, nickname: nickname, colorValue: randomColor);
|
||||
_log("초기화 완료: ${me.nickname}");
|
||||
}
|
||||
|
||||
// B. 게스트 접속 대기
|
||||
void _log(String msg) {
|
||||
final timestamp = DateTime.now().toIso8601String().split('T').last.substring(0, 8);
|
||||
print("[$timestamp] $msg");
|
||||
_logController.add("[$timestamp] $msg");
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (role == NetworkRole.guest && _clientSocket == null && hostIp != null) {
|
||||
_attemptReconnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 레디 시스템
|
||||
// ------------------------------------------------------------------------
|
||||
void toggleReady() {
|
||||
me = me.copyWith(isReady: !me.isReady);
|
||||
notifyListeners();
|
||||
|
||||
final payload = {
|
||||
'type': 'TOGGLE_READY',
|
||||
'userId': me.id,
|
||||
'isReady': me.isReady,
|
||||
};
|
||||
sendMessage(payload);
|
||||
|
||||
if (role == NetworkRole.host) {
|
||||
_checkAllReadyAndStart();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkAllReadyAndStart() {
|
||||
if (guestList.isEmpty) return;
|
||||
if (!me.isReady) return;
|
||||
|
||||
bool allGuestsReady = guestList.every((u) => u.isReady);
|
||||
|
||||
if (allGuestsReady) {
|
||||
_log("🚀 전원 준비 완료! 3초 후 게임 시작...");
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
final startPayload = {'type': 'GAME_START', 'gameId': 'quiz_ox'};
|
||||
sendMessage(startPayload);
|
||||
_messageController.add(startPayload);
|
||||
_resetAllReadyState();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _resetAllReadyState() {
|
||||
me = me.copyWith(isReady: false);
|
||||
for (int i = 0; i < guestList.length; i++) {
|
||||
guestList[i] = guestList[i].copyWith(isReady: false);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Host Logic
|
||||
// ------------------------------------------------------------------------
|
||||
Future<void> startHosting(String roomName) async {
|
||||
stopNetwork(force: true);
|
||||
role = NetworkRole.host;
|
||||
|
||||
try {
|
||||
_serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
int port = _serverSocket!.port;
|
||||
this.hostPort = port;
|
||||
|
||||
String? myIp = await _getWifiIp();
|
||||
this.hostIp = myIp ?? '127.0.0.1';
|
||||
_log("✅ 방 생성: $hostIp : $port");
|
||||
|
||||
_serverSocket!.listen((Socket client) {
|
||||
_handleNewGuest(client);
|
||||
});
|
||||
|
||||
// C. mDNS로 방 광고 (Broadcast)
|
||||
// 서비스 타입은 고유해야 함 (_playwith._tcp)
|
||||
// 이름 포맷: "방이름#호스트ID" (중복 방지 및 식별용)
|
||||
_bonsoirService = BonsoirService(
|
||||
name: '$roomName#${me.id}',
|
||||
name: '$roomName#${me.id}',
|
||||
type: '_playwith._tcp',
|
||||
port: port,
|
||||
attributes: {'ip': myIp},
|
||||
attributes: {'ip': hostIp!},
|
||||
);
|
||||
|
||||
_bonsoirBroadcast = BonsoirBroadcast(service: _bonsoirService!);
|
||||
await _bonsoirBroadcast!.ready;
|
||||
await _bonsoirBroadcast!.start();
|
||||
|
||||
print('[Host] Start advertising room: $roomName');
|
||||
// [NEW] 미디어 DB 초기화 (Host)
|
||||
await MediaManager().initialize(roomName);
|
||||
|
||||
_startHeartbeat();
|
||||
notifyListeners();
|
||||
|
||||
} catch (e) {
|
||||
print('[Host] Error starting host: $e');
|
||||
stopNetwork();
|
||||
_log("❌ 방 생성 실패: $e");
|
||||
stopNetwork(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 새로운 게스트가 접속했을 때
|
||||
void _handleNewGuest(Socket client) {
|
||||
print('[Host] New guest connected: ${client.remoteAddress.address}');
|
||||
_connectedGuests.add(client);
|
||||
|
||||
// 데이터 수신 리스너 부착
|
||||
_log("🎉 연결됨: ${client.remoteAddress.address}");
|
||||
_connectedGuests[client] = null;
|
||||
|
||||
client.listen(
|
||||
(Uint8List data) => _onDataReceived(client, data),
|
||||
onError: (e) => _removeGuest(client),
|
||||
@@ -126,163 +185,289 @@ final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4);
|
||||
}
|
||||
|
||||
void _removeGuest(Socket client) {
|
||||
print('[Host] Guest disconnected');
|
||||
final UserInfo? user = _connectedGuests[client];
|
||||
if (user != null) {
|
||||
_log("👋 퇴장: ${user.nickname}");
|
||||
guestList.removeWhere((u) => u.id == user.id);
|
||||
}
|
||||
_connectedGuests.remove(client);
|
||||
client.close();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 4. Guest Logic (참가자)
|
||||
// Guest Logic
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// 주변 방 찾기 (mDNS Discovery)
|
||||
Stream<List<BonsoirService>> discoverRooms() {
|
||||
// 리스트를 계속 갱신해서 내보내기 위한 컨트롤러
|
||||
final controller = StreamController<List<BonsoirService>>();
|
||||
final List<BonsoirService> foundServices = [];
|
||||
_bonsoirDiscovery?.stop();
|
||||
|
||||
_bonsoirDiscovery = BonsoirDiscovery(type: '_playwith._tcp');
|
||||
|
||||
_bonsoirDiscovery!.ready.then((_) {
|
||||
_bonsoirDiscovery!.start();
|
||||
|
||||
_bonsoirDiscovery!.eventStream!.listen((event) {
|
||||
if (event.type == BonsoirDiscoveryEventType.discoveryServiceFound) {
|
||||
if (event.service != null) {
|
||||
foundServices.add(event.service!);
|
||||
controller.add(List.from(foundServices));
|
||||
}
|
||||
} else if (event.type == BonsoirDiscoveryEventType.discoveryServiceLost) {
|
||||
if (event.service != null) {
|
||||
foundServices.removeWhere((s) => s.name == event.service!.name);
|
||||
controller.add(List.from(foundServices));
|
||||
}
|
||||
Future(() async {
|
||||
try {
|
||||
_bonsoirDiscovery = BonsoirDiscovery(type: '_playwith._tcp');
|
||||
await _bonsoirDiscovery!.start();
|
||||
|
||||
if (_bonsoirDiscovery?.eventStream != null) {
|
||||
_bonsoirDiscovery!.eventStream!.listen((dynamic event) {
|
||||
final String type = event.type.toString();
|
||||
if (event.service == null) return;
|
||||
|
||||
if (type.contains('Found')) {
|
||||
foundServices.removeWhere((s) => s.name == event.service!.name);
|
||||
foundServices.add(event.service!);
|
||||
controller.add(List.from(foundServices));
|
||||
} else if (type.contains('Lost')) {
|
||||
foundServices.removeWhere((s) => s.name == event.service!.name);
|
||||
controller.add(List.from(foundServices));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
_log("스캔 실패: $e");
|
||||
}
|
||||
});
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
/// 방 접속하기
|
||||
Future<void> joinRoom(String ip, int port) async {
|
||||
stopNetwork(); // 기존 연결 정리
|
||||
if (role != NetworkRole.guest) stopNetwork(force: true);
|
||||
role = NetworkRole.guest;
|
||||
hostIp = ip;
|
||||
hostPort = port;
|
||||
|
||||
try {
|
||||
print('[Guest] Connecting to $ip:$port...');
|
||||
_clientSocket = await Socket.connect(ip, port);
|
||||
print('[Guest] Connected!');
|
||||
_log("🚀 접속 시도: $ip:$port");
|
||||
_clientSocket = await Socket.connect(ip, port, timeout: const Duration(seconds: 5));
|
||||
_log("✅ 접속 성공!");
|
||||
|
||||
// 접속 성공 시 즉시 내 정보 전송 (Handshake)
|
||||
sendMessage({
|
||||
'type': 'HANDSHAKE',
|
||||
'senderId': me.id,
|
||||
'payload': me.toJson(),
|
||||
});
|
||||
sendMessage({'type': 'HANDSHAKE', 'payload': me.toJson()});
|
||||
|
||||
// [NEW] 미디어 DB 초기화 (Guest는 임시 ID 사용)
|
||||
await MediaManager().initialize("guest_${ip.replaceAll('.', '_')}");
|
||||
|
||||
_lastPongTime = DateTime.now();
|
||||
_startHeartbeat();
|
||||
_cancelDisconnectTimer();
|
||||
|
||||
// 데이터 수신 리스너
|
||||
_clientSocket!.listen(
|
||||
(Uint8List data) => _onDataReceived(_clientSocket!, data),
|
||||
onError: (e) {
|
||||
print('[Guest] Connection error: $e');
|
||||
stopNetwork();
|
||||
},
|
||||
onDone: () {
|
||||
print('[Guest] Disconnected by host');
|
||||
stopNetwork();
|
||||
},
|
||||
onError: (e) => _handleConnectionLost(e),
|
||||
onDone: () => _handleConnectionLost("Socket Closed"),
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
|
||||
} catch (e) {
|
||||
print('[Guest] Failed to join: $e');
|
||||
role = NetworkRole.none;
|
||||
notifyListeners();
|
||||
rethrow; // UI에서 에러 처리할 수 있게 던짐
|
||||
_log("❌ 접속 실패: $e");
|
||||
if (!_isReconnecting) stopNetwork(force: true);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 5. Common Logic (데이터 송수신)
|
||||
// 데이터 송수신 & 라우팅 (핵심)
|
||||
// ------------------------------------------------------------------------
|
||||
void sendPacket(PlayPacket packet) {
|
||||
sendMessage(packet.toJson());
|
||||
}
|
||||
|
||||
/// 메시지 전송
|
||||
void sendMessage(Map<String, dynamic> messageMap) {
|
||||
try {
|
||||
// JSON 변환
|
||||
final jsonString = jsonEncode(messageMap);
|
||||
// 패킷 경계 처리를 위해 끝에 줄바꿈(\n) 추가 (가장 간단한 delimiter)
|
||||
final List<int> data = utf8.encode('$jsonString\n');
|
||||
if (role == NetworkRole.guest && _clientSocket == null) return;
|
||||
|
||||
if (role == NetworkRole.host) {
|
||||
// Host는 모든 Guest에게 브로드캐스트
|
||||
for (var socket in _connectedGuests) {
|
||||
socket.add(data);
|
||||
}
|
||||
} else if (role == NetworkRole.guest) {
|
||||
// Guest는 Host에게 전송
|
||||
_clientSocket?.add(data);
|
||||
}
|
||||
} catch (e) {
|
||||
print('[Network] Send Error: $e');
|
||||
}
|
||||
final jsonString = jsonEncode(messageMap);
|
||||
// 로그 필터링
|
||||
if (messageMap['type'] != 'PING' && messageMap['type'] != 'PONG') {
|
||||
if (messageMap['type'] == 'chat') {
|
||||
_log("📤 전송: [CHAT]");
|
||||
} else if (messageMap['type'] == 'media') {
|
||||
_log("📤 전송: [MEDIA]");
|
||||
} else {
|
||||
_log("📤 전송: $jsonString");
|
||||
}
|
||||
}
|
||||
|
||||
final List<int> data = utf8.encode('$jsonString\n');
|
||||
if (role == NetworkRole.host) {
|
||||
for (var socket in _connectedGuests.keys) {
|
||||
socket.add(data);
|
||||
}
|
||||
} else {
|
||||
_clientSocket?.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 수신 처리
|
||||
void _onDataReceived(Socket socket, Uint8List data) {
|
||||
// 들어온 데이터를 String으로 변환
|
||||
final String rawString = utf8.decode(data);
|
||||
|
||||
// TCP 패킷이 뭉쳐서 올 수 있으므로 \n으로 쪼갬
|
||||
final List<String> splitMessages = rawString.split('\n');
|
||||
|
||||
|
||||
for (var msg in splitMessages) {
|
||||
if (msg.trim().isEmpty) continue;
|
||||
|
||||
|
||||
try {
|
||||
final Map<String, dynamic> parsedData = jsonDecode(msg);
|
||||
|
||||
// 1. 앱 로직으로 전달
|
||||
_messageController.add(parsedData);
|
||||
final Map<String, dynamic> jsonMap = jsonDecode(msg);
|
||||
|
||||
// 2. (옵션) Host라면, 받은 메시지를 다른 Guest들에게도 전달(Relay)해야 할 수 있음
|
||||
// 게임 로직에 따라 다르지만 보통 Host가 중계자 역할을 함
|
||||
// if (role == NetworkRole.host) { sendMessage(parsedData); }
|
||||
// 1. 시스템 메시지 (Ping/Pong)
|
||||
if (jsonMap['type'] == 'PING') {
|
||||
sendMessage({'type': 'PONG'});
|
||||
_lastPongTime = DateTime.now();
|
||||
return;
|
||||
}
|
||||
if (jsonMap['type'] == 'PONG') {
|
||||
_lastPongTime = DateTime.now();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 핸드셰이크
|
||||
if (jsonMap['type'] == 'HANDSHAKE') {
|
||||
final guestInfo = UserInfo.fromJson(jsonMap['payload']);
|
||||
_connectedGuests[socket] = guestInfo;
|
||||
guestList.removeWhere((u) => u.id == guestInfo.id);
|
||||
guestList.add(guestInfo);
|
||||
notifyListeners();
|
||||
_messageController.add(jsonMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 레디 토글
|
||||
if (jsonMap['type'] == 'TOGGLE_READY') {
|
||||
final String userId = jsonMap['userId'];
|
||||
final bool isReady = jsonMap['isReady'];
|
||||
|
||||
final index = guestList.indexWhere((u) => u.id == userId);
|
||||
if (index != -1) {
|
||||
guestList[index] = guestList[index].copyWith(isReady: isReady);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (role == NetworkRole.host) {
|
||||
sendMessage(jsonMap);
|
||||
_checkAllReadyAndStart();
|
||||
}
|
||||
_messageController.add(jsonMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 게임 시작
|
||||
if (jsonMap['type'] == 'GAME_START') {
|
||||
_resetAllReadyState();
|
||||
_messageController.add(jsonMap);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 패킷 라우팅 (Chat, Media, Game)
|
||||
if (jsonMap.containsKey('payload') && jsonMap.containsKey('senderId')) {
|
||||
final packet = PlayPacket.fromJson(jsonMap);
|
||||
|
||||
// [라우팅] 채팅 -> GlobalChatManager
|
||||
if (packet.type == PacketType.chat) {
|
||||
GlobalChatManager().onPacketReceived(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
// [라우팅] 미디어 -> MediaManager
|
||||
if (packet.type == PacketType.media) {
|
||||
MediaManager().onMediaReceived(packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 그 외 게임 데이터
|
||||
_messageController.add(jsonMap);
|
||||
|
||||
} catch (e) {
|
||||
print('[Network] Parse Error: $e\nMessage: $msg');
|
||||
_log("파싱 에러: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 6. Cleanup
|
||||
// 연결 관리
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// 네트워크 종료 및 리소스 정리
|
||||
void stopNetwork() {
|
||||
print('[Network] Stopping network...');
|
||||
void _handleConnectionLost(dynamic reason) {
|
||||
if (role != NetworkRole.guest) return;
|
||||
_log("⚠️ 연결 끊김: $reason");
|
||||
|
||||
// mDNS 중지
|
||||
_clientSocket?.destroy();
|
||||
_clientSocket = null;
|
||||
|
||||
if (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) return;
|
||||
|
||||
_disconnectWaitTimer = Timer(const Duration(seconds: RECONNECT_WAIT_SEC), () {
|
||||
_log("💀 복구 실패. 종료.");
|
||||
stopNetwork(force: true);
|
||||
});
|
||||
_attemptReconnection();
|
||||
}
|
||||
|
||||
Future<void> _attemptReconnection() async {
|
||||
if (hostIp == null || hostPort == null) return;
|
||||
_isReconnecting = true;
|
||||
while (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) {
|
||||
try {
|
||||
await joinRoom(hostIp!, hostPort!);
|
||||
_log("✅ 재접속 성공!");
|
||||
_isReconnecting = false;
|
||||
return;
|
||||
} catch (e) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
_isReconnecting = false;
|
||||
}
|
||||
|
||||
void _cancelDisconnectTimer() {
|
||||
_disconnectWaitTimer?.cancel();
|
||||
_disconnectWaitTimer = null;
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: HEARTBEAT_INTERVAL_SEC), (timer) {
|
||||
try { sendMessage({'type': 'PING'}); } catch (_) {}
|
||||
if (role == NetworkRole.guest && _lastPongTime != null) {
|
||||
if (DateTime.now().difference(_lastPongTime!).inSeconds > TIMEOUT_SEC) {
|
||||
_handleConnectionLost("Heartbeat Timeout");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> _getWifiIp() async {
|
||||
try {
|
||||
for (var interface in await NetworkInterface.list()) {
|
||||
if (interface.name.contains('wlan') || interface.name.contains('en') || interface.name.contains('ap')) {
|
||||
for (var addr in interface.addresses) {
|
||||
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
|
||||
return addr.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {/**/}
|
||||
return null;
|
||||
}
|
||||
|
||||
void stopNetwork({bool force = false}) {
|
||||
if (!force && _disconnectWaitTimer != null) return;
|
||||
|
||||
_log("🛑 종료");
|
||||
// [NEW] 미디어 DB 정리
|
||||
MediaManager().cleanup();
|
||||
|
||||
_heartbeatTimer?.cancel();
|
||||
_disconnectWaitTimer?.cancel();
|
||||
_disconnectWaitTimer = null;
|
||||
_bonsoirBroadcast?.stop();
|
||||
_bonsoirDiscovery?.stop();
|
||||
|
||||
// 소켓 닫기
|
||||
_serverSocket?.close();
|
||||
_clientSocket?.close();
|
||||
|
||||
for (var socket in _connectedGuests) {
|
||||
socket.close();
|
||||
}
|
||||
for (var s in _connectedGuests.keys) s.close();
|
||||
_connectedGuests.clear();
|
||||
|
||||
guestList.clear();
|
||||
role = NetworkRole.none;
|
||||
_serverSocket = null;
|
||||
_clientSocket = null;
|
||||
|
||||
if (force) { hostIp = null; hostPort = null; }
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user