import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:permission_handler/permission_handler.dart';
class CallScreen extends StatefulWidget {
final String callerId;
final String callerName;
final String receiverId;
final String receiverName;
final IO.Socket socket;
final bool isCaller;
const CallScreen({
Key? key,
required this.callerId,
required this.callerName,
required this.receiverId,
required this.receiverName,
required this.socket,
required this.isCaller,
}) : super(key: key);
@override
State createState() => _CallScreenState();
}
class _CallScreenState extends State {
late IO.Socket socket;
RTCPeerConnection? _peerConnection;
MediaStream? _localStream;
MediaStream? _remoteStream;
final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
bool isMicOn = true;
bool isSpeakerOn = true;
@override
void initState() {
super.initState();
socket = widget.socket;
_initCall();
}
Future _initCall() async {
await _checkMicPermissionBeforeCall();
await _initRenderers();
await _setupPeerConnection();
debugPrint('
socket.emit('join', widget.callerId);
socket.emit('join', widget.receiverId);
_registerSocketHandlers();
if (widget.isCaller) {
debugPrint('
await _startCall();
}
}
Future _checkMicPermissionBeforeCall() async {
if (!await Permission.microphone.request().isGranted) {
debugPrint('
// Burada kullanıcıyı bilgilendiren bir dialog açabilirsiniz.
} else {
debugPrint('
}
}
Future _initRenderers() async {
await _localRenderer.initialize();
await _remoteRenderer.initialize();
}
void _registerSocketHandlers() {
socket.on('offer', _onOffer);
socket.on('answer', _onAnswer);
socket.on('accepted', _onAccepted);
socket.on('ice-candidate', _onIceCandidate);
socket.on('call-cancelled', _onCallCancelled);
}
void _unregisterSocketHandlers() {
socket.off('offer', _onOffer);
socket.off('answer', _onAnswer);
socket.off('accepted', _onAccepted);
socket.off('ice-candidate', _onIceCandidate);
socket.off('call-cancelled', _onCallCancelled);
}
Future _setupPeerConnection() async {
// 1) Lokal audio stream
_localStream = await navigator.mediaDevices.getUserMedia({
'audio': true,
'video': false,
});
_localRenderer.srcObject = _localStream;
debugPrint('
// 2) PeerConnection oluştur
final config = {
'iceServers': [
{'urls': 'stun:stun.l.google.com:19302'},
{
'urls': 'turn:openrelay.metered.ca:80',
'username': 'openrelayproject',
'credential': 'openrelayproject'
},
]
};
_peerConnection = await createPeerConnection(config);
debugPrint('
// 3) Track ekle
for (var track in _localStream!.getTracks()) {
await _peerConnection!.addTrack(track, _localStream!);
}
// 4) Remote track geldiğinde renderer’a ata
_peerConnection!.onTrack = (RTCTrackEvent event) {
if (event.streams.isNotEmpty) {
setState(() {
_remoteStream = event.streams[0];
_remoteRenderer.srcObject = _remoteStream;
});
debugPrint('
}
};
// 5) ICE adaylarını signalling server’a gönder
_peerConnection!.onIceCandidate = (RTCIceCandidate c) {
debugPrint('
socket.emit('ice-candidate', {
'roomId': widget.isCaller ? widget.receiverId : widget.callerId,
'candidate': {
'candidate': c.candidate,
'sdpMid': c.sdpMid,
'sdpMLineIndex': c.sdpMLineIndex,
},
});
};
// Opsiyonel loglar
_peerConnection!.onConnectionState = (s) => debugPrint('
_peerConnection!.onIceConnectionState = (s) => debugPrint('
_peerConnection!.onSignalingState = (s) => debugPrint('
}
Future _startCall() async {
final offer = await _peerConnection!.createOffer();
await _peerConnection!.setLocalDescription(offer);
socket.emit('offer', {
'roomId': widget.receiverId,
'offer': {'sdp': offer.sdp, 'type': offer.type},
'callerId': widget.callerId,
'callerName': widget.callerName,
});
debugPrint('
}
Future _onOffer(dynamic payload) async {
if (widget.isCaller) return;
final data = (payload is List && payload.isNotEmpty) ? payload[0] : payload;
debugPrint('
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data['sdp'], data['type'])
);
final answer = await _peerConnection!.createAnswer();
await _peerConnection!.setLocalDescription(answer);
socket.emit('answer', {
'roomId': widget.callerId,
'answer': {'sdp': answer.sdp, 'type': answer.type},
});
socket.emit('accepted', widget.callerId);
debugPrint('
}
Future _onAnswer(dynamic payload) async {
if (!widget.isCaller) return;
final data = (payload is List && payload.isNotEmpty) ? payload[0] : payload;
debugPrint('
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data['sdp'], data['type'])
);
}
Future _onAccepted(dynamic _) async {
if (!widget.isCaller) return;
debugPrint('
}
Future _onIceCandidate(dynamic payload) async {
final data = (payload is List && payload.isNotEmpty) ? payload[0] : payload;
final cand = RTCIceCandidate(
data['candidate'], data['sdpMid'], data['sdpMLineIndex']
);
await _peerConnection?.addCandidate(cand);
debugPrint('
}
Future _onCallCancelled(dynamic _) async {
// Çağrı iptal edildi, temizleyip başa dön
_cleanupAndExit();
}
void _endCall() {
socket.emit(
'cancel-call',
widget.isCaller ? widget.receiverId : widget.callerId,
);
_cleanupAndExit();
}
void _cleanupAndExit() {
// 1) Stream ve PeerConnection’ı durdur / kapat
_localStream?.getTracks().forEach((t) => t.stop());
_peerConnection?.close();
// 2) Navigation: en baştaki route’a dön
if (mounted) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
@override
void dispose() {
_unregisterSocketHandlers();
_localStream?.dispose();
_remoteStream?.dispose();
_peerConnection?.dispose();
_localRenderer.dispose();
_remoteRenderer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final otherName = widget.isCaller ? widget.receiverName : widget.callerName;
return Scaffold(
backgroundColor: Colors.black87,
body: SafeArea(
child: Column(
children: [
SizedBox(height: 10),
Expanded(child: RTCVideoView(_remoteRenderer)),
SizedBox(height: 10),
SizedBox(
height: 120,
child: RTCVideoView(_localRenderer, mirror: true),
),
SizedBox(height: 20),
Text(
'$otherName ile görüşülüyor...',
style: TextStyle(color: Colors.white, fontSize: 20),
),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Mikrofon toggle
IconButton(
icon: Icon(
isMicOn ? Icons.mic : Icons.mic_off,
color: Colors.white,
),
onPressed: () {
setState(() => isMicOn = !isMicOn);
final audioTrack = _localStream?.getAudioTracks().first;
if (audioTrack != null) {
audioTrack.enabled = isMicOn;
debugPrint(
'🎙 Mikrofon şimdi ${isMicOn ? "açık" : "kapalı"}'
);
}
},
),
// Çağrıyı bitir
IconButton(
icon: Icon(Icons.call_end, color: Colors.red),
onPressed: _endCall,
),
// Hoparlör toggle
IconButton(
icon: Icon(
isSpeakerOn ? Icons.volume_up : Icons.hearing,
color: Colors.white,
),
onPressed: () {
setState(() => isSpeakerOn = !isSpeakerOn);
Helper.setSpeakerphoneOn(isSpeakerOn);
},
),
],
),
SizedBox(height: 40),
],
),
),
);
}
} ElevatedButton.icon(
icon: const Icon(Icons.call),
label: const Text('Ara'),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
final vid = prefs.getString('user_id') ?? '';
final vname = prefs.getString('user_name') ?? '';
if (vid.isEmpty || vname.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Kullanıcı bilgileri eksik. Giriş yapın.'),
),
);
return;
}
final socket = IO.io(
'http://192-----',
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.build(),
);
socket.connect();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CallScreen(
callerId: vid,
callerName: vname,
receiverId: _selectedGuide!['_id'] as String,
receiverName: _selectedGuide!['name'] as String,
socket: socket,
isCaller: true,
),
),
);
},import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_application_1/services/api.dart';
class SignalingService {
late IO.Socket _socket;
void connect() {
_socket = IO.io(
Api.socketUrl,
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.build(),
);
_socket.connect();
_socket.onConnect((_) {
print('
});
_socket.onDisconnect((_) {
print('
});
}
void joinRoom(String roomId) {
_socket.emit('join', roomId);
}
void sendOffer(String roomId, dynamic offer) {
_socket.emit('offer', {'roomId': roomId, 'offer': offer});
}
void sendAnswer(String roomId, dynamic answer) {
_socket.emit('answer', {'roomId': roomId, 'answer': answer});
}
void sendCandidate(String roomId, dynamic candidate) {
_socket.emit('ice-candidate', {'roomId': roomId, 'candidate': candidate});
}
void onOffer(Function(dynamic offer) callback) {
_socket.on('offer', callback);
}
void onAnswer(Function(dynamic answer) callback) {
_socket.on('answer', callback);
}
void onCandidate(Function(dynamic candidate) callback) {
_socket.on('ice-candidate', callback);
}
void disconnect() {
_socket.disconnect();
}
}
import 'package:flutter/material.dart';
import 'package:flutter_application_1/call_screen.dart';
// import 'package:flutter_application_1/utils/audio_service.dart'; // _currentIndex = i),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.bar_chart),
label: 'İstatistiklerim',
),
BottomNavigationBarItem(
icon: Icon(Icons.chat),
label: 'Görüşmelerim',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
label: 'Bildirimler',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profilim',
),
],
),
);
}
}
< /code>
androidmanifest.xml
Подробнее здесь: https://stackoverflow.com/questions/796 ... microphone
Мобильная версия