linkv_rtc_im 0.3.17 copy "linkv_rtc_im: ^0.3.17" to clipboard
linkv_rtc_im: ^0.3.17 copied to clipboard

linkv_rtc_im Audio/Video/IM Flutter SDK is a flutter plugin wrapper based on LinkV LVIMSDK native Android/iOS SDK

example/lib/main.dart

import 'dart:math';

import 'package:common_utils/common_utils.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:linkv_rtc_im/linkv_api_defines.dart';
import 'dart:async';
import 'package:linkv_rtc_im/rtm_flutter_plugin.dart';
import 'package:rtm_flutter_plugin_example/live_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'im_media_msg_page.dart';
import 'linkv_ui.dart';
import 'package:linkv_rtc_im/linkv_error_code.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:linkv_rtc_im/linkv_rtm_callback.dart';
import 'help.dart';
import 'package:linkv_rtc_im/bean/lv_im_msg.dart';

void main() => runApp(MyApp());

Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) async {
  print("wing myBackgroundMessageHandler:");
  if (message.containsKey('data')) {
    // Handle data message
    final dynamic data = message['data'];
    print("wing data: $data");
  }

  if (message.containsKey('notification')) {
    // Handle notification message

    final dynamic notification = message['notification'];
    print("wing notification: $notification");
  }

  // Or do other work.
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      debugShowCheckedModeBanner: false,
      home: MainView(),
    );
  }
}

class MainView extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MainView> implements LVIMEventCallback {
  final _roomIdFieldController = TextEditingController();
  final _msgInputTextEdit = TextEditingController();
  final _msgTargetIdTextEdit = TextEditingController();
  final _msgListTextEdit = TextEditingController();
  bool _isSDKInit = false;
  String _version;
  String userId;
  var TAG = "main [Wing] ";
  bool isTokenRequesting = false;
  bool isIMAuthed = false;

  @override
  void initState() {
    super.initState();
    final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
    _msgTargetIdTextEdit.text = '1496';

    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        print("wing onMessage: $message");
      },
      onBackgroundMessage: myBackgroundMessageHandler,
      onLaunch: (Map<String, dynamic> message) async {
        print("wing onLaunch: $message");
      },
      onResume: (Map<String, dynamic> message) async {
        print("wing onResume: $message");
      },
    );

    initSDK();
    RtmFlutterPlugin.getSdkVersion().then((value) => {
          this.setState(() {
            _version = value;
          })
        });
    RtmFlutterPlugin.setIMEventCallback(this);

    _roomIdFieldController.text = "1352";

    getUid().then((value) {
      userId = value;
      print("D/Wing own userId = " + userId);
      RtmFlutterPlugin.loginUser(userId, LVIMEnumDefine.LVIM_LOGIN_TYPE_AUTO)
          .then((value) {
        print(value);
        String loginResult = "";
        if (value == 0) {
          loginResult = "登录成功";
          _firebaseMessaging.getToken().then((token) {
            if(!ObjectUtil.isEmptyString(token)){
              print("wing token = " + token);
              RtmFlutterPlugin.uploadPushToken(token).then((value) {
                print("wing uploadPushToken " + value.eCode.toString());
              });
            }
          });
        } else {
          loginResult = "登录失败 code : " + value.toString();
        }

        Fluttertoast.showToast(
            msg: loginResult,
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER);
      });
    });
  }

  @override
  void dispose() {
    RtmFlutterPlugin.unInitImSDK();
    super.dispose();
  }

  Future<String> getUid() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String uid = prefs.getString("uid");
    if (uid == null || uid.length == 0) {
      uid = Random().nextInt(10000).toString();
      prefs.setString("uid", uid);
    }
    return uid;
  }

  void initSDK() {
    RtmFlutterPlugin.initImSDK(APP_ID_LIVE, APP_SECRET_LIVE, false)
        .then((result) {
      print('initSDK result:$result');
      if (result == LVErrorCode.SUCCESS) {
        _isSDKInit = true;
      }
    });
    RtmFlutterPlugin.setLogOpen(true);
  }

  void onClickHistoryMsg() async {
    String tid = _msgTargetIdTextEdit.text;
    List<LVIMMsg> list =
        await RtmFlutterPlugin.queryLocalPrivateHistoryMessage(tid, 0, 20);
    if (list == null || list.length == 0) {
      print("$TAG onClickHistoryMsg null");
      return;
    }
    print(
        "$TAG onClickHistoryMsg count = ${list.length} last dbid = ${list.first.dbid} send_State = ${list.first.sendState}");
  }

  void sendEventMsg() {
    int timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).round();
    RtmFlutterPlugin.sendMessageHasRead(_msgTargetIdTextEdit.text, timestamp)
        .then((lvimMsg) {
      print(
          '[Wing] sendEventMsg end result = ${lvimMsg.eCode}, dbid = ${lvimMsg.msg.cmdType}, sendState = ${lvimMsg.msg.sendState} stime = ${lvimMsg.msg.stime}');
      if (lvimMsg.eCode == 0) {
        Fluttertoast.showToast(msg: "Send Msg Success");
      } else {
        Fluttertoast.showToast(msg: "Send Msg Failed code:${lvimMsg.eCode}");
      }
    });

