Flutter_webrtc: Не могу присоединиться к ранее покинутому каналу

Я разрабатываю потоковое приложение: одно приложение (сервер) транслирует видео, а другое приложение (клиент) это видео отображает. Я использовал пакет 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;
}
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
107
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

После некоторых исследований я обнаружил, что моя проблема такая же, как и в вопросе Не удалось установить удаленный ответ 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);
  }

Другие вопросы по теме