关于flutter:如何用-Flutter开发一个直播应用

5次阅读

共计 16123 个字符,预计需要花费 41 分钟才能阅读完成。

昨天,我在加入在线瑜伽课程时,才意识到我的日常流动中应用了这么多的视频直播 App– 从商务会议到瑜伽课程,还有即兴演奏和电影之夜。对于大多数居家隔离的人来说,视频直播是靠近世界的最好形式。海量用户的观看和直播,也让“完满的流媒体 App”成为了新的市场诉求。

在这篇文章中,我将疏导你应用声网 Agora Flutter SDK 开发本人的直播 App。你能够依照本人的需要来定制你的利用界面,同时还可能放弃最高的视频品质和简直感触不到的提早。

开发环境

如果你是 Flutter 的老手,那么请拜访 Flutter 官网装置 Flutter。

  • 在 https://pub.dev/ 搜寻“Agora”,下载声网 Agora Flutter SDK v3.2.1
  • 在 https://pub.dev/ 搜寻“Agora”,声网 Agora Flutter RTM SDK v0.9.14
  • VS Code 或其余 IDE
  • 声网 Agora 开发者账户,请拜访 Agora.io 注册

我的项目设置

咱们先创立一个 Flutter 我的项目。关上你的终端,导航到你开发用的文件夹,而后输出以下内容。

flutter create agora_live_streaming

导航到你的 pubspec.yaml 文件,在该文件中,增加以下依赖项:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.0
  permission_handler: ^5.1.0+2
  agora_rtc_engine: ^3.2.1
  agora_rtm: ^0.9.14

在增加文件压缩包的时候,要留神缩进,免得出错。

你的我的项目文件夹中,运行以下命令来装置所有的依赖项:

flutter pub get

一旦咱们有了所有的依赖项,咱们就能够创立文件构造了。导航到 lib 文件夹,并创立一个像这样的文件构造。

创立主页面

首先,我创立了一个简略的登录表单,须要输出三个信息:用户名、频道名称和用户角色(观众或主播)。你能够依据本人的须要来定制这个界面。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {final _username = TextEditingController();
  final _channelName = TextEditingController();
  bool _isBroadcaster = false;
  String check = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        resizeToAvoidBottomInset: true,
        body: Center(
          child: SingleChildScrollView(physics: NeverScrollableScrollPhysics(),
            child: Stack(
              children: <Widget>[
                Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      Padding(padding: const EdgeInsets.all(30.0),
                        child: Image.network(
                          'https://www.agora.io/en/wp-content/uploads/2019/06/agoralightblue-1.png',
                          scale: 1.5,
                        ),
                      ),
                      Container(width: MediaQuery.of(context).size.width * 0.85,
                        height: MediaQuery.of(context).size.height * 0.2,
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: <Widget>[
                            TextFormField(
                              controller: _username,
                              decoration: InputDecoration(
                                border: OutlineInputBorder(borderRadius: BorderRadius.circular(20),
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                hintText: 'Username',
                              ),
                            ),
                            TextFormField(
                              controller: _channelName,
                              decoration: InputDecoration(
                                border: OutlineInputBorder(borderRadius: BorderRadius.circular(20),
                                  borderSide: BorderSide(color: Colors.grey),
                                ),
                                hintText: 'Channel Name',
                              ),
                            ),
                          ],
                        ),
                      ),
                      Container(width: MediaQuery.of(context).size.width * 0.65,
                        padding: EdgeInsets.symmetric(vertical: 10),
                        child: SwitchListTile(
                            title: _isBroadcaster
                                ? Text('Broadcaster')
                                : Text('Audience'),
                            value: _isBroadcaster,
                            activeColor: Color.fromRGBO(45, 156, 215, 1),
                            secondary: _isBroadcaster
                                ? Icon(
                                    Icons.account_circle,
                                    color: Color.fromRGBO(45, 156, 215, 1),
                                  )
                                : Icon(Icons.account_circle),
                            onChanged: (value) {setState(() {
                                _isBroadcaster = value;
                                print(_isBroadcaster);
                              });
                            }),
                      ),
                      Padding(padding: const EdgeInsets.symmetric(vertical: 25),
                        child: Container(width: MediaQuery.of(context).size.width * 0.85,
                          decoration: BoxDecoration(
                              color: Colors.blue,
                              borderRadius: BorderRadius.circular(20)),
                          child: MaterialButton(
                            onPressed: onJoin,
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[
                                Text(
                                  'Join',
                                  style: TextStyle(
                                      color: Colors.white,
                                      letterSpacing: 1,
                                      fontWeight: FontWeight.bold,
                                      fontSize: 20),
                                ),
                                Icon(
                                  Icons.arrow_forward,
                                  color: Colors.white,
                                )
                              ],
                            ),
                          ),
                        ),
                      ),
                      Text(
                        check,
                        style: TextStyle(color: Colors.red),
                      )
                    ],
                  ),
                ),
              ],
            ),
          ),
        ));
  }
}

