Voice call based on LinPhone for iOS

Introduction to the concepts involved in voice calls:

VoIP (Voice over Internet Protocol) is to first digitize voice signals and compress them into frames, convert them into IP data packets and transmit them on the network, so as to complete the business of voice calls. It is a communication technology that uses IP protocol to transmit voice data. VoIP calls The media stream in China uses UDP. Once the network quality is not good, the voice quality will be delayed or intermittent, but the speed is fast.

Linphone is an open source VoIP phone tool based on standard SIP, and an open source Internet phone system that follows the GPL. It is a software that allows you to query your friend's IP through the Internet and call him through the IP. It has very powerful functions and supports both desktop systems and mobile terminals, as well as WEB browsers. Using linphone, we can communicate freely on the Internet through voice, video, instant text messages.

Linphone is based on the ilbc codec; the ilbc codec compression ratio is relatively large, probably between 1/10 and 1/9. That is to say, if the voice data is 20kb per second, it will be 2kb/s after encoding, which is very small and very conducive to network transmission. It uses a C library, speex, to implement the echo cancellation module. The biggest advantage of Linphone is that it supports all platforms, android, ios, winphone, windows, linux, mac osx, web all support


Voice call development process:

1) Cocoapods integration

Cocoapods needs to import open source tripartite libraries and versions: 'linphone-sdk', '4.2' 'CocoaAsyncSocket', '7.6.5'

linphone-sdk: for the actual voice call function

CocoaAsyncSocket: Used to establish a link with the background, assign network agents and monitor call status changes (pay attention to how many agents are opened, this will involve a high price...)

2) A brief introduction to the background

Because it is not my background, I can only give a brief introduction. The language we use in the background is C++. If you don’t want to pay too much cost and have relatively high efficiency, C++ is undoubtedly a good choice. During a voice call, there are three types of interactions between the client and the background:

1. Send a request interaction to the server in charge of account information, and obtain the data model to be used for the second and third types of interaction with the second server

2. According to the data model obtained from the first type of interaction with the server, LinPhoneSDK performs the second type of interaction with the server to establish a UDP link for voice calls

3. According to the data model obtained by the first type of interaction with the background, use the socket network library GCDAsyncSocket based on the TCP/IP protocol on the client side to perform the third type of interaction with the background, establish a link, and maintain a long link for obtaining network agents and changing calls state

3) Analysis of the construction of each state of the client voice call

1. Login

I. Send a request to the server in charge of account information, pass in the corresponding URL, account number and password, use AES encryption (key and offset are agreed with the background), and obtain the data model to be used next

//向主管账号信息的服务器发送请求,建立链接,获取接下来要用到的数据模型
YGCallManager *manager = [YGCallManager instance];
[manager initSdk:model success:^(NSDictionary * _Nullable responseObject) {
      NSLog(@"initSdk:%@", responseObject);
      //与socket服务器和LinPhone服务器建立链接
     [[YGCallManager instance] login];
   } failure:^(NSError * _Nullable error) {
      NSLog(@"initSdk:%@", error);
}];

II. LinPhoneSDK establishes the second type of interaction with the server, establishes a UDP connection, and serves voice calls

ESSipManager *sipManager = [ESSipManager instance];
[sipManager login:@"你的LoginNum" password:@"你的pwd" displayName:@"" domain:@"你的sipIP:sipPort" port:@"你的sipPort" withTransport:@"UDP"];

III. When the Linphone login is successful, a successful callback will be invoked. At this time, the socket establishes a third type of connection with the server to serve for opening network seats and switching call status

sipManager.linphoneBlock = ^(NSInteger registrationState) {
   if (self.linphoneRegistrationState != registrationState) {
         self.linphoneRegistrationState = registrationState;
           if (registrationState == 2) {
               //2.socket连接
               self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
               NSError *socketError = nil;
               if (![self.clientSocket connectToHost:self.tcpModel.TransferIP onPort:self.tcpModel.TransferPort withTimeout:-1 error:&socketError]) {
                   if (socketError) {
                       NSLog(@"连接服务器失败:%@", socketError.localizedDescription);
                       return;
                }
      }

The switch call status service mentioned here, that is to say, the client needs to know about the connection and hang up, so as to do the corresponding processing. The client accepts the status change mainly through the proxy of the socket.

Main function code:

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    //接收登录服务消息
    if ([lastString containsString:@"LOGIN_SUCCEED"]) {
        //向app端推送EventLogin成功消息
        if (self.eventBlock) {
            NSDictionary *dic = @{@"msg":@"登录成功"};
            self.eventBlock(EVENTLogin, YES, dic);
        }
   //通知服务器当前账号的通道已经被占用
        NSData *data4 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"与后台协商好的信令服务"]];
        [self.clientSocket writeData:data4 withTimeout:-1 tag:4];
    }
}

