diff --git a/android/src/main/java/com/voximplant/reactnative/AudioFileManager.java b/android/src/main/java/com/voximplant/reactnative/AudioFileManager.java new file mode 100644 index 0000000..2b5493e --- /dev/null +++ b/android/src/main/java/com/voximplant/reactnative/AudioFileManager.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +package com.voximplant.reactnative; + +import com.voximplant.sdk.hardware.IAudioFile; + +import java.util.HashMap; +import java.util.Map; + +public class AudioFileManager { + private static AudioFileManager mInstance = null; + private HashMap mAudioFiles = new HashMap<>(); + + static synchronized AudioFileManager getInstance() { + if (mInstance == null) { + mInstance = new AudioFileManager(); + } + return mInstance; + } + + void addAudioFile(String fileId, IAudioFile audioFile) { + if (fileId != null && audioFile != null) { + mAudioFiles.put(fileId, audioFile); + } + } + + IAudioFile getAudioFile(String fileId) { + if (fileId == null) { + return null; + } + return mAudioFiles.get(fileId); + } + + void removeAudioFile(String fileId) { + if (fileId != null) { + mAudioFiles.remove(fileId); + } + } + + String getFileIdForAudioFile(IAudioFile audioFile) { + for(Map.Entry entry : mAudioFiles.entrySet()) { + if (entry.getValue().equals(audioFile)) { + return entry.getKey(); + } + } + return null; + } +} diff --git a/android/src/main/java/com/voximplant/reactnative/Constants.java b/android/src/main/java/com/voximplant/reactnative/Constants.java index 5e7b72a..c592f8a 100644 --- a/android/src/main/java/com/voximplant/reactnative/Constants.java +++ b/android/src/main/java/com/voximplant/reactnative/Constants.java @@ -44,6 +44,8 @@ class Constants { static final String EVENT_CAMERA_SWITCH_DONE = "VICameraSwitchDone"; static final String EVENT_CAMERA_SWITCH_ERROR = "VICameraSwitchError"; + static final String EVENT_AUDIO_FILE_STARTED = "VIAudioFileStarted"; + static final String EVENT_AUDIO_FILE_STOPPED = "VIAudioFileStopped"; static final String EVENT_NAME_CONNECTION_ESTABLISHED = "ConnectionEstablished"; static final String EVENT_NAME_CONNECTION_FAILED = "ConnectionFailed"; @@ -77,6 +79,8 @@ class Constants { static final String EVENT_NAME_CAMERA_SWITCH_DONE = "CameraSwitchDone"; static final String EVENT_NAME_CAMERA_SWITCH_ERROR = "CameraSwitchError"; + static final String EVENT_NAME_AUDIO_FILE_STARTED = "Started"; + static final String EVENT_NAME_AUDIO_FILE_STOPPED = "Stopped"; static final String EVENT_PARAM_NAME = "name"; static final String EVENT_PARAM_RESULT = "result"; @@ -113,6 +117,8 @@ class Constants { static final String EVENT_PARAM_IS_LOCAL = "isLocal"; static final String EVENT_PARAM_VIDEO_STREAM_TYPE = "videoStreamType"; + static final String EVENT_PARAM_AUDIO_FILE_ID = "fileId"; + static final String EVENT_PARAM_CURRENT_AUDIO_DEVICE = "currentDevice"; static final String EVENT_PARAM_AUDIO_DEVICE_LIST = "newDeviceList"; @@ -128,6 +134,11 @@ class Constants { static final String SCALE_TYPE_FIT = "fit"; static final String SCALE_TYPE_FILL = "fill"; + static final String IN_CALL = "incall"; + static final String NOTIFICATION = "notification"; + static final String RINGTONE = "ringtone"; + static final String UNKNOWN = "unknown"; + static final String VIDEO_STREAM_TYPE_VIDEO = "Video"; static final String VIDEO_STREAM_TYPE_SCREEN_SHARING = "ScreenSharing"; diff --git a/android/src/main/java/com/voximplant/reactnative/Utils.java b/android/src/main/java/com/voximplant/reactnative/Utils.java index b0430da..6304b51 100644 --- a/android/src/main/java/com/voximplant/reactnative/Utils.java +++ b/android/src/main/java/com/voximplant/reactnative/Utils.java @@ -14,6 +14,7 @@ import com.voximplant.sdk.client.LoginError; import com.voximplant.sdk.client.RequestAudioFocusMode; import com.voximplant.sdk.hardware.AudioDevice; +import com.voximplant.sdk.hardware.AudioFileUsage; import com.voximplant.sdk.messaging.MessengerAction; import com.voximplant.sdk.messaging.MessengerEventType; import com.voximplant.sdk.messaging.MessengerNotification; @@ -74,10 +75,14 @@ import static com.voximplant.reactnative.Constants.EVENT_PARAM_LOG_LEVEL_INFO; import static com.voximplant.reactnative.Constants.EVENT_PARAM_LOG_LEVEL_VERBOSE; import static com.voximplant.reactnative.Constants.EVENT_PARAM_LOG_LEVEL_WARNING; +import static com.voximplant.reactnative.Constants.IN_CALL; +import static com.voximplant.reactnative.Constants.NOTIFICATION; +import static com.voximplant.reactnative.Constants.RINGTONE; import static com.voximplant.reactnative.Constants.SEND_MESSAGE; import static com.voximplant.reactnative.Constants.REQUEST_ON_CALL_CONNECTED; import static com.voximplant.reactnative.Constants.REQUEST_ON_CALL_START; +import static com.voximplant.reactnative.Constants.UNKNOWN; import static com.voximplant.reactnative.Constants.VIDEO_STREAM_TYPE_SCREEN_SHARING; import static com.voximplant.reactnative.Constants.VIDEO_STREAM_TYPE_VIDEO; @@ -572,4 +577,21 @@ static String convertVideoStreamType(VideoStreamType videoStreamType) { return VIDEO_STREAM_TYPE_VIDEO; } } + + static AudioFileUsage convertStringToAudioFileUsage(String usage) { + if (usage == null) { + return AudioFileUsage.UNKNOWN; + } + switch (usage) { + case IN_CALL: + 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/reactnative/VIAudioFileModule.java b/android/src/main/java/com/voximplant/reactnative/VIAudioFileModule.java new file mode 100644 index 0000000..c9c909f --- /dev/null +++ b/android/src/main/java/com/voximplant/reactnative/VIAudioFileModule.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +package com.voximplant.reactnative; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.voximplant.sdk.Voximplant; +import com.voximplant.sdk.hardware.IAudioFile; +import com.voximplant.sdk.hardware.IAudioFileListener; + +import java.util.UUID; + +import javax.annotation.Nullable; + +import static com.voximplant.reactnative.Constants.EVENT_AUDIO_FILE_STARTED; +import static com.voximplant.reactnative.Constants.EVENT_AUDIO_FILE_STOPPED; +import static com.voximplant.reactnative.Constants.EVENT_NAME_AUDIO_FILE_STARTED; +import static com.voximplant.reactnative.Constants.EVENT_NAME_AUDIO_FILE_STOPPED; +import static com.voximplant.reactnative.Constants.EVENT_PARAM_AUDIO_FILE_ID; +import static com.voximplant.reactnative.Constants.EVENT_PARAM_NAME; +import static com.voximplant.reactnative.Constants.EVENT_PARAM_RESULT; + +public class VIAudioFileModule extends ReactContextBaseJavaModule implements IAudioFileListener { + private ReactApplicationContext mReactContext; + private Callback mCallback; + + public VIAudioFileModule(ReactApplicationContext reactContext) { + super(reactContext); + mReactContext = reactContext; + } + + @Override + public String getName() { + return "VIAudioFileModule"; + } + + @ReactMethod + public void initWithFile(ReadableMap params, Callback callback) { + if (params == null) { + callback.invoke(null, "Invalid arguments"); + return; + } + String name = params.getString("name"); + String usage = params.getString("usage"); + int rawId = mReactContext.getResources().getIdentifier(name, "raw", mReactContext.getPackageName()); + IAudioFile audioFile = Voximplant.createAudioFile(mReactContext, rawId, Utils.convertStringToAudioFileUsage(usage)); + if (audioFile == null) { + callback.invoke(null, "Internal error"); + return; + } + audioFile.setAudioFileListener(this); + String fileId = UUID.randomUUID().toString(); + AudioFileManager.getInstance().addAudioFile(fileId, audioFile); + callback.invoke(fileId, null); + } + + @ReactMethod + public void loadFile(ReadableMap params, Callback callback) { + if (params == null) { + callback.invoke(null, "Invalid arguments"); + return; + } + String url = params.getString("url"); + String usage = params.getString("usage"); + IAudioFile audioFile = Voximplant.createAudioFile(url, Utils.convertStringToAudioFileUsage(usage)); + if (audioFile == null) { + callback.invoke(null, "Internal error"); + return; + } + audioFile.setAudioFileListener(this); + String fileId = UUID.randomUUID().toString(); + AudioFileManager.getInstance().addAudioFile(fileId, audioFile); + mCallback = callback; + } + + @ReactMethod + public void play(String fileId, boolean loop) { + IAudioFile audioFile = AudioFileManager.getInstance().getAudioFile(fileId); + if (audioFile != null) { + audioFile.play(loop); + } + } + + @ReactMethod + public void stop(String fileId) { + IAudioFile audioFile = AudioFileManager.getInstance().getAudioFile(fileId); + if (audioFile != null) { + audioFile.stop(false); + } + } + + @ReactMethod + public void releaseResources(String fileId) { + IAudioFile audioFile = AudioFileManager.getInstance().getAudioFile(fileId); + if (audioFile != null) { + audioFile.release(); + AudioFileManager.getInstance().removeAudioFile(fileId); + } + } + + + @Override + public void onStart(IAudioFile audioFile) { + String fileId = AudioFileManager.getInstance().getFileIdForAudioFile(audioFile); + if (fileId != null) { + WritableMap params = Arguments.createMap(); + params.putString(EVENT_PARAM_NAME, EVENT_NAME_AUDIO_FILE_STARTED); + params.putString(EVENT_PARAM_AUDIO_FILE_ID, fileId); + params.putBoolean(EVENT_PARAM_RESULT, true); + sendEvent(EVENT_AUDIO_FILE_STARTED, params); + } + } + + @Override + public void onStop(IAudioFile audioFile) { + String fileId = AudioFileManager.getInstance().getFileIdForAudioFile(audioFile); + if (fileId != null) { + WritableMap params = Arguments.createMap(); + params.putString(EVENT_PARAM_NAME, EVENT_NAME_AUDIO_FILE_STOPPED); + params.putString(EVENT_PARAM_AUDIO_FILE_ID, fileId); + params.putBoolean(EVENT_PARAM_RESULT, true); + sendEvent(EVENT_AUDIO_FILE_STOPPED, params); + } + } + + @Override + public void onPrepared(IAudioFile audioFile) { + if (mCallback != null) { + String fileId = AudioFileManager.getInstance().getFileIdForAudioFile(audioFile); + mCallback.invoke(fileId, null); + mCallback = null; + } + } + + private void sendEvent(String eventName, @Nullable WritableMap params) { + mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params); + } +} diff --git a/android/src/main/java/com/voximplant/reactnative/VoxImplantReactPackage.java b/android/src/main/java/com/voximplant/reactnative/VoxImplantReactPackage.java index 610ebfa..77b1bd5 100644 --- a/android/src/main/java/com/voximplant/reactnative/VoxImplantReactPackage.java +++ b/android/src/main/java/com/voximplant/reactnative/VoxImplantReactPackage.java @@ -20,7 +20,8 @@ public List createNativeModules(ReactApplicationContext reactConte new VICallModule(reactContext), new VIAudioDeviceModule(reactContext), new VICameraModule(reactContext), - new VIMessagingModule(reactContext)); + new VIMessagingModule(reactContext), + new VIAudioFileModule(reactContext)); } @Override diff --git a/ios/Constants.h b/ios/Constants.h index 8a292d8..37289a2 100644 --- a/ios/Constants.h +++ b/ios/Constants.h @@ -32,6 +32,8 @@ FOUNDATION_EXPORT NSString *const kEventEndpointRemoved; FOUNDATION_EXPORT NSString *const kEventAudioDeviceChanged; FOUNDATION_EXPORT NSString *const kEventAudioDeviceListChanged; +FOUNDATION_EXPORT NSString *const kEventAudioFileStarted; +FOUNDATION_EXPORT NSString *const kEventAudioFileStopped; FOUNDATION_EXPORT NSString *const kEventNameConnectionEstablished; FOUNDATION_EXPORT NSString *const kEventNameConnectionFailed; @@ -60,6 +62,8 @@ FOUNDATION_EXPORT NSString *const kEventNameEndpointRemoved; FOUNDATION_EXPORT NSString *const kEventNameAudioDeviceChanged; FOUNDATION_EXPORT NSString *const kEventNameAudioDeviceListChanged; +FOUNDATION_EXPORT NSString *const kEventNameAudioFileStarted; +FOUNDATION_EXPORT NSString *const kEventNameAudioFileStopped; FOUNDATION_EXPORT NSString *const kEventParamName; FOUNDATION_EXPORT NSString *const kEventParamResult; @@ -97,6 +101,9 @@ FOUNDATION_EXPORT NSString *const kEventParamCurrentAudioDevice; FOUNDATION_EXPORT NSString *const kEventParamDeviceList; FOUNDATION_EXPORT NSString *const kEventParamVideoStreamType; +FOUNDATION_EXPORT NSString *const kEventParamAudioFileId; +FOUNDATION_EXPORT NSString *const kEventParamError; + FOUNDATION_EXPORT NSString *const kVideoStreamTypeVideo; FOUNDATION_EXPORT NSString *const kVideoStreamTypeScreenSharing; @@ -109,6 +116,14 @@ FOUNDATION_EXPORT NSString *const kCallErrorAlreadyInThisState; FOUNDATION_EXPORT NSString *const kCallErrorMediaIsOnHold; FOUNDATION_EXPORT NSString *const kCallErrorInternal; +FOUNDATION_EXPORT NSString *const kAudioFileErrorInternal; +FOUNDATION_EXPORT NSString *const kAudioFileErrorInterrupted; +FOUNDATION_EXPORT NSString *const kAudioFileErrorDestroyed; +FOUNDATION_EXPORT NSString *const kAudioFileErrorAlreadyPlaying; +FOUNDATION_EXPORT NSString *const kAudioFileErrorCallKitActivated; +FOUNDATION_EXPORT NSString *const kAudioFileErrorCallKitDeactivated; +FOUNDATION_EXPORT NSString *const kAudioFileErrorFailedToConfigureAudioSession; + FOUNDATION_EXPORT NSString *const kAudioDeviceEarpiece; FOUNDATION_EXPORT NSString *const kAudioDeviceSpeaker; FOUNDATION_EXPORT NSString *const kAudioDeviceWired; diff --git a/ios/Constants.m b/ios/Constants.m index 5630516..de85006 100644 --- a/ios/Constants.m +++ b/ios/Constants.m @@ -32,6 +32,9 @@ NSString *const kEventAudioDeviceChanged = @"VIAudioDeviceChanged"; NSString *const kEventAudioDeviceListChanged = @"VIAudioDeviceListChanged"; +NSString *const kEventAudioFileStarted = @"VIAudioFileStarted"; +NSString *const kEventAudioFileStopped = @"VIAudioFileStopped"; + NSString *const kEventNameConnectionEstablished = @"ConnectionEstablished"; NSString *const kEventNameConnectionFailed = @"ConnectionFailed"; @@ -61,6 +64,8 @@ NSString *const kEventNameAudioDeviceChanged = @"DeviceChanged"; NSString *const kEventNameAudioDeviceListChanged = @"DeviceListChanged"; +NSString *const kEventNameAudioFileStarted = @"Started"; +NSString *const kEventNameAudioFileStopped = @"Stopped"; NSString *const kEventParamName = @"name"; NSString *const kEventParamResult = @"result"; @@ -98,6 +103,9 @@ NSString *const kEventParamDeviceList = @"newDeviceList"; NSString *const kEventParamVideoStreamType = @"videoStreamType"; +NSString *const kEventParamAudioFileId = @"fileId"; +NSString *const kEventParamError = @"error"; + NSString *const kVideoStreamTypeVideo = @"Video"; NSString *const kVideoStreamTypeScreenSharing = @"ScreenSharing"; @@ -110,6 +118,14 @@ NSString *const kCallErrorMediaIsOnHold = @"MEDIA_IS_ON_HOLD"; NSString *const kCallErrorInternal = @"INTERNAL_ERROR"; +NSString *const kAudioFileErrorInternal = @"INTERNAL_ERROR"; +NSString *const kAudioFileErrorInterrupted = @"INTERRUPTED"; +NSString *const kAudioFileErrorDestroyed = @"DESTROYED"; +NSString *const kAudioFileErrorAlreadyPlaying = @"ALREADY_PLAYING"; +NSString *const kAudioFileErrorCallKitActivated = @"CALLKIT_ACTIVATED"; +NSString *const kAudioFileErrorCallKitDeactivated = @"CALLKIT_DEACTIVATED"; +NSString *const kAudioFileErrorFailedToConfigureAudioSession = @"FAILED_TO_CONFIGURE_AUDIO_SESSION"; + NSString *const kAudioDeviceEarpiece = @"Earpiece"; NSString *const kAudioDeviceSpeaker = @"Speaker"; NSString *const kAudioDeviceWired = @"WiredHeadset"; diff --git a/ios/Utils.h b/ios/Utils.h index 17924eb..f9dc196 100644 --- a/ios/Utils.h +++ b/ios/Utils.h @@ -18,5 +18,6 @@ + (NSString *)convertLogSeverity:(VILogSeverity)severity; + (NSDictionary *)convertAuthParamsToDictionary:(VIAuthParams *)authParams; + (NSString *)convertVideoStreamTypeToString:(VIVideoStreamType)videoStreamType; ++ (NSString *)convertAudioFileErrorToString:(VIAudioFileErrorCode)audioFileError; @end diff --git a/ios/Utils.m b/ios/Utils.m index 7b94274..66cc99c 100644 --- a/ios/Utils.m +++ b/ios/Utils.m @@ -220,4 +220,24 @@ + (NSString *)convertVideoStreamTypeToString:(VIVideoStreamType)videoStreamType } } ++ (NSString *)convertAudioFileErrorToString:(VIAudioFileErrorCode)audioFileError { + switch (audioFileError) { + case VIAudioFileErrorCodeDestroyed: + return kAudioFileErrorDestroyed; + case VIAudioFileErrorCodeInterrupted: + return kAudioFileErrorInterrupted; + case VIAudioFileErrorCodeAlreadyPlaying: + return kAudioFileErrorAlreadyPlaying; + case VIAudioFileErrorCodeCallKitActivated: + return kAudioFileErrorCallKitActivated; + case VIAudioFileErrorCodeCallKitDeactivated: + return kAudioFileErrorCallKitDeactivated; + case VIAudioFileErrorCodeFailedToConfigureAudioSession: + return kAudioFileErrorFailedToConfigureAudioSession; + case VIAudioFileErrorCodeInternal: + default: + return kAudioFileErrorInternal; + } +} + @end diff --git a/ios/VIAudioFileManager.h b/ios/VIAudioFileManager.h new file mode 100644 index 0000000..b7563f0 --- /dev/null +++ b/ios/VIAudioFileManager.h @@ -0,0 +1,14 @@ +/* +* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. +*/ + +#import + +@interface VIAudioFileManager : NSObject + ++ (void)addAudioFile:(VIAudioFile *)audioFile fileId:(NSString *)fileId; ++ (VIAudioFile *)getAudioFileById:(NSString *)fileId; ++ (NSString *)fileIdForAudioFile:(VIAudioFile *)audioFile; ++ (void)removeAudioFile:(NSString *)fileId; + +@end diff --git a/ios/VIAudioFileManager.m b/ios/VIAudioFileManager.m new file mode 100644 index 0000000..8988222 --- /dev/null +++ b/ios/VIAudioFileManager.m @@ -0,0 +1,58 @@ +/* +* Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. +*/ + +#import "VIAudioFileManager.h" + + +@interface VIAudioFileManager() +@property(nonatomic, strong) NSMutableDictionary *audioFiles; +@end + +@implementation VIAudioFileManager + ++ (VIAudioFileManager *)getInstance { + static dispatch_once_t onceToken; + static VIAudioFileManager *audioFileManager; + dispatch_once(&onceToken, ^{ + audioFileManager = [VIAudioFileManager new]; + }); + return audioFileManager; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.audioFiles = [NSMutableDictionary dictionary]; + } + return self; +} + ++ (void)addAudioFile:(VIAudioFile *)audioFile fileId:(NSString *)fileId { + [[VIAudioFileManager getInstance].audioFiles setObject:audioFile forKey:fileId]; +} + ++ (VIAudioFile *)getAudioFileById:(NSString *)fileId { + if (fileId) { + return [[VIAudioFileManager getInstance].audioFiles objectForKey:fileId]; + } + return nil; +} + ++ (NSString *)fileIdForAudioFile:(VIAudioFile *)audioFile { + NSArray *fileId = [[VIAudioFileManager getInstance].audioFiles allKeysForObject:audioFile]; + if (fileId.count != 1) { + return nil; + } + return [fileId objectAtIndex:0]; +} + ++ (void)removeAudioFile:(NSString *)fileId { + if (fileId) { + VIAudioFile *audioFile = [[VIAudioFileManager getInstance].audioFiles objectForKey:fileId]; + audioFile.delegate = nil; + [[VIAudioFileManager getInstance].audioFiles removeObjectForKey:fileId]; + } +} + +@end diff --git a/ios/VIAudioFileModule.h b/ios/VIAudioFileModule.h new file mode 100644 index 0000000..de9c52f --- /dev/null +++ b/ios/VIAudioFileModule.h @@ -0,0 +1,13 @@ +/* +* Copyright (c) 2011-2019, Zingaya, Inc. All rights reserved. +*/ + +#import +#import +#import +#import "RCTBridgeModule.h" +#import "RCTEventEmitter.h" + +@interface VIAudioFileModule : RCTEventEmitter + +@end diff --git a/ios/VIAudioFileModule.m b/ios/VIAudioFileModule.m new file mode 100644 index 0000000..35a7668 --- /dev/null +++ b/ios/VIAudioFileModule.m @@ -0,0 +1,103 @@ + +#import "VIAudioFileModule.h" +#import "VIAudioFileManager.h" +#import "Constants.h" +#import "Utils.h" + +@interface VIAudioFileModule() +@end + +@implementation VIAudioFileModule +RCT_EXPORT_MODULE(); + +- (NSArray *)supportedEvents { + return @[ + kEventAudioFileStarted, + kEventAudioFileStopped + ]; +} + ++ (BOOL)requiresMainQueueSetup { + return NO; +} + +RCT_REMAP_METHOD(initWithFile, initWithFile:(NSDictionary *)params responseCallback:(RCTResponseSenderBlock)callback) { + NSString *filename = [params objectForKey:@"name"]; + NSString *filetype = [params objectForKey:@"type"]; + if (!filename || !filetype) { + callback(@[[NSNull null], @"Invalid arguments"]); + return; + } + NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:filetype]; + if (!path) { + callback(@[[NSNull null], @"Failed to locate audio file"]); + return; + } + NSURL *fileURL = [NSURL fileURLWithPath:path]; + VIAudioFile *audioFile = [[VIAudioFile alloc] initWithURL:fileURL looped:false]; + audioFile.delegate = self; + NSString *fileId = [NSUUID UUID].UUIDString; + [VIAudioFileManager addAudioFile:audioFile fileId:fileId]; + callback(@[fileId]); +} + +RCT_REMAP_METHOD(loadFile, loadFile:(NSDictionary *)params responseCallback:(RCTResponseSenderBlock)callback) { + NSString *stringURL = [params objectForKey:@"url"]; + NSURL *url = [NSURL URLWithString:stringURL]; + NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (error) { + callback(@[[NSNull null], @"Failed to load audio file"]); + } else { + VIAudioFile *audioFile = [[VIAudioFile alloc] initWithData:data looped:false]; + audioFile.delegate = self; + NSString *fileId = [NSUUID UUID].UUIDString; + [VIAudioFileManager addAudioFile:audioFile fileId:fileId]; + callback(@[fileId, [NSNull null]]); + } + }]; + [task resume]; +} + +RCT_REMAP_METHOD(play, play:(NSString *)fileId looped:(BOOL)looped) { + VIAudioFile * audioFile = [VIAudioFileManager getAudioFileById:fileId]; + audioFile.looped = looped; + [audioFile play]; +} + +RCT_EXPORT_METHOD(stop:(NSString *)fileId) { + VIAudioFile * audioFile = [VIAudioFileManager getAudioFileById:fileId]; + [audioFile stop]; +} + +RCT_EXPORT_METHOD(releaseResources:(NSString *)fileId) { + [VIAudioFileManager removeAudioFile:fileId]; +} + +- (void)audioFile:(VIAudioFile *)audioFile didStartPlaying:(NSError *)playbackError { + NSString *fileId = [VIAudioFileManager fileIdForAudioFile:audioFile]; + NSNumber *result = [NSNumber numberWithBool:(playbackError == nil)]; + if (fileId) { + [self sendEventWithName:kEventAudioFileStarted body:@{ + kEventParamName : kEventNameAudioFileStarted, + kEventParamResult : result, + kEventParamError : [Utils convertAudioFileErrorToString:playbackError.code], + kEventParamAudioFileId : fileId + }]; + } +} + +- (void)audioFile:(VIAudioFile *)audioFile didStopPlaying:(NSError *)playbackError { + NSString *fileId = [VIAudioFileManager fileIdForAudioFile:audioFile]; + NSNumber *result = [NSNumber numberWithBool:(playbackError == nil)]; + if (fileId) { + [self sendEventWithName:kEventAudioFileStopped body:@{ + kEventParamName : kEventNameAudioFileStopped, + kEventParamResult : result, + kEventParamError : [Utils convertAudioFileErrorToString:playbackError.code], + kEventParamAudioFileId : fileId + }]; + } +} + +@end diff --git a/src/Enums.js b/src/Enums.js index 01217eb..0f0c0b1 100644 --- a/src/Enums.js +++ b/src/Enums.js @@ -330,3 +330,10 @@ export const MessengerNotification = { */ SendMessage : 'SendMessage' }; + +export const AudioFileUsage = { + IN_CALL: "incall", + NOTIFICATION: "notification", + RINGTONE: "ringtone", + UNKNOWN: "unknown" +} diff --git a/src/EventHandlers.js b/src/EventHandlers.js index 2dc91f6..26f48df 100644 --- a/src/EventHandlers.js +++ b/src/EventHandlers.js @@ -242,6 +242,26 @@ const CameraSwitchError = { }; +/** + * @property {string} name - Name of the event + * @property {Voximplant.Hardware.AudioFile} audioFile - Audio file that triggered the event + * @property {boolean} result - True if the audio file has started successfully + * @property {number} error - Error code on iOS if the audio file failed to start + */ +const AudioFileStarted = { + +}; + +/** + * @property {string} name - Name of the event + * @property {Voximplant.Hardware.AudioFile} audioFile - Audio file that triggered the event + * @property {boolean} result - True if the audio file has stopped successfully + * @property {number} error - Error code on iOS if the audio file failed to stop + */ +const AudioFileStopped = { + +}; + /** * @property {Voximplant.Messaging.MessengerAction} action - Action that triggered this event. * @property {Voximplant.Messaging.MessengerEventTypes} eventType - Messenger event type. @@ -381,4 +401,4 @@ const RetransmitEvent = { */ const ErrorEvent = { -}; \ No newline at end of file +}; diff --git a/src/hardware/AudioFile.js b/src/hardware/AudioFile.js new file mode 100644 index 0000000..df9b4cf --- /dev/null +++ b/src/hardware/AudioFile.js @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +'use strict'; +import { + Platform, + NativeModules, + NativeEventEmitter, + DeviceEventEmitter, +} from 'react-native'; +import AudioFileEventTypes from "./AudioFileEventTypes"; + +const AudioFileModule = NativeModules.VIAudioFileModule; +const EventEmitter = Platform.select({ + ios: new NativeEventEmitter(AudioFileModule), + android: DeviceEventEmitter, +}); + +/** + * @memberof Voximplant.Hardware + * @class AudioFile + * @classdesc Class may be used to play audio files. + */ +export default class AudioFile { + /** + * @member {string} url - HTTP URL of the stream to play + * @memberOf Voximplant.Hardware.AudioFile + */ + url; + /** + * @member {boolean} looped - Indicate if the audio file should be played repeatedly or once + * @memberOf Voximplant.Hardware.AudioFile + */ + looped; + /** + * @member {string} url - Local audio file name + * @memberOf Voximplant.Hardware.AudioFile + */ + name; + + /** + * @ignore + */ + constructor() { + this.listeners = {}; + this.fileId = null; + EventEmitter.addListener('VIAudioFileStarted', this._VIAudioFileStarted); + EventEmitter.addListener('VIAudioFileStopped', this._VIAudioFileStopped); + } + + /** + * Initialize AudioFile instance to play local audio file. + * + * On android, the audio file must be located in resources "raw" folder. + * + * @param {string} name - Local audio file name + * @param {string} type - Local audio file type/format, for example ".mp3" + * @param {Voximplant.Hardware.AudioFileUsage} usage - Audio file usage mode. ANDROID ONLY. + * @return {Promise} + * @memberof Voximplant.Hardware.AudioFile + */ + initWithLocalFile(name, type, usage) { + return new Promise((resolve, reject) => { + this.name = name; + AudioFileModule.initWithFile({ + name: name, + type: type, + usage: usage, + }, (fileId, error) => { + if (error) { + reject(error); + } else { + this.fileId = fileId; + resolve(); + } + }); + }) + } + + /** + * Initialize AudioFile to play a stream from a network. + * + * @param {string} url - HTTP URL of the stream to play + * @param {Voximplant.Hardware.AudioFileUsage} usage usage - Audio file usage mode. ANDROID ONLY. + * @return {Promise} + * @memberof Voximplant.Hardware.AudioFile + */ + loadFile(url, usage) { + return new Promise((resolve, reject) => { + this.url = url; + AudioFileModule.loadFile({ + url: url, + usage: usage, + }, (fileId, error) => { + if (error) { + reject(error); + } else { + this.fileId = fileId; + resolve(); + } + }); + }); + } + + /** + * Start playing the audio file repeatedly or once. + * @param {boolean} looped - Indicate if the audio file should be played repeatedly or once + * @return {Promise} + * @memberof Voximplant.Hardware.AudioFile + */ + play(looped) { + this.looped = looped; + return new Promise((resolve, reject) => { + let started = (event) => { + this.off(AudioFileEventTypes.Started, started); + console.log(`AudioFile: received event in start`); + if (event.result) { + resolve(event); + } else { + reject(event); + } + }; + this.on(AudioFileEventTypes.Started, started); + AudioFileModule.play(this.fileId, looped); + }); + } + + /** + * Stop playing of the audio file. + * + * @return {Promise} + * @memberof Voximplant.Hardware.AudioFile + */ + stop() { + return new Promise((resolve, reject) => { + let stopped = (event) => { + console.log(`AudioFile: received event in stop`); + this.off(AudioFileEventTypes.Stopped, stopped); + if (event.result) { + resolve(event); + } else { + reject(event); + } + }; + this.on(AudioFileEventTypes.Stopped, stopped); + AudioFileModule.stop(this.fileId); + }); + } + + /** + * Release all resources allocated to play the audio file. + * + * Must be called even if the audio file was not played. + * + * @memberof Voximplant.Hardware.AudioFile + */ + releaseResources() { + EventEmitter.removeListener('VIAudioFileStarted', this._VIAudioFileStarted); + EventEmitter.removeListener('VIAudioFileStopped', this._VIAudioFileStopped); + AudioFileModule.releaseResources(this.fileId); + } + + /** + * Register a handler for the specified AudioFile event. + * One event can have more than one handler. + * Use the {@link Voximplant.Hardware.AudioFile#off} method to delete a handler. + * @param {Voximplant.Hardware.AudioFileEventTypes} event + * @param {function} handler + * @memberof Voximplant.Hardware.AudioFile + */ + on(event, handler) { + if (!handler || !(handler instanceof Function)) { + console.warn(`AudioFile: on: handler is not a Function`); + return; + } + if (Object.values(AudioFileEventTypes).indexOf(event) === -1) { + console.warn(`AudioFile: on: AudioFileEventTypes does not contain ${event} event`); + return; + } + if (!this.listeners[event]) { + this.listeners[event] = new Set(); + } + this.listeners[event].add(handler); + } + + /** + * Remove a handler for the specified AudioFile event. + * @param {Voximplant.Hardware.AudioFileEventTypes} event + * @param {function} handler - Handler function. If not specified, all handlers for the event will be removed. + * @memberof Voximplant.Hardware.AudioFile + */ + off(event, handler) { + if (!this.listeners[event]) { + return; + } + if (Object.values(AudioFileEventTypes).indexOf(event) === -1) { + console.warn(`AudioFile: off: AudioFileEventTypes does not contain ${event} event`); + return; + } + if (handler && handler instanceof Function) { + this.listeners[event].delete(handler); + } else { + this.listeners[event] = new Set(); + } + } + + /** + * @private + */ + _emit(event, ...args) { + const handlers = this.listeners[event]; + if (handlers) { + for (const handler of handlers) { + handler(...args); + } + } + } + + _VIAudioFileStarted = (event) => { + console.log(`AudioFile: received started event in general handler: ${event.fileId}`); + if (event.fileId === this.fileId) { + delete event.fileId; + event.audioFile = this; + this._emit(AudioFileEventTypes.Started, event); + } + }; + + _VIAudioFileStopped = (event) => { + console.log(`AudioFile: received stopped event in general handler: ${event.fileId}`); + if (event.fileId === this.fileId) { + delete event.fileId; + event.audioFile = this; + this._emit(AudioFileEventTypes.Stopped, event); + } + }; +} diff --git a/src/hardware/AudioFileEventTypes.js b/src/hardware/AudioFileEventTypes.js new file mode 100644 index 0000000..af078bd --- /dev/null +++ b/src/hardware/AudioFileEventTypes.js @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. + */ + +/** + * Audio file events listener to be notified about audio file events + * @memberof Voximplant.Hardware + * @enum {string} + * @type {{Started: string, Stopped: string}} + */ +const AudioFileEventTypes = { + /** + * Invoked when the audio file playing is started. + * Handler function receives {@link EventHandlers.AudioFileStarted} object as an argument. + */ + Started: 'Started', + /** + * Invoked when the audio file playing is stopped. + * Handler function receives {@link EventHandlers.AudioFileStopped} object as an argument. + */ + Stopped: 'Stopped' +}; + +export default AudioFileEventTypes; diff --git a/src/hardware/index.js b/src/hardware/index.js index ffe3f96..157ada0 100644 --- a/src/hardware/index.js +++ b/src/hardware/index.js @@ -12,13 +12,18 @@ import AudioDeviceManager from './AudioDeviceManager'; import AudioDeviceEvents from './AudioDeviceEvents'; import CameraManager from './CameraManager'; import CameraEvents from './CameraEvents'; -import {AudioDevice, CameraType} from "../Enums"; +import AudioFile from "./AudioFile"; +import AudioFileEventTypes from "./AudioFileEventTypes"; +import {AudioDevice, CameraType, AudioFileUsage} from "../Enums"; export { AudioDeviceManager, AudioDeviceEvents, CameraManager, CameraEvents, + AudioFile, + AudioFileEventTypes, AudioDevice, - CameraType + CameraType, + AudioFileUsage }