这样就会创立一个相似于这样的用户界面:

每当按下“退出(Join)”按钮,它就会调用 onJoin 函数,该函数首先取得用户在通话过程中拜访其摄像头和麦克风的权限。一旦用户授予这些权限,咱们就进入下一个页面,broadcast_page.dart。

Future<void> onJoin() async {if (_username.text.isEmpty || _channelName.text.isEmpty) {setState(() {check = 'Username and Channel Name are required fields';});
    } else {setState(() {check = '';});
      await _handleCameraAndMic(Permission.camera);
      await _handleCameraAndMic(Permission.microphone);
      Navigator.of(context).push(
        MaterialPageRoute(builder: (context) => BroadcastPage(
            userName: _username.text,
            channelName: _channelName.text,
            isBroadcaster: _isBroadcaster,
          ),
        ),
      );
    }
  }

为了要求用户拜访摄像头和麦克风,咱们应用一个名为 permission_handler 的包。这里我申明了一个名为_handleCameraAndMic(), 的函数,我将在 onJoin() 函数中援用它。

Future<void> onJoin() async {if (_username.text.isEmpty || _channelName.text.isEmpty) {setState(() {check = 'Username and Channel Name are required fields';});
    } else {setState(() {check = '';});
      await _handleCameraAndMic(Permission.camera);
      await _handleCameraAndMic(Permission.microphone);
      Navigator.of(context).push(
        MaterialPageRoute(builder: (context) => BroadcastPage(
            userName: _username.text,
            channelName: _channelName.text,
            isBroadcaster: _isBroadcaster,
          ),
        ),
      );
    }
  }

建设咱们的流媒体页面

默认状况下,观众端的摄像头是禁用的,麦克风也是静音的,但主播端要提供两者的拜访权限。所以咱们在创立界面的时候,会依据客户端的角色来设计相应的款式。

每当用户抉择观众角色时,就会调用这个页面,在这里他们能够观看主播的直播,并能够抉择与主播聊天互动。

但当用户抉择作为主播角色退出时,能够看到该频道中其余主播的流,并能够抉择与频道中的所有人(主播和观众)进行互动。

上面咱们开始创立界面。

class BroadcastPage extends StatefulWidget {
  final String channelName;
  final String userName;
  final bool isBroadcaster;

  const BroadcastPage({Key key, this.channelName, this.userName, this.isBroadcaster}) : super(key: key);

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

class _BroadcastPageState extends State<BroadcastPage> {final _users = <int>[];
  final _infoStrings = <String>[];
  RtcEngine _engine;
  bool muted = false;

  @override
  void dispose() {
    // clear users
    _users.clear();
    // destroy sdk and leave channel
    _engine.destroy();
    super.dispose();}

  @override
  void initState() {super.initState();
    // initialize agora sdk
    initialize();}

  Future<void> initialize() async {}


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Stack(
          children: <Widget>[_viewRows(),
            _toolbar(),],
        ),
      ),
    );
  }
}

在这里,我创立了一个名为 BroadcastPage 的 StatefulWidget,它的构造函数包含了频道名称、用户名和 isBroadcaster(布尔值)的值。

在咱们的 BroadcastPage 类中,咱们申明一个 RtcEngine 类的对象。为了初始化这个对象,咱们创立一个 initState() 办法,在这个办法中咱们调用了初始化函数。

initialize() 函数不仅初始化声网 Agora SDK,它也是调用的其余次要函数的函数,如_initAgoraRtcEngine(),_addAgoraEventHandlers(), 和 joinChannel()。

