From 57c31995ff20b1981826c4bcc9856ea0aa17e6a8 Mon Sep 17 00:00:00 2001 From: VladimirBrejcha Date: Wed, 14 Oct 2020 18:01:34 +0300 Subject: [PATCH] Voximplant Flutter SDK 2.4.0 --- CHANGELOG.md | 5 + android/build.gradle | 4 +- .../flutter_voximplant/AudioFileManager.java | 136 ++++++++++++ .../flutter_voximplant/AudioFileModule.java | 118 +++++++++++ .../voximplant/flutter_voximplant/Utils.java | 22 ++ .../flutter_voximplant/VoximplantErrors.java | 1 + .../flutter_voximplant/VoximplantPlugin.java | 9 +- example/.gitignore | 1 + example/ios/Podfile | 79 ++----- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Classes/VIAudioFileManager.h | 16 ++ ios/Classes/VIAudioFileManager.m | 133 ++++++++++++ ios/Classes/VIAudioFileModule.h | 16 ++ ios/Classes/VIAudioFileModule.m | 110 ++++++++++ ios/Classes/VICameraModule.m | 4 +- ios/Classes/VoximplantCallManager.m | 1 + ios/Classes/VoximplantPlugin.m | 9 +- ios/Classes/VoximplantUtils.h | 2 + ios/Classes/VoximplantUtils.m | 24 +++ ios/flutter_voximplant.podspec | 4 +- lib/flutter_voximplant.dart | 1 + lib/src/client/client.dart | 2 + lib/src/error_codes.dart | 19 ++ lib/src/hardware/audio_file.dart | 193 ++++++++++++++++++ lib/src/messaging/messenger.dart | 2 +- pubspec.yaml | 2 +- 26 files changed, 842 insertions(+), 77 deletions(-) create mode 100644 android/src/main/java/com/voximplant/flutter_voximplant/AudioFileManager.java create mode 100644 android/src/main/java/com/voximplant/flutter_voximplant/AudioFileModule.java create mode 100644 ios/Classes/VIAudioFileManager.h create mode 100644 ios/Classes/VIAudioFileManager.m create mode 100644 ios/Classes/VIAudioFileModule.h create mode 100644 ios/Classes/VIAudioFileModule.m create mode 100644 lib/src/hardware/audio_file.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c37d183..5b2300b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.4.0 +* Update Android and iOS platform code to use Voximplant Android SDK 2.20.4 + and Voximplant iOS SDK 2.34.3 +* Introduce VIAudioFile API + ## 2.3.0 * Update Android and iOS platform code to use Voximplant Android SDK 2.19.0 and Voximplant iOS SDK 2.33.0 diff --git a/android/build.gradle b/android/build.gradle index 5c75dc7..543331f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ group 'com.voximplant.flutter_voximplant' -version '2.3.0' +version '2.4.0' buildscript { repositories { @@ -38,6 +38,6 @@ android { } dependencies { - api 'com.voximplant:voximplant-sdk:2.19.0' + api 'com.voximplant:voximplant-sdk:2.20.4' } } diff --git a/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileManager.java b/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileManager.java new file mode 100644 index 0000000..2a956bc --- /dev/null +++ b/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileManager.java @@ -0,0 +1,136 @@ +package com.voximplant.flutter_voximplant; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.voximplant.sdk.Voximplant; +import com.voximplant.sdk.hardware.IAudioFile; +import com.voximplant.sdk.hardware.IAudioFileListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class AudioFileManager { + private final Map mAudioFileModules; + private Handler mHandler = new Handler(Looper.getMainLooper()); + private final BinaryMessenger mMessenger; + private final Context mAppContext; + + AudioFileManager(BinaryMessenger messenger, Context context) { + this.mMessenger = messenger; + this.mAudioFileModules = new HashMap<>(); + this.mAppContext = context; + } + + void handleMethodCall(MethodCall call, MethodChannel.Result result) { + if (call.arguments == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": Invalid arguments", null)); + return; + } + switch (call.method) { + case "initWithFile": + initWithFile(call, result); + break; + case "loadFile": + loadFile(call, result); + break; + case "releaseResources": + releaseResources(call, result); + break; + default: + handleInModule(call, result); + break; + } + } + + void initWithFile(MethodCall call, MethodChannel.Result result) { + String fileName = call.argument("name"); + if (fileName == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": name is null", null)); + return; + } + String fileUsage = call.argument("usage"); + if (fileUsage == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": usage is null", null)); + return; + } + int rawId = mAppContext.getResources().getIdentifier(fileName, "raw", mAppContext.getPackageName()); + IAudioFile audioFile = Voximplant.createAudioFile(mAppContext, rawId, Utils.convertStringToAudioFileUsage(fileUsage)); + if (audioFile == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": failed to locate audio file", null)); + return; + } + String fileId = UUID.randomUUID().toString(); + AudioFileModule module = new AudioFileModule(mMessenger, audioFile, fileId, null); + mAudioFileModules.put(fileId, module); + mHandler.post(() -> result.success(fileId)); + } + + void loadFile(MethodCall call, MethodChannel.Result result) { + String fileUrl = call.argument("url"); + if (fileUrl == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": url is null", null)); + return; + } + String fileUsage = call.argument("usage"); + if (fileUsage == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": usage is null", null)); + return; + } + IAudioFile audioFile = Voximplant.createAudioFile(fileUrl, Utils.convertStringToAudioFileUsage(fileUsage)); + if (audioFile == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": failed to load audio file", null)); + return; + } + String fileId = UUID.randomUUID().toString(); + AudioFileModule module = new AudioFileModule(mMessenger, audioFile, fileId, result); + mAudioFileModules.put(fileId, module); + } + + void releaseResources(MethodCall call, MethodChannel.Result result) { + String fileId = call.argument("fileId"); + if (fileId == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": fileId is null", null)); + return; + } + if (!mAudioFileModules.containsKey(fileId)) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": could'nt find audioFile", null)); + return; + } + AudioFileModule module = mAudioFileModules.get(fileId); + if (module == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": could'nt find audioFile", null)); + return; + } + if (module.mAudioFile != null) { + module.mAudioFile.setAudioFileListener(null); + module.mAudioFile.release(); + } + mAudioFileModules.remove(fileId); + mHandler.post(() -> result.success(null)); + } + + void handleInModule(MethodCall call, MethodChannel.Result result) { + String fileId = call.argument("fileId"); + if (fileId == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": fileId is null", null)); + return; + } + if (!mAudioFileModules.containsKey(fileId)) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": could'nt find audioFile", null)); + return; + } + AudioFileModule module = mAudioFileModules.get(fileId); + if (module == null) { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": could'nt find audioFile", null)); + return; + } + module.handleMethodCall(call, result); + } +} diff --git a/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileModule.java b/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileModule.java new file mode 100644 index 0000000..4942b42 --- /dev/null +++ b/android/src/main/java/com/voximplant/flutter_voximplant/AudioFileModule.java @@ -0,0 +1,118 @@ +package com.voximplant.flutter_voximplant; + +import android.os.Handler; +import android.os.Looper; + +import com.voximplant.sdk.hardware.IAudioFile; +import com.voximplant.sdk.hardware.IAudioFileListener; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class AudioFileModule implements EventChannel.StreamHandler, IAudioFileListener { + final IAudioFile mAudioFile; + private final String mFileId; + private EventChannel mEventChannel; + private EventChannel.EventSink mEventSink; + private Handler mHandler = new Handler(Looper.getMainLooper()); + private MethodChannel.Result mLoadFileCompletion; + private MethodChannel.Result mPlayCompletion; + private MethodChannel.Result mStopCompletion; + + AudioFileModule(BinaryMessenger messenger, IAudioFile file, String fileId, MethodChannel.Result loadFileCompletion) { + mLoadFileCompletion = loadFileCompletion; + mEventChannel = new EventChannel(messenger, "plugins.voximplant.com/audio_file_events_" + fileId); + mEventChannel.setStreamHandler(this); + mFileId = fileId; + mAudioFile = file; + mAudioFile.setAudioFileListener(this); + } + + void handleMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "play": + play(call, result); + break; + case "stop": + stop(result); + break; + default: + result.notImplemented(); + break; + } + } + + void play(MethodCall call, MethodChannel.Result result) { + Object looped = call.argument("looped"); + if (looped instanceof Boolean) { + if (mAudioFile != null) { + mPlayCompletion = result; + mAudioFile.play((Boolean)looped); + } + } else { + mHandler.post(() -> result.error(VoximplantErrors.ERROR_INVALID_ARGUMENTS, call.method + ": looped is null", null)); + } + } + + void stop(MethodChannel.Result result) { + if (mAudioFile != null) { + mStopCompletion = result; + mAudioFile.stop(false); + } + } + + @Override + public void onStart(IAudioFile audioFile) { + if (mPlayCompletion != null) { + mHandler.post(() -> { + mPlayCompletion.success(null); + mPlayCompletion = null; + }); + } + } + + @Override + public void onStop(IAudioFile audioFile) { + if (mStopCompletion != null) { + mHandler.post(() -> { + mStopCompletion.success(null); + mStopCompletion = null; + }); + } else if (mFileId != null) { + Map params = new HashMap<>(); + params.put("name", "didStopPlaying"); + sendEvent(params); + } + } + + @Override + public void onPrepared(IAudioFile audioFile) { + if (mLoadFileCompletion != null) { + mHandler.post(() -> { + mLoadFileCompletion.success(mFileId); + mLoadFileCompletion = null; + }); + } + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + mEventSink = events; + } + + @Override + public void onCancel(Object arguments) { + mEventSink = null; + } + + private void sendEvent(Map event) { + if (mEventSink != null) { + mHandler.post(() -> mEventSink.success(event)); + } + } +} diff --git a/android/src/main/java/com/voximplant/flutter_voximplant/Utils.java b/android/src/main/java/com/voximplant/flutter_voximplant/Utils.java index 62e63e4..fc21492 100644 --- a/android/src/main/java/com/voximplant/flutter_voximplant/Utils.java +++ b/android/src/main/java/com/voximplant/flutter_voximplant/Utils.java @@ -8,6 +8,7 @@ import com.voximplant.sdk.call.VideoCodec; import com.voximplant.sdk.call.VideoStreamType; import com.voximplant.sdk.client.LoginError; +import com.voximplant.sdk.hardware.AudioFileUsage; import com.voximplant.sdk.messaging.IErrorEvent; import static com.voximplant.flutter_voximplant.VoximplantErrors.ERROR_INTERNAL; @@ -65,6 +66,8 @@ static String convertLoginErrorToString(LoginError error) { return VoximplantErrors.ERROR_NETWORK_ISSUES; case TOKEN_EXPIRED: return VoximplantErrors.ERROR_TOKEN_EXPIRED; + case MAU_ACCESS_DENIED: + return VoximplantErrors.ERROR_MAU_ACCESS_DENIED; case INTERNAL_ERROR: default: return ERROR_INTERNAL; @@ -87,6 +90,8 @@ static String getErrorDescriptionForLoginError(LoginError error) { return "Connection to the Voximplant Cloud is closed due to network issues."; case TOKEN_EXPIRED: return "Token expired."; + case MAU_ACCESS_DENIED: + return "Monthly Active Users (MAU) limit is reached. Payment is required."; case INTERNAL_ERROR: default: return "Internal error."; @@ -238,4 +243,21 @@ static int convertVideoStreamTypeToInt(VideoStreamType type) { return 0; } } + + static AudioFileUsage convertStringToAudioFileUsage(String usage) { + if (usage == null) { + return AudioFileUsage.UNKNOWN; + } + switch (usage) { + case "incall": + return AudioFileUsage.IN_CALL; + case "notification": + return AudioFileUsage.NOTIFICATION; + case "ringtone": + return AudioFileUsage.RINGTONE; + case "unknown": + default: + return AudioFileUsage.UNKNOWN; + } + } } diff --git a/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantErrors.java b/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantErrors.java index a82b83a..50b6466 100644 --- a/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantErrors.java +++ b/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantErrors.java @@ -16,6 +16,7 @@ class VoximplantErrors { static final String ERROR_MEDIA_IS_ON_HOLD = "ERROR_MEDIA_IS_ON_HOLD"; static final String ERROR_ACCOUNT_FROZEN = "ERROR_ACCOUNT_FROZEN"; + static final String ERROR_MAU_ACCESS_DENIED = "ERROR_MAU_ACCESS_DENIED"; static final String ERROR_INVALID_PASSWORD = "ERROR_INVALID_PASSWORD"; static final String ERROR_INVALID_STATE = "ERROR_INVALID_STATE"; static final String ERROR_INVALID_USERNAME = "ERROR_INVALID_USERNAME"; diff --git a/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantPlugin.java b/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantPlugin.java index daddf14..1ba422f 100644 --- a/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantPlugin.java +++ b/android/src/main/java/com/voximplant/flutter_voximplant/VoximplantPlugin.java @@ -27,9 +27,10 @@ public class VoximplantPlugin implements MethodCallHandler, FlutterPlugin { private CallManager mCallManager; private CameraModule mCameraModule; private MessagingModule mMessagingModule; + private AudioFileManager mAudioFileManager; public VoximplantPlugin() { - Voximplant.subVersion = "flutter-2.3.0"; + Voximplant.subVersion = "flutter-2.4.0"; } private void configure(Context context, TextureRegistry textures, BinaryMessenger messenger) { @@ -40,6 +41,7 @@ private void configure(Context context, TextureRegistry textures, BinaryMessenge mCameraModule = new CameraModule(context); mChannel.setMethodCallHandler(this); mMessagingModule = new MessagingModule(messenger); + mAudioFileManager = new AudioFileManager(messenger, context); } public static void registerWith(Registrar registrar) { @@ -71,6 +73,8 @@ public void onMethodCall(MethodCall call, Result result) { String AUDIO_DEVICE = "AudioDevice"; String VIDEO_STREAM = "VideoStream"; String CAMERA = "Camera"; + String AUDIO_FILE = "AudioFile"; + if (isMethodCallOfType(MESSAGING, call)) { mMessagingModule.handleMethodCall(excludeMethodType(call), result); @@ -97,6 +101,9 @@ public void onMethodCall(MethodCall call, Result result) { } else if (isMethodCallOfType(CAMERA, call)) { mCameraModule.handleMethodCall(excludeMethodType(call), result); + } else if (isMethodCallOfType(AUDIO_FILE, call)) { + mAudioFileManager.handleMethodCall(excludeMethodType(call), result); + } else { result.notImplemented(); } diff --git a/example/.gitignore b/example/.gitignore index f897a32..162227e 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -29,6 +29,7 @@ .pub/ /build/ .flutter-plugins-dependencies +.last_build_id # Android related **/android/**/gradle-wrapper.jar diff --git a/example/ios/Podfile b/example/ios/Podfile index c28fd4e..f7d6a5e 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +# platform :ios, '9.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -10,78 +10,29 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end - end - generated_key_values -end - -target 'Runner' do - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - # Plugin Pods +flutter_ios_podfile_setup - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true - post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index fac2c39..77397bd 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -374,7 +374,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -537,7 +537,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -586,7 +586,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Classes/VIAudioFileManager.h b/ios/Classes/VIAudioFileManager.h new file mode 100644 index 0000000..e38b3a0 --- /dev/null +++ b/ios/Classes/VIAudioFileManager.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +#import "VoximplantPlugin.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface VIAudioFileManager: NSObject + +- (instancetype)initWithPlugin:(VoximplantPlugin *)plugin; +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Classes/VIAudioFileManager.m b/ios/Classes/VIAudioFileManager.m new file mode 100644 index 0000000..55281f1 --- /dev/null +++ b/ios/Classes/VIAudioFileManager.m @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +#import "VIAudioFileManager.h" +#import "VIAudioFileModule.h" + +@interface VIAudioFileManager () + +@property(nonatomic, strong) NSMutableDictionary *audioFileModules; +@property(nonatomic, weak) VoximplantPlugin *plugin; + +@end + + +@implementation VIAudioFileManager + +- (instancetype)initWithPlugin:(VoximplantPlugin *)plugin { + self = [super init]; + if (self) { + _plugin = plugin; + _audioFileModules = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (!call.arguments) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:[call.method stringByAppendingString:@": invalid arguments"] + details:nil]); + return; + } + + if ([@"initWithFile" isEqualToString:call.method]) { + NSString *fileName = call.arguments[@"name"]; + if (!fileName) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.initWithFile: name is null" + details:nil]); + return; + } + NSString *fileType = call.arguments[@"type"]; + if (!fileType) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.initWithFile: type is null" + details:nil]); + return; + } + NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType]; + if (!filePath) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.initWithFile: failed to locate audio file" + details:nil]); + return; + } + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + VIAudioFile *audioFile = [[VIAudioFile alloc] initWithURL:fileURL looped:NO]; + NSString *fileID = [NSUUID UUID].UUIDString; + VIAudioFileModule *fileModule = [[VIAudioFileModule alloc] initWithPlugin:_plugin + audioFile:audioFile + fileID:fileID]; + _audioFileModules[fileID] = fileModule; + result(fileID); + + } else if ([@"loadFile" isEqualToString:call.method]) { + NSString *stringURL = call.arguments[@"url"]; + if (!stringURL) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.loadFile: url is null" + details:nil]); + return; + } + NSURL *URL = [NSURL URLWithString:stringURL]; + if (!URL) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.loadFile: could'nt build URL" + details:nil]); + return; + } + __weak VIAudioFileManager *weakSelf = self; + NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:URL + completionHandler:^(NSData * _Nullable data, + NSURLResponse * _Nullable response, + NSError * _Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFileModule.loadFile: failed to load audio file" + details:nil]); + } else { + __strong VIAudioFileManager *strongSelf = weakSelf; + VIAudioFile *audioFile = [[VIAudioFile alloc] initWithData:data looped:NO]; + NSString *fileID = [NSUUID UUID].UUIDString; + VIAudioFileModule *fileModule = [[VIAudioFileModule alloc] initWithPlugin:strongSelf.plugin + audioFile:audioFile + fileID:fileID]; + strongSelf.audioFileModules[fileID] = fileModule; + result(fileID); + } + }]; + [task resume]; + + } else if ([@"releaseResources" isEqualToString:call.method]) { + NSString *fileID = call.arguments[@"fileId"]; + if (!fileID) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFile.releaseResources: fileId is null" + details:nil]); + return; + } + [_audioFileModules removeObjectForKey:fileID]; + result(nil); + + } else { + NSString *fileID = [call.arguments objectForKey:@"fileId"]; + if (!fileID) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:[call.method stringByAppendingString:@": fileId is null"] + details:nil]); + return; + } + VIAudioFileModule *module = [_audioFileModules objectForKey:fileID]; + if (!module) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:[call.method stringByAppendingString:@": could'nt find audioFile"] + details:nil]); + return; + } + [module handleMethodCall:call result:result]; + } +} + +@end diff --git a/ios/Classes/VIAudioFileModule.h b/ios/Classes/VIAudioFileModule.h new file mode 100644 index 0000000..223321e --- /dev/null +++ b/ios/Classes/VIAudioFileModule.h @@ -0,0 +1,16 @@ +/* +* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. +*/ + +#import "VoximplantPlugin.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface VIAudioFileModule: NSObject + +- (instancetype)initWithPlugin:(VoximplantPlugin *)plugin audioFile:(VIAudioFile *)file fileID:(NSString *)fileID; +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Classes/VIAudioFileModule.m b/ios/Classes/VIAudioFileModule.m new file mode 100644 index 0000000..878938a --- /dev/null +++ b/ios/Classes/VIAudioFileModule.m @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +#import "VIAudioFileModule.h" +#import "VIAudioFileManager.h" +#import "VoximplantUtils.h" + +@interface VIAudioFileModule () + +@property(nonatomic, strong) FlutterEventChannel *eventChannel; +@property(nonatomic, strong) FlutterEventSink eventSink; +@property(nonatomic, strong) VIAudioFile *audioFile; +@property(nonatomic, strong) NSString *fileID; +@property(nonatomic) FlutterResult playCompletion; +@property(nonatomic) FlutterResult stopCompletion; + +@end + + +@implementation VIAudioFileModule + +- (instancetype)initWithPlugin:(VoximplantPlugin *)plugin audioFile:(VIAudioFile *)file fileID:(NSString *)fileID { + self = [super init]; + if (self) { + _eventChannel + = [FlutterEventChannel eventChannelWithName:[@"plugins.voximplant.com/audio_file_events_" stringByAppendingString:fileID] + binaryMessenger:plugin.registrar.messenger]; + [_eventChannel setStreamHandler:self]; + _fileID = fileID; + _audioFile = file; + _audioFile.delegate = self; + } + return self; +} + +- (void)dealloc { + _audioFile.delegate = nil; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"play" isEqualToString:call.method]) { + NSNumber *looped = call.arguments[@"looped"]; + if (!looped) { + result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" + message:@"VIAudioFileModule.play: looped is null" + details:nil]); + return; + } + _audioFile.looped = looped.boolValue; + [_audioFile play]; + _playCompletion = result; + } else if ([@"stop" isEqualToString:call.method]) { + [_audioFile stop]; + _stopCompletion = result; + } else { + result(FlutterMethodNotImplemented); + } +} + +#pragma mark - VIAudioFileDelegate - +- (void)audioFile:(VIAudioFile *)audioFile didStartPlaying:(NSError *)playbackError { + if (_playCompletion) { + if (playbackError) { + _playCompletion([FlutterError errorWithCode:[VoximplantUtils convertAudioFileErrorToString:playbackError.code] + message:playbackError.localizedDescription + details:nil]); + } else { + _playCompletion(nil); + } + _playCompletion = nil; + } +} + +- (void)audioFile:(VIAudioFile *)audioFile didStopPlaying:(NSError *)playbackError { + if (_stopCompletion) { + if (playbackError) { + _stopCompletion([FlutterError errorWithCode:[VoximplantUtils convertAudioFileErrorToString:playbackError.code] + message:playbackError.localizedDescription + details:nil]); + } else { + _stopCompletion(nil); + } + _stopCompletion = nil; + } else if (_eventSink) { + NSString *error; + if (playbackError) { + error = [VoximplantUtils convertAudioFileErrorToString:playbackError.code]; + } + if (_fileID) { + self.eventSink(@{ + @"name": @"didStopPlaying", + @"error": error + }); + } + } +} + +#pragma mark - FlutterStreamHandler - +- (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +- (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { + _eventSink = events; + return nil; +} + +@end diff --git a/ios/Classes/VICameraModule.m b/ios/Classes/VICameraModule.m index 6c1d05c..f778472 100644 --- a/ios/Classes/VICameraModule.m +++ b/ios/Classes/VICameraModule.m @@ -33,8 +33,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result - (void)selectCamera:(NSDictionary *)arguments result:(FlutterResult)result { if (!arguments) { result([FlutterError errorWithCode:@"ERROR_INVALID_ARGUMENTS" - message:@"VICameraManager.selectCamera: Invalid arguments" - details:nil]); + message:@"VICameraManager.selectCamera: Invalid arguments" + details:nil]); return; } NSNumber *cameraType = [arguments objectForKey:@"cameraType"]; diff --git a/ios/Classes/VoximplantCallManager.m b/ios/Classes/VoximplantCallManager.m index d8107d4..468a859 100644 --- a/ios/Classes/VoximplantCallManager.m +++ b/ios/Classes/VoximplantCallManager.m @@ -5,6 +5,7 @@ #import "VoximplantCallManager.h" @interface VoximplantCallManager() + @property(nonatomic, strong) NSMutableDictionary *callModules; @end diff --git a/ios/Classes/VoximplantPlugin.m b/ios/Classes/VoximplantPlugin.m index cbfccc2..0a6812e 100644 --- a/ios/Classes/VoximplantPlugin.m +++ b/ios/Classes/VoximplantPlugin.m @@ -10,10 +10,13 @@ #import "VoximplantCallManager.h" #import "VICameraModule.h" #import "VIMessagingModule.h" +#import "VIAudioFileModule.h" +#import "VIAudioFileManager.h" @interface VoximplantPlugin() @property(nonatomic, strong) VIClientModule *clientModule; @property(nonatomic, strong) VIAudioDeviceModule *audioDeviceModule; +@property(nonatomic, strong) VIAudioFileManager *audioFileManager; @property(nonatomic, strong) VoximplantCallManager *callManager; @property(nonatomic, strong) VICameraModule *cameraModule; @property(nonatomic, strong) VIMessagingModule *messagingModule; @@ -47,9 +50,10 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar self.callManager = [[VoximplantCallManager alloc] init]; self.clientModule = [[VIClientModule alloc] initWithRegistrar:self.registrar callManager:self.callManager]; self.audioDeviceModule = [[VIAudioDeviceModule alloc] initWithPlugin:self]; + self.audioFileManager = [[VIAudioFileManager alloc] initWithPlugin:self]; self.cameraModule = [[VICameraModule alloc] init]; self.messagingModule = [[VIMessagingModule alloc] initWithRegistrar:self.registrar]; - [VIClient setVersionExtension:@"flutter-2.3.0"]; + [VIClient setVersionExtension:@"flutter-2.4.0"]; } return self; } @@ -88,6 +92,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([call isMethodCallOfType:VIMethodTypeCamera]) { [self.cameraModule handleMethodCall:[call excludingType] result:result]; + } else if ([call isMethodCallOfType:VIMethodTypeAudioFile]) { + [self.audioFileManager handleMethodCall:[call excludingType] result:result]; + } else { result(FlutterMethodNotImplemented); } diff --git a/ios/Classes/VoximplantUtils.h b/ios/Classes/VoximplantUtils.h index 1a61d87..a6d5910 100644 --- a/ios/Classes/VoximplantUtils.h +++ b/ios/Classes/VoximplantUtils.h @@ -12,6 +12,7 @@ + (NSString *)convertCallErrorToString:(VICallErrorCode)code; + (NSString *)getErrorDescriptionForCallError:(VICallErrorCode)code; + (NSString *)convertMessagingErrorToString:(VIErrorEvent *)error; ++ (NSString *)convertAudioFileErrorToString:(VIAudioFileErrorCode)audioFileError; + (NSDictionary *)convertAuthParamsToDictionary:(VIAuthParams *)authParams; + (NSNumber *)convertVideoStreamTypeToNumber:(VIVideoStreamType)type; + (int)convertVideoRotationToInt:(RTCVideoRotation)rotation; @@ -24,6 +25,7 @@ typedef NSString *VIMethodType NS_TYPED_ENUM; static VIMethodType const VIMethodTypeMessaging = @"Messaging"; static VIMethodType const VIMethodTypeClient= @"Client"; static VIMethodType const VIMethodTypeAudioDevice = @"AudioDevice"; +static VIMethodType const VIMethodTypeAudioFile = @"AudioFile"; static VIMethodType const VIMethodTypeCall = @"Call"; static VIMethodType const VIMethodTypeCamera = @"Camera"; static VIMethodType const VIMethodTypeVideoStream = @"VideoStream"; diff --git a/ios/Classes/VoximplantUtils.m b/ios/Classes/VoximplantUtils.m index 2ea0804..97d6173 100644 --- a/ios/Classes/VoximplantUtils.m +++ b/ios/Classes/VoximplantUtils.m @@ -24,6 +24,8 @@ + (NSString *)convertLoginErrorToString:(VILoginErrorCode)code { return @"ERROR_INVALID_PASSWORD"; case VILoginErrorCodeConnectionClosed: return @"ERROR_NETWORK_ISSUES"; + case VILoginErrorCodeMAUAccessDenied: + return @"ERROR_MAU_ACCESS_DENIED"; case VILoginErrorCodeInternalError: default: return @"ERROR_INTERNAL"; @@ -48,6 +50,8 @@ + (NSString *)getErrorDescriptionForLoginError:(VILoginErrorCode)code { return @"Invalid password."; case VILoginErrorCodeConnectionClosed: return @"Connection to the Voximplant Cloud is closed"; + case VILoginErrorCodeMAUAccessDenied: + return @"Monthly Active Users (MAU) limit is reached. Payment is required."; case VILoginErrorCodeInternalError: default: return @"Internal error."; @@ -170,6 +174,26 @@ + (NSString *)convertMessagingErrorToString:(VIErrorEvent *)error { } } ++ (NSString *)convertAudioFileErrorToString:(VIAudioFileErrorCode)audioFileError { + switch (audioFileError) { + case VIAudioFileErrorCodeDestroyed: + return @"ERROR_DESTROYED"; + case VIAudioFileErrorCodeInterrupted: + return @"ERROR_INTERRUPTED"; + case VIAudioFileErrorCodeAlreadyPlaying: + return @"ERROR_ALREADY_PLAYING"; + case VIAudioFileErrorCodeCallKitActivated: + return @"ERROR_CALLKIT_ACTIVATED"; + case VIAudioFileErrorCodeCallKitDeactivated: + return @"ERROR_CALLKIT_DEACTIVATED"; + case VIAudioFileErrorCodeFailedToConfigureAudioSession: + return @"ERROR_FAILED_TO_CONFIGURE_AUDIO_SESSION"; + case VIAudioFileErrorCodeInternal: + default: + return @"ERROR_INTERNAL"; + } + } + + (NSDictionary *)convertAuthParamsToDictionary:(VIAuthParams *)authParams { NSMutableDictionary *dictionary = [NSMutableDictionary new]; [dictionary setValue:@((NSInteger)authParams.accessExpire) forKey:@"accessExpire"]; diff --git a/ios/flutter_voximplant.podspec b/ios/flutter_voximplant.podspec index f6921cf..86808fb 100644 --- a/ios/flutter_voximplant.podspec +++ b/ios/flutter_voximplant.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'flutter_voximplant' - s.version = '2.3.0' + s.version = '2.4.0' s.summary = 'Voximplant Flutter SDK' s.description = <<-DESC Voximplant plugin for embedding voice and video communication into Flutter applications. @@ -15,6 +15,6 @@ Voximplant plugin for embedding voice and video communication into Flutter appli s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'VoxImplantSDK', '2.33.0' + s.dependency 'VoxImplantSDK', '2.34.3' s.ios.deployment_target = '9.0' end diff --git a/lib/flutter_voximplant.dart b/lib/flutter_voximplant.dart index bf1fbae..881f6ec 100644 --- a/lib/flutter_voximplant.dart +++ b/lib/flutter_voximplant.dart @@ -26,3 +26,4 @@ part 'src/messaging/messenger_shared.dart'; part 'src/messaging/user.dart'; part 'src/messaging/message.dart'; part 'src/messaging/events.dart'; +part 'src/hardware/audio_file.dart'; diff --git a/lib/src/client/client.dart b/lib/src/client/client.dart index c4d2677..a939bd8 100644 --- a/lib/src/client/client.dart +++ b/lib/src/client/client.dart @@ -138,6 +138,7 @@ class VIClient { /// /// Errors: /// * [VIClientError.ERROR_ACCOUNT_FROZEN] - If the account is frozen. + /// * [VIClientError.ERROR_MAU_ACCESS_DENIED] - Monthly Active Users (MAU) limit is reached. Payment is required. /// * [VIClientError.ERROR_INTERNAL] - If an internal error occurred. /// * [VIClientError.ERROR_INVALID_PASSWORD] - if the given password is invalid. /// * [VIClientError.ERROR_INVALID_STATE] - If the client is not connected, @@ -177,6 +178,7 @@ class VIClient { /// /// Errors: /// * [VIClientError.ERROR_ACCOUNT_FROZEN] - If the account is frozen. + /// * [VIClientError.ERROR_MAU_ACCESS_DENIED] - Monthly Active Users (MAU) limit is reached. Payment is required. /// * [VIClientError.ERROR_INTERNAL] - If an internal error occurred. /// * [VIClientError.ERROR_INVALID_PASSWORD] - if the given password is invalid. /// * [VIClientError.ERROR_INVALID_STATE] - If the client is not connected, diff --git a/lib/src/error_codes.dart b/lib/src/error_codes.dart index 7825e0f..6d1ebd3 100644 --- a/lib/src/error_codes.dart +++ b/lib/src/error_codes.dart @@ -15,6 +15,7 @@ class VIException implements Exception { class VIClientError { static const String ERROR_ACCOUNT_FROZEN = 'ERROR_ACCOUNT_FROZEN'; static const String ERROR_INTERNAL = 'ERROR_INTERNAL'; + static const String ERROR_MAU_ACCESS_DENIED = 'ERROR_MAU_ACCESS_DENIED'; static const String ERROR_INVALID_PASSWORD = 'ERROR_INVALID_PASSWORD'; static const String ERROR_INVALID_STATE = 'ERROR_INVALID_STATE'; static const String ERROR_INVALID_USERNAME = 'ERROR_INVALID_USERNAME'; @@ -38,6 +39,24 @@ class VICallError { static const String ERROR_INVALID_ARGUMENTS = 'ERROR_INVALID_ARGUMENTS'; } +class VIAudioFileError { + /// Audio file playing was stopped due to instance is deallocated. + static const String ERROR_DESTROYED = 'ERROR_DESTROYED'; + /// Audio file playing was interrupted by a third party application. + static const String ERROR_INTERRUPTED = 'ERROR_INTERRUPTED'; + /// The audio file is already playing. + static const String ERROR_ALREADY_PLAYING = 'ERROR_ALREADY_PLAYING'; + /// Audio file playing was interrupted by CallKit activation. + static const String ERROR_CALLKIT_ACTIVATED = 'ERROR_CALLKIT_ACTIVATED'; + /// Audio file playing was interrupted by CallKit deactivation. + static const String ERROR_CALLKIT_DEACTIVATED = 'ERROR_CALLKIT_DEACTIVATED'; + /// Audio file failed to start playing due to audio session configuration issues. + static const String ERROR_FAILED_TO_CONFIGURE_AUDIO_SESSION = + 'ERROR_FAILED_TO_CONFIGURE_AUDIO_SESSION'; + /// Internal error occurred. + static const String ERROR_INTERNAL = 'ERROR_INTERNAL'; +} + class VIMessagingError { /// Something went wrong. Please check your input or required parameters. static const String ERROR_SOMETHING_WENT_WRONG = "ERROR_SOMETHING_WENT_WRONG"; diff --git a/lib/src/hardware/audio_file.dart b/lib/src/hardware/audio_file.dart new file mode 100644 index 0000000..1e8b93c --- /dev/null +++ b/lib/src/hardware/audio_file.dart @@ -0,0 +1,193 @@ +/// Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + +part of voximplant; + +/// Signature for callbacks reporting when the audio file playing is stopped. +/// +/// `error` - A reason to stop. iOS ONLY. +/// For all possible errors see [VIAudioFileError] +/// +/// Used in [VIAudioFile]. +typedef void VIAudioFileStopped(String error); + +/// Enum representing supported audio file usage modes +/// +/// ANDROID ONLY. +enum VIAudioFileUsage { + /// Should be used to play audio file during a call, for example to play progress tone. + /// + /// The volume is controlled by system call volume. + inCall, + + /// Should be used to play audio file for notifications outside a call. + /// + /// The volume is controlled by system alarm volume + notification, + + /// Should be used to play audio file for ringtone. + /// + /// The volume is controlled by system ring volume. + ringtone, + + /// Should be used if other modes are not applicable. + unknown +} + +/// Class may be used to play audio files. +class VIAudioFile { + + /// HTTP URL of the stream to play + final String url; + + /// Local audio file name + final String name; + + /// Indicate if the audio file should be played repeatedly or once + bool get looped => _looped; + + /// Invoked when the audio file playing is stopped. + VIAudioFileStopped onStopped; + + static MethodChannel get _methodChannel => Voximplant._channel; + StreamSubscription _eventSubscription; + String _fileId; + final String _type; + final VIAudioFileUsage _usage; + final _VIAudioFileDataSource _dataSource; + bool _looped = false; + + /// Constructs a [VIAudioFile] to play an audio from a file. + /// + /// `name` - Local audio file name + /// + /// `type` - Local audio file type/format, for example ".mp3" + /// + /// `usage` - Audio file usage mode. ANDROID ONLY. + /// + /// On android, the audio file must be located in resources "raw" folder. + VIAudioFile.file(this.name, this._type, + {VIAudioFileUsage usage = VIAudioFileUsage.unknown}) + : _dataSource = _VIAudioFileDataSource.file, + _usage = usage, + url = null; + + /// Constructs a [VIAudioFile] to play an audio file obtained from the network. + /// + /// `url` - HTTP URL of the stream to play + /// + /// `usage` - Audio file usage mode. ANDROID ONLY. + VIAudioFile.network(this.url, + {VIAudioFileUsage usage = VIAudioFileUsage.unknown}) + : _dataSource = _VIAudioFileDataSource.network, + _usage = usage, + name = null, + _type = null; + + /// Initialize and prepare the audio file to play + /// + /// Must be used before any other interactions with the object + Future initialize() async { + try { + if (_dataSource == _VIAudioFileDataSource.file) { + _fileId = await _methodChannel.invokeMethod( + 'AudioFile.initWithFile', { + 'name': name, + 'type': _type, + 'usage': _audioFileUsageToString(_usage) + }); + } else if (_dataSource == _VIAudioFileDataSource.network) { + _fileId = await Voximplant._channel.invokeMethod( + 'AudioFile.loadFile', { + 'url': url, + 'usage': _audioFileUsageToString(_usage) + }); + } + this._eventSubscription = + EventChannel('plugins.voximplant.com/audio_file_events_$_fileId') + .receiveBroadcastStream() + .listen((event) { + if (event['name'] == 'didStopPlaying') { + if (onStopped != null) { + onStopped(event['error']); + } + } + }); + } on PlatformException catch (e) { + throw VIException(e.code, e.message); + } catch (e) { + rethrow; + } + } + + /// Starts playing the audio file + /// + /// `looped` - Indicate if the audio file should be played repeatedly or once + /// + /// Throws [VIException], if an error occurred. + /// For all possible errors see [VIAudioFileError] + Future play(bool looped) async { + try { + await _methodChannel.invokeMethod('AudioFile.play', + {'fileId': _fileId, 'looped': looped}); + _looped = looped; + return Future.value(); + } on PlatformException catch (e) { + throw VIException(e.code, e.message); + } catch (e) { + rethrow; + } + } + + /// Stops playing of the audio file + /// + /// Throws [VIException], if an error occurred. + /// For all possible errors see [VIAudioFileError] + Future stop() async { + try { + await _methodChannel + .invokeMethod('AudioFile.stop', {'fileId': _fileId}); + return Future.value(); + } on PlatformException catch (e) { + throw VIException(e.code, e.message); + } catch (e) { + rethrow; + } + } + + /// Releases all resources allocated for playing audio file. + /// + /// Must be called even if the file was not played. + /// + /// Throws [VIException], if an error occurred. + Future releaseResources() async { + try { + await _methodChannel.invokeMethod( + 'AudioFile.releaseResources', {'fileId': _fileId}); + _eventSubscription.cancel(); + return Future.value(); + } on PlatformException catch (e) { + throw VIException(e.code, e.message); + } catch (e) { + rethrow; + } + } + + String _audioFileUsageToString(VIAudioFileUsage usage) { + switch (usage) { + case VIAudioFileUsage.inCall: + return 'incall'; + case VIAudioFileUsage.notification: + return 'notification'; + case VIAudioFileUsage.ringtone: + return 'ringtone'; + case VIAudioFileUsage.unknown: + return 'unknown'; + default: + return 'unknown'; + } + } +} + +enum _VIAudioFileDataSource { + file, network +} diff --git a/lib/src/messaging/messenger.dart b/lib/src/messaging/messenger.dart index d33ac98..aec854f 100644 --- a/lib/src/messaging/messenger.dart +++ b/lib/src/messaging/messenger.dart @@ -4,7 +4,7 @@ part of voximplant; /// Signature for callbacks reporting that an user was edited /// as the result of [VIMessenger.editUser], [VIMessenger.managePushNotifications] -// /// or analogous methods from other Voximplant SDKs and Messaging API. +/// or analogous methods from other Voximplant SDKs and Messaging API. /// /// Used in [VIMessenger]. /// diff --git a/pubspec.yaml b/pubspec.yaml index 8808351..286dea8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_voximplant description: Voximplant plugin for embedding voice and video communication into Flutter applications. -version: 2.3.0 +version: 2.4.0 authors: - Voximplant Team - Yulia Grigorieva