Flutter webrtc音频无法在android上运行

Posted

技术标签:

【中文标题】Flutter webrtc音频无法在android上运行【英文标题】:Flutter webrtc audio not working on android 【发布时间】:2021-02-18 17:39:33 【问题描述】:

在 Flutter 中,我希望在两个节点之间进行语音通话。我正在使用Flutter-WebRTC。我正在做一些测试,视频似乎正在使用 webrtc,但没有音频。 我看到远程对等方的视频,但在任何一侧都听不到任何音频。

一个是我的安卓手机,另一个是模拟器

我的 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" />

我看到远程对等方的视频,但在任何一侧都听不到任何音频。我错过了什么吗?

【问题讨论】:

【参考方案1】:

一个月前我遇到了完全相同的问题。 确保通过设置模拟器的麦克风处于活动状态并使用主机麦克风。 我需要注意的另一点是音频仅在从模拟器启动呼叫时才起作用。

当我在我的真实手机上点击通话按钮时,相机打开但音频没有。但是当我先点击模拟器上的按钮时,一切正常。

如果您使用的是 Android Studio,请注意每次启动模拟器时都会禁用使用主机音频输入的选项。

正如documentation 所说:

如果您想使用主机音频数据,您可以通过转到扩展控制 > 麦克风并启用虚拟麦克风使用主机音频输入来启用该选项。每当模拟器重新启动时,此选项会自动禁用。

【讨论】:

谢谢。您能否也请分享您使用的代码? 你也能从模拟器中获得声音吗? 对不起,我的回答迟了,很遗憾我不能把公共代码放在这里,因为我知道这是一个为客户制作的私人应用程序,但是我会将link 加入到包含以下内容的 mega.nz 文件夹中我的应用程序中与 WebRTC 相关的文件。该应用程序不需要视频,因此您只能找到麦克风的代码。要回答您的第二个问题,是的,我能够从模拟器中获得声音。我还在 Android Studio 中加入了“主机音频输入设置”的图像。希望对您有所帮助。 谢谢!因此,您正在向 peerconnection 添加音轨以在此处从手机发送音频:_localStream.getTracks().forEach((track) async =&gt; await pc.addTrack(track, _localStream)); 但是请您告诉我您如何播放在 webrtc 上收到的音频?是在onTrack 事件中吗? 我的荣幸。是的,我使用 RTCVideoRenderer 播放音频,这不是最佳选择,但我没有太多时间,所以我举了这个例子(之前的 Github 链接)并删除了显示视频的小部件。正如我所说,这远非最佳,如果您有时间,我建议您更深入地了解 RTCVideoRenderer 如何播放音频并创建自己的 RTCAudioRenderer 类【参考方案2】:
_getUserMedia() async 
    final Map<String, dynamic> mediaConstraints = 
      'audio': false, // ---- Make it true
      'video': 
        'facingMode': 'user',
      ,
    ;

使下面的音频为真。

【讨论】:

以上是关于Flutter webrtc音频无法在android上运行的主要内容,如果未能解决你的问题,请参考以下文章

如何修复在flutter webview中无法访问视频流(NotAllowedError)以使用html5 + webRTC相机api?

使用 webrtc 进行音频广播

如何在服务器上使用 ffmpeg 从 WebRTC 流中获取音频和视频

如何在 Flutter(Android 和 Ios)中实现与 WebRTC 的电话会议视频聊天

webrtc 上用于远程流的网络音频分析器

webRTC 真的可以在没有噪音的情况下流式传输高质量的音频吗?