I wanted to make video and non-video calls in my application. There is no problem in video calls, but the microphone does not physically open on both the video and non-video call pages. The green microphone emblem in the notification section on Android 12+ devices does not appear. I’m thinking of making this search system myself using socket_io and flutter_webrtc.What should I do?
CallScreen
import 'package:flutter/material.dart';
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<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> {
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<void> _initCall() async {
await _checkMicPermissionBeforeCall();
await _initRenderers();
await _setupPeerConnection();
debugPrint('✅ PeerConnection & local stream hazır');
socket.emit('join', widget.callerId);
socket.emit('join', widget.receiverId);
_registerSocketHandlers();
if (widget.isCaller) {
debugPrint('🚀 Teklif (offer) oluşturuluyor…');
await _startCall();
}
}
Future<void> _checkMicPermissionBeforeCall() async {
if (!await Permission.microphone.request().isGranted) {
debugPrint('❌ Mikrofon izni aktif değil!');
// Burada kullanıcıyı bilgilendiren bir dialog açabilirsiniz.
} else {
debugPrint('✅ Mikrofon izni aktif');
}
}
Future<void> _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<void> _setupPeerConnection() async {
// 1) Lokal audio stream
_localStream = await navigator.mediaDevices.getUserMedia({
'audio': true,
'video': false,
});
_localRenderer.srcObject = _localStream;
debugPrint('🎤 Lokal stream alındı');
// 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('🔌 PeerConnection oluşturuldu');
// 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('📺 Remote stream geldi');
}
};
// 5) ICE adaylarını signalling server’a gönder
_peerConnection!.onIceCandidate = (RTCIceCandidate c) {
debugPrint('❄️ ICE candidate: ${c.candidate}');
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('📡 connection state: $s');
_peerConnection!.onIceConnectionState = (s) => debugPrint('❄️ ICE state: $s');
_peerConnection!.onSignalingState = (s) => debugPrint('📶 Signaling state: $s');
}
Future<void> _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('📤 Offer emit edildi');
}
Future<void> _onOffer(dynamic payload) async {
if (widget.isCaller) return;
final data = (payload is List && payload.isNotEmpty) ? payload[0] : payload;
debugPrint('📥 Offer alındı: ${data['sdp']}');
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('📤 Answer emit edildi');
}
Future<void> _onAnswer(dynamic payload) async {
if (!widget.isCaller) return;
final data = (payload is List && payload.isNotEmpty) ? payload[0] : payload;
debugPrint('📥 Answer alındı: ${data['sdp']}');
await _peerConnection!.setRemoteDescription(
RTCSessionDescription(data['sdp'], data['type'])
);
}
Future<void> _onAccepted(dynamic _) async {
if (!widget.isCaller) return;
debugPrint('📩 Karşı taraf kabul etti');
}
Future<void> _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('❄️ ICE candidate eklendi');
}
Future<void> _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,
),
),
);
},
SignalingService
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('✅ WebSocket bağlandı: ${_socket.id}');
});
_socket.onDisconnect((_) {
print('🔌 WebSocket bağlantısı kesildi');
});
}
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();
}
}
IncomingCallScreen
import 'package:flutter/material.dart';
import 'package:flutter_application_1/call_screen.dart';
// import 'package:flutter_application_1/utils/audio_service.dart'; // <<< SES KALDIRILDI
import 'package:socket_io_client/socket_io_client.dart' as IO;
class IncomingCallScreen extends StatefulWidget {
final String callerId;
final String callerName;
final String receiverId;
final String receiverName;
final IO.Socket socket;
const IncomingCallScreen({
Key? key,
required this.callerId,
required this.callerName,
required this.receiverId,
required this.receiverName,
required this.socket,
}) : super(key: key);
@override
State<IncomingCallScreen> createState() => _IncomingCallScreenState();
}
class _IncomingCallScreenState extends State<IncomingCallScreen> {
@override
void initState() {
super.initState();
// AudioService.playIncoming(); // <<< SES KALDIRILDI
}
void _acceptCall() {
// AudioService.stopIncoming(); // <<< SES KALDIRILDI
widget.socket.emit('accepted', widget.callerId);
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => CallScreen(
callerId: widget.callerId,
callerName: widget.callerName,
receiverId: widget.receiverId,
receiverName: widget.receiverName,
socket: widget.socket,
isCaller: false,
),
),
);
}
}
void _rejectCall() {
// AudioService.stopIncoming(); // <<< SES KALDIRILDI
widget.socket.emit('cancel-call', widget.callerId);
if (mounted) Navigator.of(context).pop();
}
@override
void dispose() {
// AudioService.stopIncoming(); // <<< SES KALDIRILDI
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black87,
body: SafeArea(
child: Column(
children: [
const Spacer(),
const Icon(Icons.call, size: 100, color: Colors.greenAccent),
const SizedBox(height: 20),
Text(
'${widget.callerName} sizi arıyor...',
style: const TextStyle(color: Colors.white, fontSize: 24),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
icon: const Icon(Icons.call_end),
label: const Text('Reddet'),
onPressed: _rejectCall,
),
ElevatedButton.icon(
style:
ElevatedButton.styleFrom(backgroundColor: Colors.green),
icon: const Icon(Icons.call),
label: const Text('Kabul Et'),
onPressed: _acceptCall,
),
],
),
const SizedBox(height: 40),
],
),
),
);
}
}
MainPageGuide
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:geolocator/geolocator.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_application_1/incoming_call_screen.dart';
import 'package:flutter_application_1/incoming_video_call.dart';
import 'package:flutter_application_1/guide/statistic_page.dart';
import 'package:flutter_application_1/guide/meetings.dart';
import 'package:flutter_application_1/guide/notifications_page.dart';
import 'package:flutter_application_1/guide/profile_page.dart';
import 'package:flutter_application_1/services/guide_service.dart';
class MainPageGuide extends StatefulWidget {
const MainPageGuide({Key? key}) : super(key: key);
@override
State<MainPageGuide> createState() => _MainPageGuideState();
}
class _MainPageGuideState extends State<MainPageGuide>
with WidgetsBindingObserver {
int _currentIndex = 0;
String? _email, _name, _id;
Position? _currentPosition;
Timer? _locationUpdateTimer;
IO.Socket? _socket;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_loadSharedData();
_startLocationUpdates();
}
Future<void> _loadSharedData() async {
final prefs = await SharedPreferences.getInstance();
_email = prefs.getString('user_email');
_name = prefs.getString('user_name');
_id = prefs.getString('user_id');
if (_id != null) _initSocket(_id!);
if (_email != null) await _fetchLocation();
}
void _initSocket(String guideId) {
_socket = IO.io('http://192----', <String, dynamic>{
'transports': ['websocket'],
'autoConnect': true,
});
_socket!.onConnect((_) {
print('📡 Bağlandı (guide $guideId)');
_socket!.emit('join', guideId);
});
// SESLİ çağrı
_socket!.on('incoming-call', (data) {
final callerId = data['callerId'] as String?;
final callerName = data['callerName'] as String?;
if (callerId != null && callerName != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => IncomingCallScreen(
callerId: callerId,
callerName: callerName,
receiverId: guideId,
receiverName: _name ?? '',
socket: _socket!,
),
),
);
}
});
// GÖRÜNTÜLÜ çağrı
_socket!.on('incoming-video-call', (data) {
final callerId = data['callerId'] as String?;
final callerName = data['callerName'] as String?;
if (callerId != null && callerName != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => IncomingVideoCallScreen(
callerId: callerId,
callerName: callerName,
receiverId: guideId,
receiverName: _name ?? '',
socket: _socket!,
),
),
);
}
});
_socket!.onDisconnect((_) => print('🔌 Bağlantı koptu'));
}
void _startLocationUpdates() {
_locationUpdateTimer =
Timer.periodic(const Duration(seconds: 30), (_) => _fetchLocation());
}
Future<void> _fetchLocation() async {
if (_email == null) return;
try {
final pos = await Geolocator.getCurrentPosition();
setState(() => _currentPosition = pos);
await updateGuideLocation(
email: _email!,
latitude: pos.latitude,
longitude: pos.longitude,
isOnline: true,
);
} catch (e) {
print('Konum alınamadı: $e');
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_email == null) return;
final offline = state == AppLifecycleState.paused ||
state == AppLifecycleState.detached;
final online = state == AppLifecycleState.resumed;
if (offline) {
updateGuideLocation(
email: _email!,
latitude: 0,
longitude: 0,
isOnline: false,
);
} else if (online && _currentPosition != null) {
updateGuideLocation(
email: _email!,
latitude: _currentPosition!.latitude,
longitude: _currentPosition!.longitude,
isOnline: true,
);
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_locationUpdateTimer?.cancel();
_socket?.disconnect();
super.dispose();
}
@override
Widget build(BuildContext context) {
final pages = [
const StatisticsPage(),
MeetingsPage(guideId: _id ?? '', guideName: _name ?? ''),
const NotificationsPage(),
const ProfilePage(),
];
return Scaffold(
body: pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
selectedItemColor: Colors.blue,
unselectedItemColor: Colors.grey,
onTap: (i) => setState(() => _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',
),
],
),
);
}
}
android manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.flutter_application_1">
<uses-feature
android:glEsVersion="0x00020000"
android:required="true"/>
<!-- GEREKLİ İZİNLER -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:label="flutter_application_1"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<service
android:name="io.flutter.plugins.webrtc.WebRTCForegroundService"
android:exported="false"
android:foregroundServiceType="microphone|camera"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false"/>
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>