IV. Problems encountered in the login function: When the login was implemented at the beginning, it was found that multiple logins would often report that the user was busy and could not log in. Later, after joint debugging with the background, it was found that the single sign-on function (that is, this login will kick out the previous login) was not perfect, and the socket interface was optimized and upgraded. The process was modified to: Send the preparation status to the voice call server = > Send an initialization message to the assigned agent server => send a clear channel message to the server = "send a login message to the server

Part of the code to realize the function (mainly send the negotiated socket with the background):

//登录
- (void)ygTcpLogin {
    //发送准备状态给服务器
    NSData *data = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
    [self.clientSocket writeData:data withTimeout:-1 tag:0];
    //发送初始化消息给转发服务器
    NSData *data1 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
    [self.clientSocket writeData:data1 withTimeout:-1 tag:1];
    //发送清理通道消息给服务器
    NSData *data2 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
    [self.clientSocket writeData:data2 withTimeout:-1 tag:2];
    //发送登录消息给服务器
    NSData *data3 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"与后台约定好的信令服务"];
    [self.clientSocket writeData:data3 withTimeout:-1 tag:3];
}

V. Problem 2 encountered when doing the login function: Linphone and the voice call background cannot establish a connection on iOS16 or later versions, because the port may be occupied. The solution is to assign an unoccupied random port to LinPhone each time it connects

Part of the code for the function implemented:

    [LinphoneManager.instance resetLinphoneCore];

    LinphoneProxyConfig *config = linphone_core_create_proxy_config(LC);
    LinphoneAddress *addr = linphone_address_new([NSString stringWithFormat:@"sip:%@@%@",username, domain].UTF8String);
    LinphoneAddress *tmpAddr = linphone_address_new([NSString stringWithFormat:@"sip:%@",domain].UTF8String);
    linphone_address_set_username(addr, username.UTF8String);
    linphone_address_set_port(addr, linphone_address_get_port(tmpAddr));
    linphone_address_set_domain(addr, linphone_address_get_domain(tmpAddr));
    if (displayName && ![displayName isEqualToString:@""]) {
        linphone_address_set_display_name(addr, displayName.UTF8String);
    }
    linphone_proxy_config_set_identity_address(config, addr);
    if (transport) {
        linphone_proxy_config_set_route(
                                        config,
                                        [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
                                        .UTF8String);
        linphone_proxy_config_set_server_addr(
                                              config,
                                              [NSString stringWithFormat:@"%s;transport=%s", domain.UTF8String, transport.lowercaseString.UTF8String]
                                              .UTF8String);
    }
    
    linphone_proxy_config_enable_publish(config, FALSE);
    linphone_proxy_config_enable_register(config, TRUE);
    
    LinphoneAuthInfo *info =
    linphone_auth_info_new(linphone_address_get_username(addr), // username
                           NULL,                                // user id
                           password.UTF8String,                        // passwd
                           NULL,                                // ha1
                           linphone_address_get_domain(addr),   // realm - assumed to be domain
                           linphone_address_get_domain(addr)    // domain
                           );
    linphone_core_add_auth_info(LC, info);
    linphone_address_unref(addr);
    linphone_address_unref(tmpAddr);
    //分配随机端口
    LCSipTransports transportValue = {-1,-1,-1,-1};
    
    if (linphone_core_set_sip_transports(LC, &transportValue)) {
        NSLog(@"cannot set transport");
    }

2. Outbound call

I. Outgoing call function process: client dials => informs the server to put the current account in a busy state and assigns a seat => the client receives the socket callback of whether the seat transfer is successful and performs corresponding processing, and the server (voice background) sends Send relevant information such as the virtual number and the called number to Lin Phone, and notify Lin Phone to make an Internet call 

Part of the code to realize the outbound call function (the client mainly sends the information of itself and the callee to the background, and LinPhone’s call is actually triggered by the background):

//当前账号置为繁忙
NSData *data8 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"繁忙信令"];
[self.clientSocket writeData:data8 withTimeout:-1 tag:8];

