Flutter iOS CallKit集成插件ios_callkit的使用

Flutter iOS CallKit集成插件ios_callkit的使用

动机

我们使用CallKit来处理iOS 13以上的VoIP通知。查看WWDC2019视频了解更多信息。因此,我们需要将CallKit和PushKit一起使用。尽管网络上有一些VoIP通知的示例,但专门针对Flutter的很少。我决定创建一个flutter插件,包含所需的最小功能。你可以使用这个插件,但实际目的是帮助你根据自己的服务创建一个定制的VoIPKit。

需求

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

使用方法

1. 安装

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

dependencies:
  ios_callkit: ^x.x.x

2. 在Xcode中设置Capability

  1. 选择背景模式 > 语音过IP和远程通知为开启。
  2. 选择推送通知。
  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>AppIconName</string>
<key>FIVKLocalizedName</key>
<string>ios_callkit</string>
<key>FIVKSkipRecallScreen</key>
<true/>

4. 为CallKit添加新的图像集

ios/Runner/Assets.xcassets/AppIcon-VoIPKit目录下添加一个图标(.png.pdf),用于锁屏时来电显示。

5. 创建VoIP服务证书

  1. 访问Apple开发者网站并创建一个新的VoIP服务证书(.cer)。更多信息请查看此文档
  2. 使用KeyChainAccess从.cer生成.p12文件,并使用openssl生成.pem文件。

6. 从你的服务器请求VoIP通知APNs

查看Apple文档以了解更多信息。

示例数据(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>",
          "videoType": true
        }
    }
}

可以使用curl测试VoIP通知,如下所示:

curl -v \
-d '{"aps":{"alert":{"uuid":"982cf533-7b1b-4cf6-a6e0-004aab68c503","incoming_caller_id":"0123456789","incoming_caller_name":"Tester","videoType":true}}}' \
-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>

示例代码

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:ios_callkit/ios_callkit.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(() {
    IOSCallKit.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](/user/override)
  _SelectCallRollState createState() => _SelectCallRollState();
}

class _SelectCallRollState extends State<SelectCallRoll> {
  void _performExampleAction(ExampleAction action) async {
    switch (action) {
      case ExampleAction.RequestAuthorization:
        final granted = await IOSCallKit.instance.requestAuthLocalNotification();
        print('🎈 example: requestAuthLocalNotification granted = $granted');
        break;
      case ExampleAction.GetSettings:
        final settings = await IOSCallKit.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](/user/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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

更多关于Flutter iOS CallKit集成插件ios_callkit的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter iOS CallKit集成插件ios_callkit的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter项目中集成iOS的CallKit功能,可以使用ios_callkit插件。下面是一个示例代码,展示了如何在Flutter应用中集成并使用ios_callkit插件来实现基本的CallKit功能。

1. 添加依赖

首先,在你的pubspec.yaml文件中添加ios_callkit依赖:

dependencies:
  flutter:
    sdk: flutter
  ios_callkit: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

2. 配置iOS项目

由于ios_callkit是iOS特定的插件,你需要在Xcode中进行一些配置。

  • 打开Xcode,选择你的Runner项目。
  • Info.plist中添加必要的权限配置,比如NSMicrophoneUsageDescriptionNSCameraUsageDescription(如果你的应用需要这些权限)。
  • 确保你的应用具有VoIP后台模式权限。在Capabilities标签页中,启用Background Modes并勾选Voice over IP

3. 编写Flutter代码

下面是一个简单的Flutter应用示例,展示如何使用ios_callkit插件:

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

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

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

class _MyAppState extends State<MyApp> {
  CXProvider? provider;

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

  @override
  void dispose() {
    provider?.reportEndCall(withUUID: 'your-call-uuid', reason: CXEndCallReasonEnded);
    provider = null;
    super.dispose();
  }

  Future<void> _initCallKit() async {
    if (Platform.isIOS) {
      provider = await CXProvider.init(
        configuration: CXProviderConfiguration(
          localizedName: 'Your App Name',
          supportsVideo: false,
          maximumCallsPerCallGroup: 1,
          supportsReinviting: false,
          supportsHolding: false,
          supportsGrouping: false,
          supportsUngrouping: false,
        ),
      );

      provider?.setDelegate(CXProviderDelegate(
        provider: provider!,
        performAnswerCallAction: (CXAnswerCallAction action) async {
          // Handle answer call action
          action.fulfill();
        },
        performEndCallAction: (CXEndCallAction action) async {
          // Handle end call action
          action.fulfill();
        },
        providerDidReset: () {
          // Handle provider reset
        },
      ));
    }
  }

  Future<void> _startCall() async {
    if (provider != null) {
      var uuid = UUID().v4(); // Generate a unique UUID for the call
      var handle = CXHandle(type: CXHandleType.generic, value: 'caller-id');
      var update = CXCallUpdate();
      update.remoteHandle = handle;
      update.hasVideo = false;

      provider!.reportNewIncomingCall(
        withUUID: uuid,
        update: update,
        completion: (CXError? error) {
          if (error == null) {
            // Call successfully reported
          } else {
            // Handle error
          }
        },
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('CallKit Demo'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: _startCall,
            child: Text('Start Call'),
          ),
        ),
      ),
    );
  }
}

4. UUID生成

在上面的代码中,我们使用了UUID().v4()来生成一个唯一的UUID。你需要确保在你的项目中引入了uuid包。在pubspec.yaml中添加依赖:

dependencies:
  uuid: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get

5. 运行应用

现在你可以运行你的Flutter应用,点击“Start Call”按钮来模拟一个来电,并观察CallKit的集成效果。

请注意,这只是一个简单的示例,实际应用中你可能需要处理更多的逻辑,比如来电界面的自定义、来电铃声的设置、VoIP推送的配置等。详细的信息可以参考ios_callkit插件的官方文档和Apple的CallKit文档。

回到顶部