Звук Flutter webrtc не работает на Android

Во Flutter я хочу сделать голосовой вызов между двумя узлами. Я использую Flutter-WebRTC. Я проводил некоторые тесты, и видео, кажется, работает с webrtc, но нет звука. Я вижу видео удаленного узла, но не слышу звука ни с одной из сторон.

Один пир — это мой телефон на Android, а другой — эмулятор.

Мой код main.dart:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:sdp_transform/sdp_transform.dart';
import 'dart:developer' as developer;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'WebRTC lets learn together'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  bool _offer = false;
  RTCPeerConnection _peerConnection;
  MediaStream _localStream;
  RTCVideoRenderer _localRenderer = new RTCVideoRenderer();
  RTCVideoRenderer _remoteRenderer = new RTCVideoRenderer();

  final sdpController = TextEditingController();

  @override
  dispose() {
    _localRenderer.dispose();
    _remoteRenderer.dispose();
    sdpController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    initRenderers();
    _createPeerConnection().then((pc) {
      _peerConnection = pc;
    });
    super.initState();
  }

  initRenderers() async {
    await _localRenderer.initialize();
    await _remoteRenderer.initialize();
  }

  void _createOffer() async {
    RTCSessionDescription description =
        await _peerConnection.createOffer({'offerToReceiveAudio': 1, 'offerToReceiveVideo': 1});
    var session = parse(description.sdp);
    print(json.encode(session));
    _offer = true;

    _peerConnection.setLocalDescription(description);
  }

  void _createAnswer() async {
    RTCSessionDescription description =
        await _peerConnection.createAnswer({'offerToReceiveAudio': 1, 'offerToReceiveVideo': 1});

    var session = parse(description.sdp);
    print(json.encode(session));

    _peerConnection.setLocalDescription(description);
  }

  void _setRemoteDescription() async {
    String jsonString = sdpController.text;
    dynamic session = await jsonDecode('$jsonString');

    String sdp = write(session, null);

    // RTCSessionDescription description =
    //     new RTCSessionDescription(session['sdp'], session['type']);
    RTCSessionDescription description =
        new RTCSessionDescription(sdp, _offer ? 'answer' : 'offer');
    print(description.toMap());

    await _peerConnection.setRemoteDescription(description);
  }

  void _addCandidate() async {
    String jsonString = sdpController.text;
    dynamic session = await jsonDecode('$jsonString');
    print(session['candidate']);
    dynamic candidate =
        new RTCIceCandidate(session['candidate'], session['sdpMid'], session['sdpMlineIndex']);
    await _peerConnection.addCandidate(candidate);
  }

  _createPeerConnection() async {
    Map<String, dynamic> configuration = {
      "iceServers": [
        {"url": "stun:stun.l.google.com:19302"},
      ]
    };

    final Map<String, dynamic> offerSdpConstraints = {
      "mandatory": {
        "OfferToReceiveAudio": true,
        "OfferToReceiveVideo": true,
      },
      "optional": [],
    };

    _localStream = await _getUserMedia();

    RTCPeerConnection pc = await createPeerConnection(configuration, offerSdpConstraints);
    pc.addStream(_localStream);

    pc.onIceCandidate = (e) {
      if (e.candidate != null) {
        print(json.encode({
          'candidate': e.candidate.toString(),
          'sdpMid': e.sdpMid.toString(),
          'sdpMlineIndex': e.sdpMlineIndex,
        }));
      }
    };

    pc.onIceConnectionState = (e) {
      print(e);
    };

    pc.onAddStream = (stream) {
      print('addStream: ' + stream.id);
      _remoteRenderer.srcObject = stream;
    };

    return pc;
  }

  _getUserMedia() async {
    final Map<String, dynamic> mediaConstraints = {
      'audio': false,
      'video': {
        'facingMode': 'user',
      },
    };

    MediaStream stream = await MediaDevices.getUserMedia(mediaConstraints);

    _localRenderer.srcObject = stream;

    return stream;
  }

  SizedBox videoRenderers() => SizedBox(
      height: 210,
      child: Row(children: [
        Flexible(
          child: new Container(
            key: new Key("local"),
            margin: new EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
            decoration: new BoxDecoration(color: Colors.black),
            child: new RTCVideoView(_localRenderer)
          ),
        ),
        Flexible(
          child: new Container(
              key: new Key("remote"),
              margin: new EdgeInsets.fromLTRB(5.0, 5.0, 5.0, 5.0),
              decoration: new BoxDecoration(color: Colors.black),
              child: new RTCVideoView(_remoteRenderer)),
        )
      ]));

  Row offerAndAnswerButtons() =>
      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[
        new RaisedButton(
          onPressed: _createOffer,
          child: Text('Offer'),
          color: Colors.amber,
        ),
        RaisedButton(
          onPressed: _createAnswer,
          child: Text('Answer'),
          color: Colors.amber,
        ),
      ]);

  Row sdpCandidateButtons() =>
      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[
        RaisedButton(
          onPressed: _setRemoteDescription,
          child: Text('Set Remote Desc'),
          color: Colors.amber,
        ),
        RaisedButton(
          onPressed: _addCandidate,
          child: Text('Add Candidate'),
          color: Colors.amber,
        )
      ]);

  Padding sdpCandidatesTF() => Padding(
        padding: const EdgeInsets.all(16.0),
        child: TextField(
          controller: sdpController,
          keyboardType: TextInputType.multiline,
          maxLines: 4,
          maxLength: TextField.noMaxLength,
        ),
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Container(
            child: Column(children: [
          videoRenderers(),
          offerAndAnswerButtons(),
          sdpCandidatesTF(),
          sdpCandidateButtons(),
        ])));
  }
}