//分配座席,并建立语音通话链接
NSData *data9 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:@"座席建立链接信令"];
[self.clientSocket writeData:data9 withTimeout:-1 tag:9];

II. After the outbound action is completed, it is necessary to wait for the callee to see if it answers, which requires monitoring the change of the call state, which is realized by the state change macro provided by LinPhone, and we need to notify the consequence Monitoring, once the status change is detected, call LinPhone related API to answer or hang up.

Part of the code that implements the function:

//监听通话状态变化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onCallUpdate:) name:kLinphoneCallUpdate object:nil];

- (void) onCallUpdate: (NSNotification*) notification {
    NSDictionary* userInfo = [notification userInfo];
    NSValue* c = [userInfo valueForKey:@"call"];
    //    int state = (int)[userInfo valueForKey:@"state"];
    LinphoneCallState state = [[userInfo objectForKey:@"state"] intValue];
    NSString* message = [userInfo valueForKey:@"message"];
    NSLog(@"========== state: %d, message: %@", state, message);
    LinphoneCall* call = c.pointerValue;
    
    NSDictionary *dict = @{@"call" : [NSValue valueWithPointer:call],
                           @"state" : [NSNumber numberWithInt:state],
                           @"message" : message};
    
    switch (state) {
        //接听
        case LinphoneCallIncomingReceived: {
            [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_COMMING object: self userInfo:dict];
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
            case LinphoneCallOutgoingInit:
            case LinphoneCallConnected:
            case LinphoneCallStreamsRunning: {
                // check video
                if (![self isVideoEnabled:call]) {
                    const LinphoneCallParams *param = linphone_call_get_current_params(call);
                    const LinphoneCallAppData *callAppData =
                    (__bridge const LinphoneCallAppData *)(linphone_call_get_user_data(call));
                    if (state == LinphoneCallStreamsRunning && callAppData->videoRequested &&
                        linphone_call_params_low_bandwidth_enabled(param)) {
                        // too bad video was not enabled because low bandwidth
                        
                        NSLog(@"带宽太低,无法开启视频通话");
                        
                        callAppData->videoRequested = FALSE; /*reset field*/
                    }
                }
                [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_STREAM_UPDATE object:self userInfo:dict];
                break;
            }
            case LinphoneCallUpdatedByRemote: {
                const LinphoneCallParams *current = linphone_call_get_current_params(call);
                const LinphoneCallParams *remote = linphone_call_get_remote_params(call);
                
                /* remote wants to add video */
                if ((linphone_core_video_display_enabled([LinphoneManager getLc]) && !linphone_call_params_video_enabled(current) &&
                     linphone_call_params_video_enabled(remote)) &&
                    (!linphone_core_get_video_policy([LinphoneManager getLc])->automatically_accept ||
                     (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) &&
                      floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max))) {
                         linphone_core_defer_call_update([LinphoneManager getLc], call);
                         
                         
                         [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_REMOTE_OPEN_CEMERA object: self userInfo:dict];
                         
                         //                     [self allowToOpenCameraByRemote:call];
                         
                     } else if (linphone_call_params_video_enabled(current) && !linphone_call_params_video_enabled(remote)) {
                         
                     }
                break;
            }
            case LinphoneCallUpdating:
            break;
            case LinphoneCallPausing:
            case LinphoneCallPaused:
            break;
            case LinphoneCallPausedByRemote:
            break;
        //挂断
        case LinphoneCallEnd: {//LinphoneCallEnd
            [NSNotificationCenter.defaultCenter postNotificationName:ES_ON_CALL_END object: self userInfo:NULL];
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
        case LinphoneCallReleased: {
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
        }
        case LinphoneCallError:
        default:
            break;
    }
}

When the callBlock is triggered, the corresponding answering method of LinPhone will be called:

- (void)acceptCall:(LinphoneCall *)call evenWithVideo:(BOOL)video {
	LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
	if (!lcallParams) {
		LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
		return;
	}

	if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
		bool low_bandwidth = self.network == network_2g;
		if (low_bandwidth) {
			LOGI(@"Low bandwidth mode");
		}
		linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
	}
	linphone_call_params_enable_video(lcallParams, video);

	linphone_call_accept_with_params(call, lcallParams);
    linphone_call_params_unref(lcallParams);
}

