Flutter VoIP通信插件flutter_ios_voip_kit的使用

发布于 1周前 作者 yibo5220 来自 Flutter

Flutter VoIP通信插件flutter_ios_voip_kit的使用

简介

flutter_ios_voip_kit 是一个用于在 Flutter iOS 应用中处理 VoIP 通知的插件。它利用了 CallKit 和 PushKit 来实现一对一视频通话。本文档将详细介绍如何安装和使用该插件。

动机

我们需要在 iOS 13 及以上版本中使用 CallKit 来处理传入的 VoIP 通知。查看 WWDC2019 视频以获取更多信息。因此,将 CallKit 和 PushKit 结合使用的需求越来越多。然而,网络上关于使用 CallKit 和 PushKit 的 VoIP 通知样本仍然很少(特别是针对 Flutter)。我决定创建一个包含最小必要功能的 Flutter 插件。你可以使用这个插件,但实际目的是帮助你创建适合你服务的 VoIPKit。

要求

  • 仅支持 iOS,不支持 Android。
  • 支持 iOS 10 及以上版本。
  • 仅支持一对一通话,不支持群组通话。
  • 需要一个服务器来通过 APNs 推送 VoIP 通知。
  • 实际进行视频或通话时,需要链接到类似 WebRTC 的服务(例如:Agora, SkyWay, Amazon Kinesis Video Streams)。

使用方法

1. 安装

在你的 pubspec.yaml 文件中添加 flutter_ios_voip_kit 作为依赖项:

dependencies:
  flutter_ios_voip_kit: ^latest_version

2. 在 Xcode 中设置能力

  1. 选择 Background Modes > Voice over IP 和 Remote notifications。
  2. 选择 Push Notifications。
  3. 在选择了能力后,编辑 ios/Runner/Info.plist 文件:
<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
    <string>voip</string>
</array>

3. 编辑 Info.plist

编辑 ios/Runner/Info.plist 文件,添加以下内容:

<key>FIVKIconName</key>
<string>AppIcon-VoIPKit</string>
<key>FIVKLocalizedName</key>
<string>VoIP-Kit</string>
<key>FIVKSupportVideo</key>
<true/>
<key>FIVKSkipRecallScreen</key>
<true/>

4. 为 CallKit 添加新的图像集

将图标(.png 或 .pdf)添加到 ios/Runner/Assets.xcassets/AppIcon-VoIPKit,以便在锁定的 iPhone 上显示通话时使用。

5. 创建 VoIP 服务证书

  1. 访问 Apple Developer 并创建一个新的 VoIP 服务证书(.cer)。查看更多信息
  2. 使用 KeyChainAccess 从 .cer 创建 .p12,并使用 openssl 创建 .pem:
openssl pkcs12 -in voip_services.p12 -out voip_services.pem -nodes -clcerts

6. 从服务器请求 VoIP 通知(APNs)

  1. 查看 Apple 文档。
  2. 发送通知请求到 APNs
  3. 添加数据(payload)如下:
{
    "aps": {
        "alert": {
          "uuid": "<Version 4 UUID (e.g.: https://www.uuidgenerator.net/version4)>",
          "incoming_caller_id": "<your service user id>",
          "incoming_caller_name": "<your service user name>",
        }
    }
}
  1. 使用 curl 测试 VoIP 通知:
curl -v \
-d '{"aps":{"alert":{"uuid":"982cf533-7b1b-4cf6-a6e0-004aab68c503","incoming_caller_id":"0123456789","incoming_caller_name":"Tester"}}}' \
-H "apns-push-type: voip" \
-H "apns-expiration: 0" \
-H "apns-priority: 0" \
-H "apns-topic: <your app’s bundle ID>.voip" \
--http2 \
--cert ./voip_services.pem \
https://api.sandbox.push.apple.com/3/device/<VoIP device Token for your iPhone>

尝试示例应用

你可以尝试示例应用,而无需服务器。这里有一个使用 SkyWay 确认 flutter_ios_voip_kit 运行的示例

示例代码

以下是一个简单的示例代码,展示了如何使用 flutter_ios_voip_kit

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_ios_voip_kit/flutter_ios_voip_kit.dart';

import 'incoming_call_page.dart';
import 'outgoing_call_page.dart';

enum ExampleAction { RequestAuthorization, GetSettings }