//    RtmFlutterPlugin.sendEventMessage(_msgTargetIdTextEdit.text, "flag_event_notify", _msgInputTextEdit.text).then((lvimMsg) {
//      print('[Wing] sendEventMsg end result = ${lvimMsg.eCode}, dbid = ${lvimMsg.msg.cmdType}, sendState = ${lvimMsg.msg.sendState} stime = ${lvimMsg.msg.stime}');
//      if (lvimMsg.eCode == 0) {
//        Fluttertoast.showToast(msg: "Send Msg Success");
//      } else {
//        Fluttertoast.showToast(msg: "Send Msg Failed code:${lvimMsg.eCode}");
//      }
//    });
  }

  void queryUnreadCount() async {
    print("queryUnreadCount start");
    var unReadMessageNum = await RtmFlutterPlugin.queryUnReadMessageNum();
    print(TAG + "unReadMessageNum = " + unReadMessageNum.toString());
    Fluttertoast.showToast(msg: "unreadCount:${unReadMessageNum}");
  }

  void onClickSessionList() async {
    List<LVIMSession> list = await RtmFlutterPlugin.querySessionList(
        LVIMEnumDefine.LVIMDB_TYPE_ROOM);
    if (list == null || list.length == 0) {
      print("$TAG onClickSessionList null");
      return;
    }

    list.forEach((element) {
      print(
          "$TAG onClickSessionList from=${element.lastMsg.fromID} to=${element.lastMsg.toID} dbid = ${element.lastMsg.dbid}, send_State = ${element.lastMsg.sendState}, unread_count = ${element.unreadCount}, content = ${element.lastMsg.msgContent}");
    });
  }

  void onClear1234Unread() async {
    String tid = _msgTargetIdTextEdit.text;
    int result = await RtmFlutterPlugin.clearPrivateSessionUnreadMsg(
        tid, LVIMEnumDefine.LVIMDB_TYPE_ROOM);
    print(TAG + " onClear1234Unread result = $result");
  }

  void onClearAllUnread() async {
    int result = await RtmFlutterPlugin.clearPrivateAllUnreadMsg(
        LVIMEnumDefine.LVIMDB_TYPE_ROOM);
    print(TAG + " onClearAllUnread result = $result");
  }

  void sendMessageHasRead() async {
    String tid = _msgTargetIdTextEdit.text;
    int stime = new DateTime.now().millisecondsSinceEpoch;
    var result = await RtmFlutterPlugin.sendMessageHasRead(tid, stime);

    print(TAG +
        " sendMessageHasRead result = ${result.eCode}, tid = $tid, stime = $stime");
    print(
        'sendMessageHasRead 回调成功');
    Fluttertoast.showToast(msg: "sendMessageHasRead result = ${result.eCode}, tid = $tid, stime = $stime");
  }

  void onDeleteChat() async {
    String tid = _msgTargetIdTextEdit.text;
    int rs = await RtmFlutterPlugin.deleteLocalPrivateSession(tid);
    print(TAG + " onDeleteChat result = $rs, tid = $tid");
  }

  void onClickSend() {
    print(
        "[Wing] sendPrivateMessage start to = ${_msgTargetIdTextEdit.text}, content = ${_msgInputTextEdit.text}");
    var lvPushContent = new LVPushContent();
    lvPushContent.title = "push title";
    lvPushContent.body = "push content";
    lvPushContent.extra = "push extra";
    lvPushContent.click_action = "FLUTTER_NOTIFICATION_CLICK";
    RtmFlutterPlugin.sendPrivateMessage(
            LVIMEnumDefine.IM_SUBTYPE_TEXT,
            _msgTargetIdTextEdit.text,
            "msgType",
            _msgInputTextEdit.text,
            lvPushContent)
        .then((lvimMsg) {
      print(
          '[Wing] sendPrivateMessage end result = ${lvimMsg.eCode}, dbid = ${lvimMsg.msg.dbid}, sendState = ${lvimMsg.msg.sendState} stime = ${lvimMsg.msg.stime}');
      print(
          'sendPrivateMessage 回调成功');
      if (lvimMsg.eCode == 0) {
        Fluttertoast.showToast(msg: "Send Msg Success");
      } else {
        Fluttertoast.showToast(msg: "Send Msg Failed code:${lvimMsg.eCode}");
      }
//          resend(lvimMsg.msg);
    });
//    RtmFlutterPlugin.setPrivateDBStorageMax(5);
  }

  // 消息重发
  void resend(LVIMMsg msg) {
    RtmFlutterPlugin.sendMessage(msg).then((lvimMsg) {
      print(
          '[Wing] sendMessage end result = ${lvimMsg.eCode}, dbid = ${lvimMsg.msg.dbid}, sendState = ${lvimMsg.msg.sendState} stime = ${lvimMsg.msg.stime}');
      if (lvimMsg.eCode == 0) {
        Fluttertoast.showToast(msg: "Send Msg Success");
      } else {
        Fluttertoast.showToast(msg: "Send Msg Failed code:${lvimMsg.eCode}");
      }
    });
  }

  void onClickWatchLive() async {
    print("onClickWatchLive");

    bool isCan = await canLive();
    if (!isCan) return;

    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) =>
                LivePage(false, _roomIdFieldController.text, userId)));
  }

  void onClickStartLive() async {
    print("onClickStartLive");
    bool isCan = await canLive();
    if (!isCan) return;

    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) =>
                LivePage(true, _roomIdFieldController.text, userId)));
  }

  Future<bool> canLive() async {
    // 1. 相机权限
    bool canLive = await checkPermission();
    print("canlive = $canLive");
    if (!canLive) return canLive;

    // 2. 是否输入了房间id
    if (_roomIdFieldController.text.length == 0) {
      LinkvUi.showAlert(context, "Please enter your room number!");
      return false;
    }

    // 3. SDK是否已经初始化成功了
    if (!_isSDKInit) {
      initSDK();
      return false;
    }

    // 4. IM是否鉴权通过
    if (!isIMAuthed) {
      Fluttertoast.showToast(
          msg: "请等待鉴权成功后进入房间",
          toastLength: Toast.LENGTH_SHORT,
          gravity: ToastGravity.CENTER);
      return false;
    }

    return true;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(
          title: Text("uid :$userId"),
        ),
        body: SafeArea(
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onTap: () => FocusScope.of(context).requestFocus(new FocusNode()),
            child: Container(
              padding: const EdgeInsets.only(left: 20, top: 20, right: 20),
              child: ListView(
                children: <Widget>[
                  Row(
                    children: <Widget>[
                      Text("Version: "),
                      Expanded(
                        child: Text("$_version"),
                      )
                    ],
                  ),
                  TextButton(
                    onPressed: () {
                      if (_msgTargetIdTextEdit.text.length == 0) {
                        Fluttertoast.showToast(msg: "please target user id");
                        return;
                      }
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                              builder: (ctx) => IMMediaMsgPage(
                                    userid: userId,
                                    targetUid: _msgTargetIdTextEdit.text,
                                  )));
                    },
                    child: Text("测试IM私信"),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  TextField(
                    controller: _msgInputTextEdit,
                    decoration: InputDecoration(
                        contentPadding: const EdgeInsets.only(
                            left: 10, top: 12, bottom: 12),
                        hintText: "Please enter message Content",
                        enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey)),
                        focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.cyan))),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  TextField(
                    controller: _msgTargetIdTextEdit,
                    decoration: InputDecoration(
                        contentPadding: const EdgeInsets.only(
                            left: 10, top: 12, bottom: 12),
                        hintText: "Please enter target user id",
                        enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey)),
                        focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.cyan))),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  TextField(
                    controller: _msgListTextEdit,
                    decoration: InputDecoration(
                        contentPadding: const EdgeInsets.only(
                            left: 10, top: 12, bottom: 12),
                        hintText: "received message",
                        enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.cyan)),
                        focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.cyan))),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Container(
                    height: 50,
                    width: MediaQuery.of(context).size.width - 20,
                    color: Colors.cyan,
                    child: CupertinoButton(
                      child: Text(
                        "Send Private Message",
                        style: TextStyle(color: Colors.white),
                      ),
                      onPressed: onClickSend,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Row(
                    children: <Widget>[
                      Container(
                        height: 50,
                        width: (MediaQuery.of(context).size.width - 60) * 0.5,
                        color: Colors.cyan,
                        child: CupertinoButton(
                            child: Text(
                              "History Msg",
                              style: TextStyle(color: Colors.white),
                            ),
                            onPressed: onClickHistoryMsg),
                      ),
                      Container(
                        width: 20,
                      ),
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "Session List",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: onClickSessionList)),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Row(
                    children: <Widget>[
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "Clear 1234 Unread",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: onClear1234Unread)),
                      Container(
                        width: 20,
                      ),
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "Clear All Unread",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: onClearAllUnread)),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Row(
                    children: <Widget>[
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "delete chat",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: onDeleteChat)),
                      Container(
                        width: 20,
                      ),
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "send read receipt",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: sendMessageHasRead)),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Row(
                    children: <Widget>[
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "send event msg",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: sendEventMsg)),
                      Container(
                        width: 20,
                      ),
                      Container(
                          height: 50,
                          width: (MediaQuery.of(context).size.width - 60) * 0.5,
                          color: Colors.cyan,
                          child: CupertinoButton(
                              child: Text(
                                "get unread count",
                                style: TextStyle(color: Colors.white),
                              ),
                              onPressed: queryUnreadCount)),
                    ],
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 50),
                  ),
                  TextField(
                    controller: _roomIdFieldController,
                    decoration: InputDecoration(
                        contentPadding: const EdgeInsets.only(
                            left: 10, top: 12, bottom: 12),
                        hintText: "Please enter room_id",
                        enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.grey)),
                        focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.cyan))),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Container(
                    height: 50,
                    width: MediaQuery.of(context).size.width - 20,
                    color: Colors.cyan,
                    child: CupertinoButton(
                      child: Text(
                        "Start Live",
                        style: TextStyle(color: Colors.white),
                      ),
                      onPressed: onClickStartLive,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                  ),
                  Container(
                    height: 50,
                    width: MediaQuery.of(context).size.width - 20,
                    color: Colors.cyan,
                    child: CupertinoButton(
                      child: Text(
                        "Watch Live",
                        style: TextStyle(color: Colors.white),
                      ),
                      onPressed: onClickWatchLive,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

// MARK: - Permission
  Future<bool> checkPermission() async {
    Permission _camera = Permission.camera;
    Permission _microphone = Permission.microphone;

    PermissionStatus status;
    status = await _camera.request();
    if (status != PermissionStatus.granted) return false;
    status = await _microphone.request();
    if (status != PermissionStatus.granted) return false;

    return true;
  }

  @override
  void onIMMessageReceive(var msg) {
    var msgContent = msg.msgContent.toString();
    print(TAG + " onIMMessageReceive  fromid = " + msg.fromID);
    _msgListTextEdit.text = msg.fromID + " : " + msgContent;
    // 发送该消息已读
    RtmFlutterPlugin.sendMessageHasRead(msg.fromID, msg.stime);
  }

  @override
  void onEventMessageReceive(var msg) {
    print(TAG +
        " onEventMessageReceive content = ${msg.msgContent}   type = ${msg.extend1} ");
  }

  @override
  void onReadMessageReceive(var msg, int stime) {
    // stime之前的消息皆可设为已读
    print(TAG + "onReadMessageReceive : $stime");

    _msgListTextEdit.text = msg.fromID + " : 已读时间戳为 $stime 的消息";
  }

  @override
  void onIMTokenExpired(String uid, String token) {
    isIMAuthed = false;
    print(TAG + "onIMTokenExpired uid = " + uid);
  }

  @override
  void onIMAuthSucceed(String uid, String token, int unReadMsgSize) {
    isIMAuthed = true;
    print(TAG + "onIMAuthSucceed uid = " + uid + " token = " + token);
    Fluttertoast.showToast(
        msg: "IM 鉴权成功",
        toastLength: Toast.LENGTH_LONG,
        gravity: ToastGravity.CENTER);
  }

  @override
  void onIMAuthFailed(
      String uid, String token, int ecode, int rcode, bool isTokenExpired) {
    isIMAuthed = false;
    print(TAG + "onIMAuthFailed uid = " + uid + " ecode =$ecode ");
  }

  @override
  void onQueryIMToken() {
    requestReleaseIMToken();
//    requestDebugIMToken();
  }

  void requestDebugIMToken() {
    if (isTokenRequesting) return;

    print(TAG + "requestDebugToken start");
    isTokenRequesting = true;
    RtmFlutterPlugin.requestDebugToken(userId, "Test").then((result) {
      isTokenRequesting = false;
      int code = result["code"];
      String resultMsg = result["result"];
      print(
          TAG + "requestDebugToken end msg  = " + resultMsg + " code = $code");

      /// 获取debug token 成功
      if (code == 0) {
        RtmFlutterPlugin.setIMToken(userId, resultMsg);
      }
    });
  }

  void requestReleaseIMToken() {
    if (isTokenRequesting) return;

    print(TAG + "requestReleaseIMToken start userId = $userId");
    isTokenRequesting = true;
    Help.requestXMYIMToken(userId).then((value) {
      isTokenRequesting = true;
      if (value["status"] == "200" && value["data"]["code"] == "200") {
        print(TAG + "setIMToken token = ${value["data"]["token"]}");
        print(TAG + "setIMToken userId = ${userId}");
        RtmFlutterPlugin.setIMToken(userId, value["data"]["token"]);
      }
      print(TAG + "requestReleaseIMToken end data = $value");
    });

    // RtmFlutterPlugin.setIMToken(userId, "xxxx");
  }

  @override
  void onIMMessageReadedAcks(List<int> listId) {
    // TODO: implement onIMMessageReadedAcks
  }

  @override
  void onIMUserKicked(String uid, int code) {
    // TODO: implement onIMUserKicked
  }

  @override
  void onIMConnected() {
    // TODO: implement onIMConnected
    Fluttertoast.showToast(
        msg: "onIMConnected()方法被调用,网络连接",
        toastLength: Toast.LENGTH_LONG,
        gravity: ToastGravity.CENTER);
  }

  @override
  void onIMLosted() {
    // TODO: implement onIMLosted
    Fluttertoast.showToast(
        msg: "onIMLosted()方法被调用,网络断开",
        toastLength: Toast.LENGTH_LONG,
        gravity: ToastGravity.CENTER);
  }

  @override
  void onIMConnectState(int connectState) {
    // TODO: implement onIMConnectState
    print("onIMConnectState()方法被调用,connectState = $connectState");
    Fluttertoast.showToast(
        msg: "onIMConnectState()方法被调用,connectState = $connectState",
        toastLength: Toast.LENGTH_LONG,
        gravity: ToastGravity.CENTER);
  }

  @override
  void onIMSendMessageReceive(msg) {


  }

}
0
likes
25
pub points
0%
popularity

Publisher

unverified uploader

linkv_rtc_im Audio/Video/IM Flutter SDK is a flutter plugin wrapper based on LinkV LVIMSDK native Android/iOS SDK

Homepage

License

MIT (LICENSE)

Dependencies

ffi, flutter

More

Packages that depend on linkv_rtc_im