В build.gradle изменил minSdkVersion на 21.

В AndroidManifest.xml добавлено:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

Я вижу видео удаленного узла, но не слышу звука ни с одной из сторон. Я что-то упускаю?


person user5155835    schedule 18.02.2021    source источник


Ответы (1)


Месяц назад столкнулся с точно такой же проблемой. Убедитесь, зайдя в настройки, что микрофон эмулятора активен и использует микрофон хоста. Еще один момент, о котором мне нужно было позаботиться, это то, что звук работал только тогда, когда вызов был инициирован из эмулятора.

Когда я нажал кнопку вызова на своем реальном телефоне, камера включилась, но не было звука. Но когда я сначала нажимаю кнопку на эмуляторе, все работает хорошо.

Если вы используете студию Android, будьте осторожны: возможность использования аудиовхода хоста отключается при каждом запуске эмулятора.

Как сказано в документации:

Если вы хотите использовать аудиоданные хоста, вы можете включить эту опцию, перейдя в Расширенные элементы управления > Микрофон и включив Виртуальный микрофон использует аудиовход хоста. Этот параметр автоматически отключается при каждом перезапуске эмулятора.

person Tanguy Lhinares    schedule 19.02.2021
comment
Спасибо. Не могли бы вы также поделиться кодом, который вы использовали? - person user5155835; 19.02.2021
comment
И удалось ли получить звук с эмулятора? - person user5155835; 19.02.2021
comment
Извините за мой поздний ответ, к сожалению, я не могу разместить здесь общедоступный код, зная, что это частное приложение, созданное для клиента, однако я присоединюсь к ссылка на папку mega.nz, содержащую файлы, связанные с WebRTC в моем приложении. Приложению не нужно видео, поэтому вы найдете только код для микрофона. Чтобы ответить на ваш второй вопрос, да, я смог получить звук от эмулятора. Я также присоединил изображение для настроек аудиовхода хоста в Android Studio. Надеюсь, это поможет в любом случае. - person Tanguy Lhinares; 19.02.2021
comment
Благодарю вас! Итак, вы добавляете дорожки в одноранговое соединение для отправки звука с телефона здесь: _localStream.getTracks().forEach((track) async => await pc.addTrack(track, _localStream)); Но не могли бы вы сообщить мне, как вы воспроизводите звук, полученный на webrtc? Это в событии onTrack? - person user5155835; 19.02.2021
comment
Я не очень хорошо знаком с библиотекой webrtc и у меня нет времени углубляться в этот проект, но могу посоветовать вам две ссылки, которые помогли мне разработать все. Github — Flutter Webrtc и файл webrtc официальная документация - person Tanguy Lhinares; 19.02.2021
comment
Еще раз спасибо. Не могли бы вы просто сообщить мне, использовали ли вы сам RTCVideoRenderer для воспроизведения звука? или это было что-то другое? - person user5155835; 27.02.2021
comment
Не за что. Да, я использовал RTCVideoRenderer для воспроизведения звука, это не оптимально, но у меня не было много времени, поэтому я взял пример (предыдущая ссылка на Github) и удалил виджет, который отображал видео. Как я уже сказал, это далеко не оптимально, и если у вас есть время, я советую вам более подробно изучить, как RTCVideoRenderer воспроизводит звук, и создать свой собственный класс RTCAudioRenderer. - person Tanguy Lhinares; 01.03.2021