extension on ExampleAction {
  String get title {
    switch (this) {
      case ExampleAction.RequestAuthorization:
        return 'Authorize Notifications';
      case ExampleAction.GetSettings:
        return 'Check Settings';
      default:
        return 'Unknown';
    }
  }
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runZonedGuarded(() {
    FlutterIOSVoIPKit.instance.onDidUpdatePushToken = (token) {
      print('🎈 example: onDidUpdatePushToken token = $token');
    };
    runApp(MaterialApp(
      routes: <String, WidgetBuilder>{
        OutgoingCallPage.routeName: (_) => OutgoingCallPage(),
        IncomingCallPage.routeName: (_) => IncomingCallPage(),
      },
      home: SelectCallRoll(),
    ));
  }, (object, stackTrace) {});
}

class SelectCallRoll extends StatefulWidget {
  @override
  _SelectCallRollState createState() => _SelectCallRollState();
}

class _SelectCallRollState extends State<SelectCallRoll> {
  void _performExampleAction(ExampleAction action) async {
    switch (action) {
      case ExampleAction.RequestAuthorization:
        final granted = await FlutterIOSVoIPKit.instance.requestAuthLocalNotification();
        print('🎈 example: requestAuthLocalNotification granted = $granted');
        break;
      case ExampleAction.GetSettings:
        final settings = await FlutterIOSVoIPKit.instance.getLocalNotificationsSettings();
        print('🎈 example: getLocalNotificationsSettings settings: \n$settings');

        showDialog(
            context: context,
            builder: (ctx) {
              return AlertDialog(
                title: Text('Settings'),
                content: Text('$settings'),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.of(ctx).pop(),
                    child: Text('Ok'),
                  )
                ],
              );
            });
        break;
      default:
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Select call roll'),
        actions: [
          PopupMenuButton<ExampleAction>(
            icon: Icon(Icons.more_vert),
            onSelected: (action) => _performExampleAction(action),
            itemBuilder: (BuildContext context) {
              return ExampleAction.values.map((ExampleAction choice) {
                return PopupMenuItem<ExampleAction>(
                  value: choice,
                  child: Text(choice.title),
                );
              }).toList();
            },
          ),
        ],
      ),
      body: SafeArea(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 18),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                const Text(
                  '📱 To try out the example app, you need two iPhones with iOS 10 or later.',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    height: 1.5,
                    fontWeight: FontWeight.bold,
                    fontSize: 18,
                  ),
                ),
                _button(isCaller: true),
                _button(isCaller: false),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _button({
    @required bool isCaller,
  }) {
    return SizedBox(
      width: 140,
      height: 140,
      child: RawMaterialButton(
        padding: EdgeInsets.zero,
        elevation: 8.0,
        shape: CircleBorder(),
        fillColor: Colors.blue,
        onPressed: () {
          Navigator.pushNamed(
            context,
            isCaller ? OutgoingCallPage.routeName : IncomingCallPage.routeName,
          );
        },
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            Icon(
              isCaller ? Icons.call : Icons.ring_volume,
              size: 32,
            ),
            Text(
              isCaller ? '🤙 Caller' : '🔔 Callee',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 18,
                color: Colors.white,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

常见问题

CallKit 是否有呼叫和拨出呼叫屏幕?

  • 不。CallKit 仅支持来电屏幕。你需要自己制作呼叫和拨出呼叫屏幕。

我可以使用远程推送设备令牌而不是 VoIP 设备令牌吗?

  • 不可以。由于 VoIP 令牌和推送令牌的规格不同,需要在数据库中分别管理它们。

在 iOS 13 上无法获取 VoIP 令牌

  • 请卸载应用,重启终端并重新安装应用。稍后即可获取。

无法接收 VoIP 通知

  • 请检查以下项目:
    1. VoIP 设备令牌是否正确?
    2. 是否设置了带有 .voip 的应用包标识符作为 apns-topic?
    3. 是否设置了 voip 作为 apns-push-type?
    4. APNs 终端(开发或生产)是否正确?
    5. 对于 iOS 13,如果 CallKit 呼叫多次失败,可能无法接收 VoIP 通知。请卸载应用,重启终端并重新安装应用。

在锁定屏幕上没有显示来电图标

  • 图标图像应为边长为 40 点的正方形。颜色被忽略,请使用不同的 alpha 值设计。
  • 如果使用 PDF 创建,请勾选 Preserve Vector Data 以调整大小,并将 Single Scale 更改为 Scales

参考资料


更多关于Flutter VoIP通信插件flutter_ios_voip_kit的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter VoIP通信插件flutter_ios_voip_kit的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 flutter_ios_voip_kit 插件进行 VoIP 通信的示例代码。这个插件主要用于在 Flutter 应用中实现 iOS 平台上的 VoIP 功能。请注意,这只是一个基本的示例,实际项目中可能需要根据具体需求进行调整。

前提条件

  1. 确保你的 Flutter 环境已经配置好。
  2. pubspec.yaml 文件中添加 flutter_ios_voip_kit 依赖:
dependencies:
  flutter:
    sdk: flutter
  flutter_ios_voip_kit: ^最新版本号
  1. 运行 flutter pub get 命令来安装依赖。

配置 iOS 项目

  1. 打开 ios/Runner/Info.plist 文件,添加必要的权限配置,比如麦克风权限和通知权限。
<key>NSMicrophoneUsageDescription</key>
<string>App需要访问麦克风以进行VoIP通话</string>
<key>UIBackgroundModes</key>
<array>
    <string>voip</string>
</array>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
  1. 确保在 Xcode 中启用了 Background Modes 中的 VoIP 选项。

代码示例

1. 初始化 VoIPKit

在你的 Flutter 应用中,首先进行初始化:

import 'package:flutter/material.dart';
import 'package:flutter_ios_voip_kit/flutter_ios_voip_kit.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('VoIP Demo'),
        ),
        body: VoIPDemoPage(),
      ),
    );
  }
}

