startScreenCapture method
Future<void>
startScreenCapture(
)
Implementation
Future<void> startScreenCapture() async {
try {
// Marcar actividad (inicio explícito)
_markActivity();
// Verificar si ya está corriendo el servicio
final isRunning = await NativeBridge.isScreenCaptureRunning();
if (!isRunning) {
// Solicitar permiso de MediaProjection si no está activo
final granted = await NativeBridge.requestMediaProjection();
if (!granted) {
log('❌ Permiso de captura de pantalla denegado');
return;
}
// Esperar un momento para que el servicio inicie
await Future.delayed(const Duration(milliseconds: 500));
}
// Iniciar servicio foreground
final started = await NativeBridge.startScreenCapture();
if (!started) {
log('❌ No se pudo iniciar el servicio de captura');
return;
}
log('✅ Servicio de captura iniciado');
// 1. Crear PeerConnection PRIMERO con TURN server propio
if (_peerConnection == null) {
await testTurnConnectivity(turnServerIP ?? '', turnServerPort ?? 0);
_peerConnection = await createPeerConnection(
{
'iceServers': [
if (turnServerIP != null && turnServerPort != null)
{
'urls': 'turn:$turnServerIP:$turnServerPort',
if (turnServerUsername != null) 'username': turnServerUsername,
if (turnServerCredential != null) 'credential': turnServerCredential,
},
// TURNS (TLS) - opcional para certificados SSL
// {
// 'urls': 'turns:192.168.28.132:5349',
// 'username': 'remotecontrol',
// 'credential': 'remotecontrol123',
// },
// STUN servers públicos (gratuitos)
{'urls': 'stun:stun.l.google.com:19302'},
{'urls': 'stun:stun1.l.google.com:19302'},
],
'iceTransportPolicy': 'all', // Permite STUN y TURN
// 'iceTransportPolicy': 'relay', // Forzar uso de TURN para debugging
'bundlePolicy': 'max-bundle',
'rtcpMuxPolicy': 'require',
'sdpSemantics': 'unified-plan',
},
{
'mandatory': {},
'optional': [
{'DtlsSrtpKeyAgreement': true},
],
},
);
// Configurar handlers de ICE candidates
_peerConnection!.onIceCandidate = (candidate) {
if (candidate.candidate == null || candidate.candidate!.isEmpty) {
log('🧊 ICE: Candidato vacío (fin de gathering)');
return;
}
// Marcar actividad cuando lleguen candidatos
_markActivity();
final candStr = candidate.candidate!;
// 🔍 Detectar tipo de candidato
String icon = '⚪';
String type = 'UNKNOWN';
if (candStr.contains('typ relay')) {
icon = '🔵';
type = 'TURN (relay)';
// Extraer IP del relay
final relayMatch = RegExp(r'raddr (\S+)').firstMatch(candStr);
if (relayMatch != null) {
// se puede usar relayMatch.group(1) para debug si se necesita
}
} else if (candStr.contains('typ srflx')) {
icon = '🟡';
type = 'STUN (srflx)';
} else if (candStr.contains('typ host')) {
icon = '🟢';
type = 'HOST (local)';
}
// Log completo para debugging
final display = candStr.length > 80 ? '${candStr.substring(0, 80)}...' : candStr;
log('$icon ICE [$type]: $display');
_sendMessage({'type': 'ice-candidate', 'candidate': candidate.toMap()});
};
// Configurar handler de estado de conexión
_peerConnection!.onConnectionState = (state) {
log('🔗 Estado de conexión WebRTC: $state');
if (state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) {
log('❌ Conexión WebRTC falló - intentando limpiar recursos');
_handleConnectionFailure();
} else if (state == RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
log('✅ Conexión WebRTC establecida exitosamente');
_markActivity();
}
};
// Handler de estado de ICE
_peerConnection!.onIceConnectionState = (state) {
log('🧊 Estado ICE: $state');
if (state == RTCIceConnectionState.RTCIceConnectionStateFailed || state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
log('⚠️ ICE connection issue: $state');
}
};
// Handler de recolección de ICE
_peerConnection!.onIceGatheringState = (state) {
log('🧊 Estado de recolección ICE: $state');
_markActivity();
};
log('✅ PeerConnection creado con configuración mejorada');
}
// 2. Capturar pantalla para WebRTC
_localStream = await navigator.mediaDevices.getDisplayMedia({
'video': {'width': 1280, 'height': 720, 'frameRate': 30},
'audio': false,
});
log('✅ Stream de pantalla capturado');
// 3. Agregar stream local al PeerConnection
_localStream!.getTracks().forEach((track) {
log('➕ Agregando track: ${track.kind} - ${track.id}');
_peerConnection!.addTrack(track, _localStream!);
});
log('✅ Tracks agregados al PeerConnection');
// 4. Crear oferta DESPUÉS de agregar tracks
RTCSessionDescription offer = await _peerConnection!.createOffer({'offerToReceiveAudio': false, 'offerToReceiveVideo': true});
await _peerConnection!.setLocalDescription(offer);
log('✅ Oferta WebRTC creada');
// 5. Enviar oferta al servidor
_sendMessage({'type': 'webrtc-offer', 'sdp': offer.toMap()});
// Marcar actividad tras generar y enviar oferta
_markActivity();
log('✅ Oferta enviada al servidor');
log('✅ Screen capture y WebRTC iniciados completamente');
// 6. Timeout de conexión - si no se conecta en 60 segundos, limpiar
Future.delayed(const Duration(seconds: 60), () {
if (_peerConnection != null && _peerConnection!.connectionState != RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
log('⏱️ Timeout de conexión WebRTC - limpiando recursos');
log(jsonEncode(_peerConnection!.connectionState));
_handleConnectionFailure();
}
});
} catch (e) {
log('❌ Error al iniciar screen capture: $e');
// Limpiar en caso de error
await stopScreenCapture();
}
}