Future<void> initialize() async {print('Client Role: ${widget.isBroadcaster}');
    if (appId.isEmpty) {setState(() {
        _infoStrings.add('APP_ID missing, please provide your APP_ID in settings.dart',);
        _infoStrings.add('Agora Engine is not starting');
      });
      return;
    }
    await _initAgoraRtcEngine();
    _addAgoraEventHandlers();
    await _engine.joinChannel(null, widget.channelName, null, 0);
  }

当初让咱们来理解一下咱们在 initialize() 中调用的这三个函数的意义。

  • _initAgoraRtcEngine() 用于创立声网 Agora SDK 的实例。应用你从声网 Agora 开发者后盾失去的我的项目 App ID 来初始化它。在这外面,咱们应用 enableVideo() 函数来启用视频模块。为了将频道配置文件从视频通话(默认值)改为直播,咱们调用 setChannelProfile() 办法,而后设置用户角色。
Future<void> _initAgoraRtcEngine() async {_engine = await RtcEngine.create(appId);
    await _engine.enableVideo();
    await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
    if (widget.isBroadcaster) {await _engine.setClientRole(ClientRole.Broadcaster);
    } else {await _engine.setClientRole(ClientRole.Audience);
    }
}
  • _addAgoraEventHandlers() 是一个解决所有次要回调函数的函数。咱们从 setEventHandler() 开始,它监听 engine 事件并接管相应 RtcEngine 的统计数据。

一些重要的回调包含:

  • joinChannelSuccess() 在本地用户退出指定频道时被触发。它返回频道名,用户的 uid,以及本地用户退出频道所需的工夫(以毫秒为单位)。
  • leaveChannel() 与 joinChannelSuccess() 相同,因为它是在用户来到频道时触发的。每当用户来到频道时,它就会返回调用的统计信息。这些统计包含提早、CPU 使用率、持续时间等。
  • userJoined() 是一个当近程用户退出一个特定频道时被触发的办法。一个胜利的回调会返回近程用户的 id 和通过的工夫。
  • userOffline() 与 userJoined() 相同,因为它产生在用户来到频道的时候。一个胜利的回调会返回 uid 和离线的起因,包含掉线、退出等。
  • firstRemoteVideoFrame() 是一个当近程视频的第一个视频帧被渲染时被调用的办法,它能够帮忙你返回 uid、宽度、高度和通过的工夫。
void _addAgoraEventHandlers() {_engine.setEventHandler(RtcEngineEventHandler(error: (code) {setState(() {
        final info = 'onError: $code';
        _infoStrings.add(info);
      });
    }, joinChannelSuccess: (channel, uid, elapsed) {setState(() {
        final info = 'onJoinChannel: $channel, uid: $uid';
        _infoStrings.add(info);
      });
    }, leaveChannel: (stats) {setState(() {_infoStrings.add('onLeaveChannel');
        _users.clear();});
    }, userJoined: (uid, elapsed) {setState(() {
        final info = 'userJoined: $uid';
        _infoStrings.add(info);
        _users.add(uid);
      });
    }, userOffline: (uid, elapsed) {setState(() {
        final info = 'userOffline: $uid';
        _infoStrings.add(info);
        _users.remove(uid);
      });
    },
   ));
  }
  • joinChannel() 一个频道在视频通话中就是一个房间。一个 joinChannel() 函数能够帮忙用户订阅一个特定的频道。这能够应用咱们的 RtcEngine 对象来申明:
await _engine.joinChannel(token, "channel-name", "Optional Info", uid);

留神:此我的项目是开发环境,仅供参考,请勿间接用于生产环境。倡议在生产环境中运行的所有 RTE App 都应用 Token 鉴权。对于声网 Agora 平台中基于 Token 鉴权的更多信息,请参考声网文档核心:https://docs.agora.io/cn

以上总结了制作这个实时互动视频直播所需的所有性能和办法。当初咱们能够制作咱们的组件了,它将负责咱们利用的残缺用户界面。