At this point, a call-answer process has been established, and the next step is to hang up, because if there is an answer, there must be a hang-up

3. hang up

There are two types of hangups, one is hung up by one's own party, and the other is hung up by the other party. These are two different ways of handling

I. Your side hangs up

Process: Your party clicks to hang up => notify the background that your party has hung up, and the hangup process will be executed soon => notify LinPhone to execute the hangup => LinPhone executes the hangup and then informs the voice server to execute the hangup process => the voice server informs the long connection with the socket The server hangs up processing => the server that has a long connection with the socket will call back to the client, set the complete call end socket signaling to the socket server, and set the own side to be in an idle state => the voice server will send a call complete end to Linphone, Linphone cleans up the call data and notifies the client to clean up the local call data

Start notifying the background to hang up your own party and notify Linphone to end the call:

 NSData *data20 = [self sendMsgWithName:self.tcpModel.PhoneNo type:@"" targetType:@"" msg:[NSString stringWithFormat:@"开始结束通话信令服务"];

  LinphoneCore* lc = [LinphoneManager getLc];
    LinphoneCall* currentcall = linphone_core_get_current_call(lc);
    if (linphone_core_is_in_conference(lc) || // In conference
        (linphone_core_get_conference_size(lc) > 0) // Only one conf
        ) {
        linphone_core_terminate_conference(lc);
    } else if(currentcall != NULL) { // In a call
//        linphone_core_terminate_call(lc, currentcall);
        linphone_call_terminate(currentcall);
    } else {
        const MSList* calls = linphone_core_get_calls(lc);
        if (ms_list_size(calls) == 1) { // Only one call
//            linphone_core_terminate_call(lc,(LinphoneCall*)(calls->data));
            linphone_call_terminate((LinphoneCall *)(calls->data));
        }
    }

The background receives the signal to end the call, and notifies the client to transmit the completed socket information, and the client sends to the background part of the code that it sets as idle:

//设置话后
        NSData *data11 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"话后信令"]];
        [self.clientSocket writeData:data11 withTimeout:-1 tag:11];
     //设置话后提交
        NSData *data12 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"话后提交信令"]];
        [self.clientSocket writeData:data12 withTimeout:-1 tag:12];
    //空闲
        NSData *data19 = [self sendMsgWithName:self.tcpModel.PhoneNo type:@"FlyCcs" targetType:@"FlyCcs" msg:[NSString stringWithFormat:@"空闲信令"]];
        [self.clientSocket writeData:data19 withTimeout:-1 tag:19];

LinPhone sends a notification to completely eliminate the call, and the client executes part of the code for data cleaning of the local call:

case LinphoneCallReleased: {
            if (self.callBlock) {
                self.callBlock((NSInteger)state, dict);
            }
            break;
 }


