Я разрабатываю потоковое приложение: одно приложение (сервер) транслирует видео, а другое приложение (клиент) это видео отображает. Я использовал пакет flutter_webrtc
для общения в реальном времени. Я следовал следующему руководству:
https://thewikihow.com/video_hAKQzNQmNe0
https://github.com/md-weber/webrtc_tutorial/
В настоящее время серверное приложение может успешно создать канал и транслировать видео, а клиентское приложение может присоединиться к этому каналу и просмотреть видео. Но когда клиент покидает канал и позже пытается подключиться к тому же каналу, он не может получить видео, отображается только черный экран. Ошибки не отображаются.
Я использовал flutter_riverpod
в качестве управления состоянием, и все приведенные ниже коды находятся внутри StateNotifiers
.
Функция создания канала со стороны серверного приложения.
Future<String> startStream(RTCVideoRenderer localVideo) async {
state = ProcessState.loading;
final stream = await navigator.mediaDevices.getUserMedia(<String, dynamic>{
'video': true,
'audio': true,
});
localVideo.srcObject = stream;
localStream = stream;
final roomId = await _createRoom();
await Wakelock.enable();
state = ProcessState.working;
return roomId;
}
Future<String> _createRoom() async {
final db = FirebaseFirestore.instance;
final roomRef = db.collection('rooms').doc();
peerConnection = await createPeerConnection(configuration);
localStream?.getTracks().forEach((track) {
peerConnection?.addTrack(track, localStream!);
});
// Code for collecting ICE candidates below
final callerCandidatesCollection = roomRef.collection('callerCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
callerCandidatesCollection.add(candidate.toMap() as Map<String, dynamic>);
};
// Add code for creating a room
final offer = await peerConnection!.createOffer();
await peerConnection!.setLocalDescription(offer);
final roomWithOffer = <String, dynamic>{'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
roomId = roomRef.id;
// Listening for remote session description below
roomRef.snapshots().listen((snapshot) async {
final data = snapshot.data();
if (peerConnection?.getRemoteDescription() != null && data != null && data['answer'] != null){
final answer = data['answer'] as Map<String, dynamic>;
final description = RTCSessionDescription(
answer['sdp'] as String?,
answer['type'] as String?,
);
await peerConnection?.setRemoteDescription(description);
}
});
// Listen for remote Ice candidates below
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
for (final change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
final data = change.doc.data();
peerConnection!.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
}
});
return roomId!;
}
Функция для присоединения к каналу со стороны клиентского приложения.
Future<bool> startStream(String? roomId, RTCVideoRenderer remoteVideo) async {
if (roomId == null || roomId.isEmpty) {
return false;
}
state = ProcessState.loading;
final result = await _joinRoom(roomId, remoteVideo);
if (result) {
state = ProcessState.working;
await Wakelock.enable();
} else {
state = ProcessState.notInitialized;
}
return result;
}
Future<bool> _joinRoom(String roomId, RTCVideoRenderer remoteVideo) async {
final db = FirebaseFirestore.instance;
final DocumentReference roomRef = db.collection('rooms').doc(roomId);
final roomSnapshot = await roomRef.get();
if (roomSnapshot.exists) {
peerConnection = await createPeerConnection(configuration);
peerConnection?.onAddStream = (MediaStream stream) {
onAddRemoteStream?.call(stream);
remoteStream = stream;
};
// Code for collecting ICE candidates below
final calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection?.onIceCandidate = (RTCIceCandidate candidate) {
final candidateMap = candidate.toMap() as Map<String, dynamic>;
calleeCandidatesCollection.add(candidateMap);
};
peerConnection?.onTrack = (RTCTrackEvent event) {
event.streams[0].getTracks().forEach((track) {
remoteStream?.addTrack(track);
});
};
// Code for creating SDP answer below
final data = roomSnapshot.data() as Map<String, dynamic>?;
final offer = data?['offer'] as Map<String, dynamic>?;
await peerConnection?.setRemoteDescription(
RTCSessionDescription(
offer?['sdp'] as String?,
offer?['type'] as String?,
),
);
final answer = await peerConnection!.createAnswer();
await peerConnection!.setLocalDescription(answer);
final roomWithAnswer = <String, dynamic>{
'answer': {
'type': answer.type,
'sdp': answer.sdp,
}
};
await roomRef.update(roomWithAnswer);
// Listening for remote ICE candidates below
roomRef.collection('callerCandidates').snapshots().listen((snapshot) {
for (final document in snapshot.docChanges) {
final data = document.doc.data();
peerConnection!.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
});
this.roomId = roomId;
return true;
}
return false;
}
После некоторых исследований я обнаружил, что моя проблема такая же, как и в вопросе Не удалось установить удаленный ответ sdp: Звонок в неправильном состоянии: стабильный. Это было вызвано тем, что один RTCPeerConnection
может установить только одно одноранговое соединение. Так что я мог бы исправить это, создавая новый RTCPeerConnection
на стороне сервера каждый раз, когда новый клиент хочет присоединиться к каналу.
Future<String> _createRoom() async {
final db = FirebaseFirestore.instance;
final roomRef = db.collection('rooms').doc();
await newPeerConnection(roomRef);
roomId = roomRef.id;
// Listening for remote session description below
roomRef.snapshots().listen((snapshot) async {
final data = snapshot.data();
if (data != null && data['answer'] != null) {
final answer = data['answer'] as Map<String, dynamic>;
final description = RTCSessionDescription(
answer['sdp'] as String?,
answer['type'] as String?,
);
await peerConnectionList.last.setRemoteDescription(description);
await newPeerConnection(roomRef);
}
});
// Listen for remote Ice candidates below
roomRef.collection('calleeCandidates').snapshots().listen((snapshot) {
for (final change in snapshot.docChanges) {
if (change.type == DocumentChangeType.added) {
final data = change.doc.data();
peerConnectionList.last.addCandidate(
RTCIceCandidate(
data?['candidate'] as String?,
data?['sdpMid'] as String?,
data?['sdpMLineIndex'] as int?,
),
);
}
}
});
return roomId!;
}
Future<void> newPeerConnection(DocumentReference roomRef) async {
final peerConnection = await createPeerConnection(
configuration,
offerSdpConstraints,
);
_registerPeerConnectionListeners(peerConnection);
localStream?.getTracks().forEach((track) {
peerConnection.addTrack(track, localStream!);
});
final offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
final roomWithOffer = <String, dynamic>{'offer': offer.toMap()};
await roomRef.set(roomWithOffer);
peerConnectionList.add(peerConnection);
}