在我的办法中,我申明了两个小部件(_viewRows() 和_toolbar(),它们负责显示主播的网格,以及一个由断开、静音、切换摄像头和音讯按钮组成的工具栏。

咱们从 _viewRows() 开始。为此,咱们须要晓得主播和他们的 uid 来显示他们的视频。咱们须要一个带有他们 uid 的本地和近程用户的通用列表。为了实现这一点,咱们创立一个名为_getRendererViews() 的小组件,其中咱们应用了 RtcLocalView 和 RtcRemoteView.。

List<Widget> _getRenderViews() {final List<StatefulWidget> list = [];
    if(widget.isBroadcaster) {list.add(RtcLocalView.SurfaceView());
    }
    _users.forEach((int uid) => list.add(RtcRemoteView.SurfaceView(uid: uid)));
    return list;
  }

  /// Video view wrapper
  Widget _videoView(view) {return Expanded(child: Container(child: view));
  }

  /// Video view row wrapper
  Widget _expandedVideoRow(List<Widget> views) {final wrappedViews = views.map<Widget>(_videoView).toList();
    return Expanded(
      child: Row(children: wrappedViews,),
    );
  }

  /// Video layout wrapper
  Widget _viewRows() {final views = _getRenderViews();
    switch (views.length) {
      case 1:
        return Container(
            child: Column(children: <Widget>[_videoView(views[0])],
        ));
      case 2:
        return Container(
            child: Column(
          children: <Widget>[_expandedVideoRow([views[0]]),
            _expandedVideoRow([views[1]])
          ],
        ));
      case 3:
        return Container(
            child: Column(
          children: <Widget>[_expandedVideoRow(views.sublist(0, 2)),
            _expandedVideoRow(views.sublist(2, 3))
          ],
        ));
      case 4:
        return Container(
            child: Column(
          children: <Widget>[_expandedVideoRow(views.sublist(0, 2)),
            _expandedVideoRow(views.sublist(2, 4))
          ],
        ));
      default:
    }
    return Container();}

有了它,你就能够实现一个残缺的视频通话 app。为了减少断开通话、静音、切换摄像头和音讯等性能,咱们将创立一个名为__toolbar() 有四个按钮的根本小组件。而后依据用户角色对这些按钮进行款式设计,这样观众只能进行聊天,而主播则能够应用所有的性能:

Widget _toolbar() {
    return widget.isBroadcaster
        ? Container(
            alignment: Alignment.bottomCenter,
            padding: const EdgeInsets.symmetric(vertical: 48),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RawMaterialButton(
                  onPressed: _onToggleMute,
                  child: Icon(
                    muted ? Icons.mic_off : Icons.mic,
                    color: muted ? Colors.white : Colors.blueAccent,
                    size: 20.0,
                  ),
                  shape: CircleBorder(),
                  elevation: 2.0,
                  fillColor: muted ? Colors.blueAccent : Colors.white,
                  padding: const EdgeInsets.all(12.0),
                ),
                RawMaterialButton(onPressed: () => _onCallEnd(context),
                  child: Icon(
                    Icons.call_end,
                    color: Colors.white,
                    size: 35.0,
                  ),
                  shape: CircleBorder(),
                  elevation: 2.0,
                  fillColor: Colors.redAccent,
                  padding: const EdgeInsets.all(15.0),
                ),
                RawMaterialButton(
                  onPressed: _onSwitchCamera,
                  child: Icon(
                    Icons.switch_camera,
                    color: Colors.blueAccent,
                    size: 20.0,
                  ),
                  shape: CircleBorder(),
                  elevation: 2.0,
                  fillColor: Colors.white,
                  padding: const EdgeInsets.all(12.0),
                ),
                RawMaterialButton(
                  onPressed: _goToChatPage,
                  child: Icon(
                    Icons.message_rounded,
                    color: Colors.blueAccent,
                    size: 20.0,
                  ),
                  shape: CircleBorder(),
                  elevation: 2.0,
                  fillColor: Colors.white,
                  padding: const EdgeInsets.all(12.0),
                ),
              ],
            ),
          )
        : Container(
            alignment: Alignment.bottomCenter,
            padding: EdgeInsets.only(bottom: 48),
            child: RawMaterialButton(
              onPressed: _goToChatPage,
              child: Icon(
                Icons.message_rounded,
                color: Colors.blueAccent,
                size: 20.0,
              ),
              shape: CircleBorder(),
              elevation: 2.0,
              fillColor: Colors.white,
              padding: const EdgeInsets.all(12.0),
            ),
          );
  }

让咱们来看看咱们申明的四个性能:

  • _onToggleMute() 能够让你的数据流静音或者勾销静音。这里,咱们应用 muteLocalAudioStream() 办法,它采纳一个布尔输出来使数据流静音或勾销静音。