sipManager.callBlock = ^(NSInteger callState, NSDictionary *dict) {
        if (self->_ISDIALOUT == 0) {
            return;
        }
        if (callState == 1) {
            NSLog(@"接听--callState == 1");
            LinphoneCall *call = [dict[@"call"] pointerValue];
            [weakManager acceptCall:(ESCall *)call];
        } else if (callState == 18) {
            NSLog(@"挂断--callState == 18");
            //向app端推送EventHangup
//            if (self.eventBlock) {
//                NSDictionary *dic = @{@"CallId":@"",
//                                      @"CallTime":@"",
//                                      @"CodeCause":@"",
//                                      @"ConnectTime":@"",
//                                      @"EndTime":@"",
//                                      @"Rebark":self.Rebark ? self.Rebark : @"",
//                                      @"SignalIng":@"",
//                                      @"WavFile":self.WavFile ? self.WavFile : @""
//                };
//                self.eventBlock(EVENTHangup, YES, dic);
//            }
            //清空本地数据
            self->_callModel = nil;
            self->_CallId = nil;
            self->_ConnectTime = nil;
            self->_Rebark = nil;
            self->_WavFile = nil;
            self->_ISDIALOUT = 10;
   }

II. The other party hangs up 

Process; the other party hangs up => LinPhone notifies the client to hang up => LinPhone notifies the server to hang up the call => the server notifies the client to initiate the end of the call and set it as idle signaling => the server notifies LinPhone to cancel the call data => LinPhone Notify the client to delete the call data

The key code to realize the function is the same as the hang-up code of your own side, so it will not be listed here

4. Incoming call

Process: LinPhone receives incoming call notification => notifies server and client callback receives incoming call => server notifies client of incoming call and related incoming call messages through socket agent => client sends busy signaling and forwards assigned agent signaling => call channel established

Part of the code to realize the function (the part repeated with the above is omitted):


//繁忙信令
NSData *data13 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"当前账号置为繁忙的信令"]]; //与data10一样;
        [self.clientSocket writeData:data13 withTimeout:-1 tag:13];

