Commit a6563206 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

call: add options

This patch adds the following call options:
- mute audio
- mute video
- pause call
- switch camera

Change-Id: Ia02deae0e86e117d9c262ed3db2ec223414a4c92
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent 70702cba
......@@ -238,7 +238,6 @@
62A88D371F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */; };
62A88D391F6C323500F8AB18 /* PresenceAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */; };
62A88D3B1F6C3ACC00F8AB18 /* PresenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */; };
62AA15B21FF422810064A063 /* src in Resources */ = {isa = PBXBuildFile; fileRef = 62AA15B11FF422810064A063 /* src */; };
62AA15BF1FFC36840064A063 /* VideoAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 62AA15BE1FFC36840064A063 /* VideoAdapter.mm */; };
62AA15C31FFC39C80064A063 /* VideoAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62AA15C21FFC39C80064A063 /* VideoAdapterDelegate.swift */; };
62AA15CA1FFD3D7E0064A063 /* VideoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62AA15C91FFD3D7E0064A063 /* VideoService.swift */; };
......@@ -512,7 +511,6 @@
62A88D361F6C2ED400F8AB18 /* PresenceAdapterDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceAdapterDelegate.swift; sourceTree = "<group>"; };
62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PresenceAdapter.mm; sourceTree = "<group>"; };
62A88D3A1F6C3ACC00F8AB18 /* PresenceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceService.swift; sourceTree = "<group>"; };
62AA15B11FF422810064A063 /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = ../../daemon/src; sourceTree = "<group>"; };
62AA15BD1FFC366D0064A063 /* VideoAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VideoAdapter.h; sourceTree = "<group>"; };
62AA15BE1FFC36840064A063 /* VideoAdapter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VideoAdapter.mm; sourceTree = "<group>"; };
62AA15C21FFC39C80064A063 /* VideoAdapterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoAdapterDelegate.swift; sourceTree = "<group>"; };
......
......@@ -37,5 +37,6 @@
- (NSDictionary<NSString*,NSString*>*)callDetailsWithCallId:(NSString*)callId;
- (NSArray<NSString*>*)calls;
- (void) sendTextMessageWithCallID:(NSString*)callId message:(NSDictionary*)message accountId:(NSString*)accountId sMixed:(bool)isMixed;
- (BOOL) muteMedia:(NSString*)callId mediaType:(NSString*)media muted:(bool)muted;
@end
......@@ -118,7 +118,7 @@ static id <CallsAdapterDelegate> _delegate;
bool muted) {
if (CallsAdapter.delegate) {
NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
[CallsAdapter.delegate muteVideoWithCall: callIdString mute: muted];
[CallsAdapter.delegate videoMutedWithCall: callIdString mute: muted];
}
}));
......@@ -126,8 +126,7 @@ static id <CallsAdapterDelegate> _delegate;
bool muted) {
if (CallsAdapter.delegate) {
NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
[CallsAdapter.delegate muteAudioWithCall: callIdString mute: muted];
[CallsAdapter.delegate audioMutedWithCall: callIdString mute: muted];
}
}));
......@@ -175,6 +174,10 @@ static id <CallsAdapterDelegate> _delegate;
return [Utils vectorToArray:calls];
}
- (BOOL)muteMedia:(NSString*)callId mediaType:(NSString*)media muted:(bool)muted {
return muteLocalMedia(std::string([callId UTF8String]), std::string([media UTF8String]), muted);
}
#pragma mark AccountAdapterDelegate
+ (id <CallsAdapterDelegate>)delegate {
......
......@@ -167,7 +167,7 @@
<constraint firstAttribute="height" constant="50" id="RC6-BN-8ID"/>
<constraint firstAttribute="width" constant="50" id="wXp-ed-p3H"/>
</constraints>
<state key="normal" image="mute_video"/>
<state key="normal" image="video_running"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
......@@ -187,7 +187,7 @@
<constraint firstAttribute="width" constant="50" id="KAL-kW-Ak4"/>
<constraint firstAttribute="height" constant="50" id="guZ-o3-hkm"/>
</constraints>
<state key="normal" image="mute_audio"/>
<state key="normal" image="audio_running"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
......@@ -326,8 +326,8 @@
<outlet property="infoContainer" destination="3RN-4M-qR4" id="CUO-h6-mFf"/>
<outlet property="infoLabelConstraint" destination="72y-vN-PbI" id="sGV-5n-H9t"/>
<outlet property="mainView" destination="QpJ-Sx-9dG" id="0y9-R4-q5W"/>
<outlet property="muteAudioButton" destination="UHr-JD-OOn" id="nWm-1S-3Im"/>
<outlet property="muteVideoButton" destination="Ezt-ru-2cK" id="NXD-9b-dCI"/>
<outlet property="muteAudioButton" destination="Ezt-ru-2cK" id="0Cf-gc-w9s"/>
<outlet property="muteVideoButton" destination="UHr-JD-OOn" id="ciL-mV-cxA"/>
<outlet property="nameLabel" destination="73Y-N1-Yga" id="XcQ-V6-ZrF"/>
<outlet property="pauseCallButton" destination="f4r-vz-8h0" id="rqE-oX-VxA"/>
<outlet property="profileImageView" destination="fnt-PQ-Q6P" id="MgB-Ev-bTc"/>
......@@ -338,30 +338,13 @@
</objects>
<point key="canvasLocation" x="-74.400000000000006" y="131.78410794602701"/>
</scene>
<!--View Controller-->
<scene sceneID="zIN-Yk-WAp">
<objects>
<viewController id="n3h-fo-GaE" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="VPJ-zk-yXm"/>
<viewControllerLayoutGuide type="bottom" id="DbS-bx-Lwr"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="htX-h9-kf0">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fSK-jF-f8o" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
<resources>
<image name="audio_running" width="24" height="24"/>
<image name="ic_contact_picture" width="128" height="128"/>
<image name="mute_audio" width="24" height="24"/>
<image name="mute_video" width="24" height="24"/>
<image name="pause_call" width="24" height="24"/>
<image name="stop_call" width="24" height="24"/>
<image name="switch_camera" width="24" height="24"/>
<image name="video_running" width="24" height="24"/>
</resources>
</document>
......@@ -76,13 +76,33 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
}
func setupBindings() {
//Cancel button action
//bind actions
self.cancelButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.removeFromScreen()
self?.viewModel.cancelCall()
}).disposed(by: self.disposeBag)
self.muteAudioButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.muteAudio()
}).disposed(by: self.disposeBag)
self.muteVideoButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.muteVideo()
}).disposed(by: self.disposeBag)
self.pauseCallButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.pauseCall()
}).disposed(by: self.disposeBag)
self.switchCameraButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.switchCamera()
}).disposed(by: self.disposeBag)
//Data bindings
self.viewModel.contactImageData.asObservable()
.observeOn(MainScheduler.instance)
......@@ -125,7 +145,6 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
.subscribe(onNext: { [weak self] frame in
if let image = frame {
DispatchQueue.main.async {
self?.callView.isHidden = false
self?.incomingVideo.image = image
}
}
......@@ -142,12 +161,37 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
}).disposed(by: self.disposeBag)
self.viewModel.showCallOptions
.subscribeOn(MainScheduler.instance)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { show in
if show {
self.showContactInfo()
}
}).disposed(by: self.disposeBag)
self.viewModel.videoButtonState
.observeOn(MainScheduler.instance)
.bind(to: self.muteVideoButton.rx.image())
.disposed(by: self.disposeBag)
self.viewModel.videoMuted
.observeOn(MainScheduler.instance)
.bind(to: self.capturedVideo.rx.isHidden)
.disposed(by: self.disposeBag)
self.viewModel.audioButtonState
.observeOn(MainScheduler.instance)
.bind(to: self.muteAudioButton.rx.image())
.disposed(by: self.disposeBag)
self.viewModel.callButtonState
.observeOn(MainScheduler.instance)
.bind(to: self.pauseCallButton.rx.image())
.disposed(by: self.disposeBag)
self.viewModel.callPaused
.observeOn(MainScheduler.instance)
.bind(to: self.callView.rx.isHidden)
.disposed(by: self.disposeBag)
}
func removeFromScreen() {
......
......@@ -107,6 +107,8 @@ class CallViewModel: Stateable, ViewModel {
}).asDriver(onErrorJustReturn: "")
}()
//let timer: Observable<String>
lazy var callDuration: Driver<String> = {
let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.takeUntil(self.callService.currentCall
......@@ -116,9 +118,10 @@ class CallViewModel: Stateable, ViewModel {
})
.map({ elapsed in
return CallViewModel.formattedDurationFrom(interval: elapsed)
})
return self.callService.currentCall.filter({ call in
return call.state == .current
}).share()
return self.callService.currentCall.filter({ [weak self] call in
return call.state == .current &&
call.callId == self?.call?.callId
}).flatMap({ _ in
return timer
}).asDriver(onErrorJustReturn: "")
......@@ -126,7 +129,9 @@ class CallViewModel: Stateable, ViewModel {
lazy var bottomInfo: Observable<String> = {
return callService.currentCall.map({ [weak self] call in
if call.state == .connecting || call.state == .ringing && call.callType == .outgoing && call.callId == self?.call?.callId {
if call.state == .connecting || call.state == .ringing &&
call.callType == .outgoing &&
call.callId == self?.call?.callId {
return L10n.Calls.calling
} else if call.state == .over {
return L10n.Calls.callFinished
......@@ -137,8 +142,9 @@ class CallViewModel: Stateable, ViewModel {
}()
lazy var showCallOptions: Observable<Bool> = {
return Observable.combineLatest(self.callIsActive, self.screenTapped.asObservable()) {(active, tapped) -> Bool in
return active && tapped
return Observable.combineLatest(self.callIsActive,
self.screenTapped.asObservable()) {(active, tapped) -> Bool in
return active && tapped
}
}()
......@@ -152,6 +158,75 @@ class CallViewModel: Stateable, ViewModel {
var screenTapped = BehaviorSubject(value: false)
lazy var videoButtonState: Observable<UIImage?> = {
let onImage = UIImage(asset: Asset.videoRunning)
let offImage = UIImage(asset: Asset.videoMuted)
return self.videoMuted.map({ muted in
if muted {
return offImage
}
return onImage
})
}()
lazy var videoMuted: Observable<Bool> = {
return self.callService.currentCall.filter({ call in
call.callId == self.call?.callId &&
call.state == .current
}).map({call in
return call.videoMuted
})
}()
lazy var audioButtonState: Observable<UIImage?> = {
let onImage = UIImage(asset: Asset.audioRunning)
let offImage = UIImage(asset: Asset.audioMuted)
return self.audioMuted.map({ muted in
if muted {
return offImage
}
return onImage
})
}()
lazy var audioMuted: Observable<Bool> = {
return self.callService.currentCall.filter({ call in
call.callId == self.call?.callId &&
call.state == .current
}).map({call in
return call.audioMuted
})
}()
lazy var callButtonState: Observable<UIImage?> = {
let unpauseCall = UIImage(asset: Asset.unpauseCall)
let pauseCall = UIImage(asset: Asset.pauseCall)
return self.callPaused.map({ muted in
if muted {
return unpauseCall
}
return pauseCall
})
}()
lazy var callPaused: Observable<Bool> = {
return self.callService.currentCall.filter({ call in
call.callId == self.call?.callId &&
(call.state == .hold ||
call.state == .unhold ||
call.state == .current)
}).map({call in
if call.state == .hold ||
(call.state == .current && call.peerHolding) {
return true
}
return false
})
}()
required init(with injectionBag: InjectionBag) {
self.callService = injectionBag.callService
self.contactsService = injectionBag.contactsService
......@@ -206,8 +281,9 @@ class CallViewModel: Stateable, ViewModel {
guard let photo = profile.photo else {
return
}
guard let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? else {
return
guard let data = NSData(base64Encoded: photo,
options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? else {
return
}
self.contactImageData.value = data
}
......@@ -215,4 +291,47 @@ class CallViewModel: Stateable, ViewModel {
func respondOnTap() {
self.screenTapped.onNext(true)
}
// MARK: call options
func pauseCall() {
guard let call = self.call else {
return
}
if call.state == .current {
self.callService.hold(callId: call.callId)
.subscribe(onCompleted: { [weak self] in
self?.log.info("call paused")
}, onError: { [weak self](error) in
self?.log.info(error)
}).disposed(by: self.disposeBag)
} else if call.state == .hold {
self.callService.unhold(callId: call.callId)
.subscribe(onCompleted: { [weak self] in
self?.log.info("call unpaused")
}, onError: { [weak self](error) in
self?.log.info(error)
}).disposed(by: self.disposeBag)
}
}
func muteAudio() {
guard let call = self.call else {
return
}
let mute = !call.audioMuted
self.callService.muteAudio(call: call.callId, mute: mute)
}
func muteVideo() {
guard let call = self.call else {
return
}
let mute = !call.videoMuted
self.callService.muteVideo(call: call.callId, mute: mute)
}
func switchCamera() {
self.videoService.switchCamera()
}
}
......@@ -48,6 +48,8 @@ struct ColorAsset {
enum Asset {
static let accountIcon = ImageAsset(name: "account_icon")
static let addPerson = ImageAsset(name: "add_person")
static let audioMuted = ImageAsset(name: "audio_muted")
static let audioRunning = ImageAsset(name: "audio_running")
static let backgroundRing = ImageAsset(name: "background_ring")
static let blockIcon = ImageAsset(name: "block_icon")
static let callButton = ImageAsset(name: "call_button")
......@@ -57,13 +59,14 @@ enum Asset {
static let fallbackAvatar = ImageAsset(name: "fallback_avatar")
static let icContactPicture = ImageAsset(name: "ic_contact_picture")
static let moreSettings = ImageAsset(name: "more_settings")
static let muteAudio = ImageAsset(name: "mute_audio")
static let muteVideo = ImageAsset(name: "mute_video")
static let pauseCall = ImageAsset(name: "pause_call")
static let ringLogo = ImageAsset(name: "ring_logo")
static let settingsIcon = ImageAsset(name: "settings_icon")
static let stopCall = ImageAsset(name: "stop_call")
static let switchCamera = ImageAsset(name: "switch_camera")
static let unpauseCall = ImageAsset(name: "unpause_call")
static let videoMuted = ImageAsset(name: "video_muted")
static let videoRunning = ImageAsset(name: "video_running")
// swiftlint:disable trailing_comma
static let allColors: [ColorAsset] = [
......@@ -71,6 +74,8 @@ enum Asset {
static let allImages: [ImageAsset] = [
accountIcon,
addPerson,
audioMuted,
audioRunning,
backgroundRing,
blockIcon,
callButton,
......@@ -80,13 +85,14 @@ enum Asset {
fallbackAvatar,
icContactPicture,
moreSettings,
muteAudio,
muteVideo,
pauseCall,
ringLogo,
settingsIcon,
stopCall,
switchCamera,
unpauseCall,
videoMuted,
videoRunning,
]
// swiftlint:enable trailing_comma
@available(*, deprecated, renamed: "allImages")
......
......@@ -63,6 +63,7 @@ class CallModel {
var accountId: String = ""
var audioMuted: Bool = false
var videoMuted: Bool = false
var peerHolding: Bool = false
var stateValue = CallState.unknown.rawValue
var callTypeValue = CallType.missed.rawValue
......@@ -140,5 +141,9 @@ class CallModel {
if let accountId = dictionary[CallDetailKey.accountIdKey.rawValue] {
self.accountId = accountId
}
if let peerHolding = dictionary[CallDetailKey.peerHoldingKey.rawValue]?.toBool() {
self.peerHolding = peerHolding
}
}
}
......@@ -2,17 +2,17 @@
"images" : [
{
"idiom" : "universal",
"filename" : "ic_volume_mute_white.png",
"filename" : "ic_mic_off_white.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_volume_mute_white_2x.png",
"filename" : "ic_mic_off_white_2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_volume_mute_white_3x.png",
"filename" : "ic_mic_off_white_3x.png",
"scale" : "3x"
}
],
......
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_mic_white.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_mic_white_2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_mic_white_3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
......@@ -2,17 +2,17 @@
"images" : [
{
"idiom" : "universal",
"filename" : "ic_replay_white.png",
"filename" : "rotate_camera.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_replay_white_2x.png",
"filename" : "rotate_camera2.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_replay_white_3x.png",
"filename" : "rotate_camera3.png",
"scale" : "3x"
}
],
......
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_play_arrow_white.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_play_arrow_white_2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_play_arrow_white_3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_videocam_off_white.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_videocam_off_white_2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_videocam_off_white_3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
\ No newline at end of file
......@@ -25,6 +25,6 @@
func receivingCall(withAccountId accountId: String, callId: String, fromURI uri: String)
func newCallStarted(withAccountId accountId: String, callId: String, toURI uri: String)
func callPlacedOnHold(withCallId callId: String, holding: Bool)
func muteAudio(call callId: String, mute: Bool)
func muteVideo(call callId: String, mute: Bool)
func audioMuted(call callId: String, mute: Bool)
func videoMuted(call callId: String, mute: Bool)
}
......@@ -32,6 +32,15 @@ enum CallServiceError: Error {
case placeCallFailed
}
enum MediaType: String, CustomStringConvertible {
case audio = "MEDIA_TYPE_AUDIO"
case video = "MEDIA_TYPE_VIDEO"
var description: String {
return self.rawValue
}
}
struct Base64VCard {
var data: [Int: String] //The key is the number of vCard part
var partsReceived: Int
......@@ -147,6 +156,41 @@ class CallsService: CallsAdapterDelegate {
})
}
func muteAudio(call callId: String, mute: Bool) {
self.callsAdapter
.muteMedia(callId,
mediaType: String(describing: MediaType.audio),
muted: mute)
}
func muteVideo(call callId: String, mute: Bool) {
self.callsAdapter
.muteMedia(callId,
mediaType: String(describing: MediaType.video),
muted: mute)
}
func sendVCard(callID: String, accountID: String) {
if accountID.isEmpty || callID.isEmpty {
return
}
VCardUtils.loadVCard(named: VCardFiles.myProfile.rawValue,
inFolder: VCardFolders.profile.rawValue)
.subscribe(onSuccess: { [unowned self] card in
VCardUtils.sendVCard(card: card,
callID: callID,
accountID: accountID,
sender: self)
}).disposed(by: disposeBag)
}
func sendChunk(callID: String, message: [String: String], accountId: String) {
self.callsAdapter.sendTextMessage(withCallID: callID,
message: message,
accountId: accountId,
sMixed: true)