void _onToggleMute() {setState(() {muted = !muted;});
    _engine.muteLocalAudioStream(muted);
  }
  • _onSwitchCamera() 能够让你在前摄像头和后摄像头之间切换。在这里,咱们应用 switchCamera() 办法,它能够帮忙你实现所需的性能。
void _onSwitchCamera() {_engine.switchCamera();
  }
  • _onCallEnd() 断开呼叫并返回主页。
void _onCallEnd(BuildContext context) {Navigator.pop(context);
}
  • _goToChatPage() 导航到聊天界面。
void _goToChatPage() {Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => RealTimeMessaging(
          channelName: widget.channelName,
          userName: widget.userName,
          isBroadcaster: widget.isBroadcaster,
        ),)
    );
  }

建设咱们的聊天屏幕

为了扩大观众和主播之间的互动,咱们增加了一个聊天页面,任何人都能够发送音讯。要做到这一点,咱们应用声网 Agora Flutter RTM 包,它提供了向特定同行发送音讯或向频道播送音讯的选项。在本教程中,咱们将把音讯播送到频道上。

咱们首先创立一个有状态的小组件,它的构造函数领有所有的输出值:频道名称、用户名和 isBroadcaster。咱们将在咱们的逻辑中应用这些值,也将在咱们的页面设计中应用这些值。

为了初始化咱们的 SDK,咱们申明 initState() 办法,其中我申明的是_createClient(),它负责初始化。

class RealTimeMessaging extends StatefulWidget {
  final String channelName;
  final String userName;
  final bool isBroadcaster;

  const RealTimeMessaging({Key key, this.channelName, this.userName, this.isBroadcaster})
      : super(key: key);

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

class _RealTimeMessagingState extends State<RealTimeMessaging> {
  bool _isLogin = false;
  bool _isInChannel = false;

  final _channelMessageController = TextEditingController();

  final _infoStrings = <String>[];

  AgoraRtmClient _client;
  AgoraRtmChannel _channel;

  @override
  void initState() {super.initState();
    _createClient();}

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          body: Container(padding: const EdgeInsets.all(16),
            child: Column(
              children: [_buildInfoList(),
                Container(
                  width: double.infinity,
                  alignment: Alignment.bottomCenter,
                  child: _buildSendChannelMessage(),),
              ],
            ),
          )),
    );
  }  
}

在咱们的_createClient() 函数中,咱们创立一个 AgoraRtmClient 对象。这个对象将被用来登录和登记一个特定的频道。

void _createClient() async {_client = await AgoraRtmClient.createInstance(appId);
    _client.onMessageReceived = (AgoraRtmMessage message, String peerId) {_logPeer(message.text);
    };
    _client.onConnectionStateChanged = (int state, int reason) {
      print('Connection state changed:' +
          state.toString() +
          ', reason:' +
          reason.toString());
      if (state == 5) {_client.logout();
        print('Logout.');
        setState(() {_isLogin = false;});
      }
    };

    _toggleLogin();
    _toggleJoinChannel();}

在我的_createClient() 函数中,我援用了另外两个函数:

  • _toggleLogin() 应用 AgoraRtmClient 对象来登录和登记一个频道。它须要一个 Token 和一个 user ID 作为参数。这里,我应用用户名作为用户 ID。
void _toggleLogin() async {if (!_isLogin) {
      try {await _client.login(null, widget.userName);
        print('Login success:' + widget.userName);
        setState(() {_isLogin = true;});
      } catch (errorCode) {print('Login error:' + errorCode.toString());
      }
    }
  }
  • _toggleJoinChannel() 创立了一个 AgoraRtmChannel 对象,并应用这个对象来订阅一个特定的频道。这个对象将被用于所有的回调,当一个成员退出,一个成员来到,或者一个用户收到音讯时,回调都会被触发。
void _toggleJoinChannel() async {
    try {_channel = await _createChannel(widget.channelName);
      await _channel.join();
      print('Join channel success.');

      setState(() {_isInChannel = true;});
    } catch (errorCode) {print('Join channel error:' + errorCode.toString());
    }
  }

到这里,你将领有一个功能齐全的聊天利用。当初咱们能够制作小组件了,它将负责咱们利用的残缺用户界面。