//分配座席信令        
NSData *data14 = [self sendMsgWithName:@"" type:@"" targetType:@"FlyCn" msg:[NSString stringWithFormat:@"转发分配座席的信令"]; //与data10一样
        [self.clientSocket writeData:data14 withTimeout:-1 tag:14];

4. Answer

 Part of the code that implements the main function:

LinphoneCallParams *lcallParams = linphone_core_create_call_params(theLinphoneCore, call);
	if (!lcallParams) {
		LOGW(@"Could not create call parameters for %p, call has probably already ended.", call);
		return;
	}

	if ([self lpConfigBoolForKey:@"edge_opt_preference"]) {
		bool low_bandwidth = self.network == network_2g;
		if (low_bandwidth) {
			LOGI(@"Low bandwidth mode");
		}
		linphone_call_params_enable_low_bandwidth(lcallParams, low_bandwidth);
	}
	linphone_call_params_enable_video(lcallParams, video);

	linphone_call_accept_with_params(call, lcallParams);
    linphone_call_params_unref(lcallParams);

Handset/speaker switch Mute switch Hold call

There are also some relatively small functions, such as earpiece/speaker switching, mute switching

//听筒/扬声器切换
- (void)speakerToggle {
    //true:开启扬声器; false:关闭扬声器
    [LinphoneManager.instance setSpeakerEnabled:self->speaker];
    self->speaker = !self->speaker;
}

//静音切换
- (void)muteToggle {
    //true:开启静音; false:关闭静音
    linphone_core_enable_mic(LC, self->mute);
    self->mute = !self->mute;
}

Also, if you are busy with other things first, keep the current call, and the other party cannot hear the voice:

Process: The client sends a hold current call signal to the server => the client sends a keep busy state signal to the server => the voice service area notifies LinPhone to enter the hold current call state

//保持
- (void)holdCall {
    NSData *data30 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持通话信令"];
    [self.clientSocket writeData:data30 withTimeout:-1 tag:30];
    
    NSData *data31 = [self sendMsgWithName:@"" type:@"" targetType:@"" msg:[NSString stringWithFormat:@"保持繁忙状态信令"];
    [self.clientSocket writeData:data31 withTimeout:-1 tag:31];
    
    //向app端推送状态事件EventAgentState
    if (self.eventBlock) {

    }
    
    self.flag = ([self.flag isEqualToString:@"HOLDCALL"]) ? @"REHOLDCALL" : @"HOLDCALL";
}

Realization of voice call background keep alive

 The keep alive during the call has been implemented by the bottom layer of LinPhone, but there is no call and it is still in the background mode. How to keep alive to ensure that the incoming call is received and the ringing is played. The ringing here is not a short ringing of the notification in WeChat, but a long The ringing of time, my implementation here is mainly based on playing silent music

I. LinPhone calls back the status of the client to notify the incoming call, call and play the long ringtone in the callback of the incoming call, when hanging up the call, notify the hangup in the callback, and then end playing the ringtone

manager.eventBlock = ^(EVENT event, BOOL result, NSDictionary * _Nonnull resultMsg) {
        switch (event) {
            case EVENTAgentState: {
                //状态变化事件,下一行注释均为回调具体参数
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTAgentState object:nil userInfo:resultMsg];
            }
                break;
            case EVENTLogin: {
                //登录成功事件
                if (result) {
                    ViewController *vc = [[ViewController alloc] init];
                    [self.navigationController pushViewController:vc animated:YES];
                } else {
                    sender.enabled = YES;
                    [self alertWithMessage:@"登录失败,请重新登录"];
                }
            }
                break;
            case EVENTQuitLogin: {
                //退出登录或工号异处登录事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTQuitLogin object:nil userInfo:resultMsg];
            }
                break;
            case EVENTMakeCall: {
                //外呼事件
            }
                break;
            case EVENTComeCall: {
                //来电事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kComeCallUpdate object:nil userInfo:resultMsg];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置多声道播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
                [[ZSAVPlayerInstance sharedInstance] start];
            }
                break;
            case EVENTCalling: {
                //通话中事件
                [[ZSAVPlayerInstance sharedInstance] stop];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置后台播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
            }
                break;
            case EVENTHangup: {
                //挂机事件
                [[NSNotificationCenter defaultCenter] postNotificationName:kEVENTHangup object:nil userInfo:resultMsg];
                [[ZSAVPlayerInstance sharedInstance] stop];
                AVAudioSession *audioSession = [AVAudioSession sharedInstance];
                // 设置后台播放
                NSError *error = nil;
                [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
            }
                break;
            default:
                break;
        }
        [resultMsg enumerateKeysAndObjectsUsingBlock:^(id key,id obj, BOOL *stop) {
             NSLog(@"event:%d key:%@ value:%@", (int)event, key, obj);
          }];
    };

II. Questions:

In this process, the problem encountered is that there is a conflict between the background keep alive music and the ringtone playback. The background keepalive music should set AVAudioSession to AVAudioSessionCategoryPlayback background playback mode, the ringtone cannot be played in this mode, and the playback ringtone should be set to AVAudioSessionCategoryMultiRoute Channel mode, wait until the ringtone is played and then set it to AVAudioSessionCategoryPlayback background playback mode,

Player part code:

static ZSAVPlayerInstance *instance;
static dispatch_once_t onceToken;
+ (instancetype)sharedInstance {
    dispatch_once(&onceToken, ^{
        instance = [ZSAVPlayerInstance new];
    });
    return instance;
}


- (void)setup {
    [self setupAudioSession];
    [self setupAudioPlayer];
}

- (void)setupAudioSession {
    // 新建AudioSession会话
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    // 设置多声道混合播放
    NSError *error = nil;
    [audioSession setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    if (error) {
        NSLog(@"Error setCategory AVAudioSession: %@", error);
    }
    NSLog(@"%d", audioSession.isOtherAudioPlaying);
    NSError *activeSetError = nil;
    // 启动AudioSession,如果一个前台app正在播放音频则可能会启动失败
    [audioSession setActive:YES error:&activeSetError];
    if (activeSetError) {
        NSLog(@"Error activating AVAudioSession: %@", activeSetError);
    }
}

- (void)setupAudioPlayer {
    //铃声文件
    self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"notes_of_the_optimistic" withExtension:@"caf"] error:nil];
    //音量
    self.audioPlayer.volume = 1.0;
    //播放多次
    self.audioPlayer.numberOfLoops = -1;
    [self.audioPlayer prepareToPlay];
}

#pragma mark - public method

- (void)start {
    NSLog(@"--ringUrl:%@", self.ringUrl.absoluteString);
    [self setupAudioSession];
    [self setupAudioPlayer];
    if (!self.audioPlayer.isPlaying) {
        [self.audioPlayer play];
    }
}

- (void)stop {
    [self.audioPlayer stop];
}

epilogue

These are the main knowledge points of voice calls, and there are some others that need to be combined with specific business processes, which is inconvenient to say. Video calls can also use LinPhone. Write again when you have time. Give a star if you find it helpful!

Guess you like

Origin blog.csdn.net/weixin_42433480/article/details/129277064