class VoIPDemoPage extends StatefulWidget {
  @override
  _VoIPDemoPageState createState() => _VoIPDemoPageState();
}

class _VoIPDemoPageState extends State<VoIPDemoPage> {
  VoIPKit? _voipKit;

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

  Future<void> initVoIPKit() async {
    _voipKit = VoIPKit();

    // 注册VoIP通知
    _voipKit!.registerVoIPNotifications()!
        .then((_) => print('VoIP notifications registered'))
        .catchError((error) => print('Error registering VoIP notifications: $error'));

    // 监听来电
    _voipKit!.onIncomingCall!.listen((call) {
      print('Incoming call: ${call.uuid}');
      // 在这里处理来电逻辑,比如显示来电界面
    });

    // 监听挂断
    _voipKit!.onCallEnded!.listen((call) {
      print('Call ended: ${call.uuid}');
      // 在这里处理挂断逻辑
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        onPressed: () {
          // 发起VoIP呼叫的示例(需要后端支持)
          // _voipKit!.startCall(to: 'callee_sip_address');
        },
        child: Text('发起VoIP呼叫'),
      ),
    );
  }
}

2. 处理来电通知

在 iOS 上,VoIP 通知需要特别处理。你需要配置一个 PushKit 服务来处理这些通知。由于 Flutter 本身不直接处理原生代码,这部分通常需要在原生 iOS 代码中进行。不过,flutter_ios_voip_kit 插件已经为你封装好了大部分逻辑,你只需要确保在 AppDelegate 中进行必要的配置。

打开 ios/Runner/AppDelegate.swift 文件,确保有以下代码:

import UIKit
import Flutter
import PushKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    // 配置 PushKit
    let voipRegistry = PKPushRegistry(queue: .main)
    voipRegistry.delegate = self
    voipRegistry.desiredPushTypes = [PKPushType.voIP]
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

// 扩展 AppDelegate 以遵守 PKPushRegistryDelegate 协议
extension AppDelegate: PKPushRegistryDelegate {
  func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) {
    if type == .voIP {
      // 处理VoIP通知
      let uuidString = payload.dictionaryPayload["uuid"] as? String ?? UUID().uuidString
      let voipKitPlugin = FlutterVoIPKitPlugin.sharedInstance()
      voipKitPlugin.reportIncomingCall(with: uuidString, payload: payload.dictionaryPayload)
    }
  }
}

注意事项

  1. 后端支持:VoIP 功能通常需要后端服务器的支持,用于处理 SIP 信令和媒体流。
  2. 通知处理:确保你的应用能够正确处理和处理 VoIP 通知,特别是在后台运行时。
  3. 测试环境:在实际部署之前,在测试环境中充分测试 VoIP 功能,以确保其稳定性和可靠性。

这个示例提供了一个基本的框架,你可以根据具体需求进行扩展和修改。希望这对你有帮助!

回到顶部