这里,我申明了两个小组件:_buildSendChannelMessage() 和_buildInfoList().

  • _buildSendChannelMessage() 创立一个输出字段并触发一个函数来发送音讯。
  • _buildInfoList() 对音讯进行款式设计,并将它们放在惟一 的容器中。你能够依据设计需要来定制这些小组件。

这里有两个小组件:

  • _buildSendChannelMessage() 我曾经申明了一个 Row,它增加了一个文本输出字段和一 个按钮,这个按钮在被按下时调用 _toggleSendChannelMessage。
Widget _buildSendChannelMessage() {if (!_isLogin || !_isInChannel) {return Container();
    }
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        Container(width: MediaQuery.of(context).size.width * 0.75,
          child: TextFormField(
            showCursor: true,
            enableSuggestions: true,
            textCapitalization: TextCapitalization.sentences,
            controller: _channelMessageController,
            decoration: InputDecoration(
              hintText: 'Comment...',
              border: OutlineInputBorder(borderRadius: BorderRadius.circular(20),
                borderSide: BorderSide(color: Colors.grey, width: 2),
              ),
              enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(20),
                borderSide: BorderSide(color: Colors.grey, width: 2),
              ),
            ),
          ),
        ),
        Container(
          decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(40)),
              border: Border.all(
                color: Colors.blue,
                width: 2,
              )),
          child: IconButton(icon: Icon(Icons.send, color: Colors.blue),
            onPressed: _toggleSendChannelMessage,
          ),
        )
      ],
    );
  }

这个函数调用咱们之前申明的对象应用的 AgoraRtmChannel 类中的 sendMessage() 办法。这用到一个类型为 AgoraRtmMessage 的输出。

void _toggleSendChannelMessage() async {
    String text = _channelMessageController.text;
    if (text.isEmpty) {print('Please input text to send.');
      return;
    }
    try {await _channel.sendMessage(AgoraRtmMessage.fromText(text));
      _log(text);
      _channelMessageController.clear();} catch (errorCode) {print('Send channel message error:' + errorCode.toString());
    }
  }

_buildInfoList() 将所有本地音讯排列在左边,而用户收到的所有音讯则在右边。而后,这个文本音讯被包裹在一个容器内,并依据你的须要进行款式设计。

Widget _buildInfoList() {
    return Expanded(
        child: Container(
            child: _infoStrings.length > 0
                ? ListView.builder(
                    reverse: true,
                    itemBuilder: (context, i) {
                      return Container(
                        child: ListTile(
                          title: Align(alignment: _infoStrings[i].startsWith('%')
                                ? Alignment.bottomLeft
                                : Alignment.bottomRight,
                            child: Container(padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
                              color: Colors.grey,
                              child: Column(crossAxisAlignment: _infoStrings[i].startsWith('%') ?  CrossAxisAlignment.start : CrossAxisAlignment.end,
                                children: [_infoStrings[i].startsWith('%')
                                  ? Text(_infoStrings[i].substring(1),
                                      maxLines: 10,
                                      overflow: TextOverflow.ellipsis,
                                      textAlign: TextAlign.right,
                                      style: TextStyle(color: Colors.black),
                                    )
                                  : Text(_infoStrings[i],
                                      maxLines: 10,
                                      overflow: TextOverflow.ellipsis,
                                      textAlign: TextAlign.right,
                                      style: TextStyle(color: Colors.black),
                                    ),
                                  Text(
                                    widget.userName,
                                    textAlign: TextAlign.right,
                                    style: TextStyle(fontSize: 10,),   
                                  )
                                ],
                              ),
                            ),
                          ),
                        ),
                      );
                    },
                    itemCount: _infoStrings.length,
                  )
                : Container()));
  }

测试

一旦咱们实现了实时直播利用的开发,咱们能够在咱们的设施上进行测试。在终端中找到你的我的项目目录,而后运行这个命令。

flutter run

论断

祝贺,你曾经实现了本人的实时互动视频直播利用,应用声网 Agora Flutter SDK 开发了这个利用,并通过声网 Agora Flutter RTM SDK 实现了交互。

获取本文的 Demo:https://github.com/Meherdeep/Interactive-Broadcasting

获取更多教程、Demo、技术帮忙,请点击「浏览原文」拜访声网开发者社区。

正文完
 0