App crashes on every 4th incoming call when using CallKit in VoIP app

I’m developing a VoIP app and using CallKit to handle incoming calls. However, I’ve encountered an issue: every time I answer the fourth incoming call, my app crashes. This happens consistently with the fourth incoming call, regardless of the previous ones. The crash doesn’t seem to be tied to a specific call but rather occurs at the point of answering the fourth call.

I’ve reviewed my CallKit implementation, but I’m unable to pinpoint the exact cause of the crash. I suspect the issue may lie in how I’m handling or passing call information to CallKit, but I’m not sure exactly what’s going wrong.

Can anyone assist me in identifying the source of this problem? Is there a known issue with CallKit or a specific area in my code that I should investigate? Any insights or guidance would be greatly appreciated.

Here’s the code I’m using to answer incoming calls:

#CallManager.swift:

import UIKit
import CallKit
import AVFoundation


private let sharedManager = CallManager.init()

protocol CallManagerDelegate : class {
    
    func callDidAnswer()
    func callDidEnd()
    func callDidHold(isOnHold : Bool)
    func callDidFail()
    func didUpdateActiveCallId(_ callId: String)
    
    // Neue Methode für Stummschaltung
    func callDidMute(isMuted: Bool)
}





class CallManager: NSObject, CXProviderDelegate, AVAudioPlayerDelegate {

    static let shared = CallManager()
    var activeCallId: String? 
    
    var provider: CXProvider?
    var callController: CXCallController?
    var activeCalls: [UUID: UUID] = [:] // Map zur Verwaltung von Anrufen

    // Closure für eingehende Anrufe
    var onIncomingCall: ((UUID) -> Void)?
    var onOutgoingCall: ((UUID) -> Void)?
    // Aktuelle UUID des aktiven Anrufs (optional)
    var currentCallID: UUID?
    
    var isSIPRegistered: Bool = false // Füge eine Eigenschaft hinzu, um den SIP-Registrierungsstatus zu speichern.
    var muteStatus: [Int32: Bool] = [:]
    
    weak var delegate : CallManagerDelegate?
    
    override init() {
       
        let providerConfiguration = CXProviderConfiguration(localizedName: "MyApp")
        providerConfiguration.supportsVideo = true
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportedHandleTypes = [.generic]

        provider = CXProvider(configuration: providerConfiguration)
        callController = CXCallController()
        super.init()
        provider?.setDelegate(self, queue: nil)
    }
    
    // Methode, um zu überprüfen, ob SIP registriert ist und registrieren, wenn nötig
    func checkAndRegisterSIP() {
        // Stelle sicher, dass die SIP-Daten im SipManager konfiguriert sind
        if SipManager.shared.isConfigured {
            if !isSIPRegistered {
                // SIP registrieren
                print("SIP ist nicht registriert. Versuche Registrierung...")

                // Verwende die SIP-Daten aus SipManager
                let username = SipManager.shared.sipUsername
                let password = SipManager.shared.sipPassword
                let registrar = SipManager.shared.sipIp
                let port = SipManager.shared.sipPort

                // Jetzt können wir die Registrierung des SIP-Accounts selbst durchführen
                registerSIPAccount(username: username, password: password, registrar: registrar, port: port)
            }
        } else {
            print("SIP-Konfiguration ist nicht vollständig!")
        }
    }
    
    // SIP-Account manuell registrieren
    func registerSIPAccount(username: String, password: String, registrar: String, port: String) {
        // Hier musst du den Code für die SIP-Registrierung mit deinen Daten implementieren.
        // Beispiel (mit einer hypothetischen SIP-Bibliothek):
        
        print("Registrierung des SIP-Accounts mit den folgenden Daten:")
        print("Username: (username), Registrar: (registrar), Port: (port)")
        
        // Registrierung durchführen – Hier hängt es davon ab, wie du PJSIP oder eine andere SIP-Bibliothek verwendest.
        // Zum Beispiel:
        
        // Beispiel mit PJSIP:
        // let status = PJSIP.registerAccount(username: username, password: password, registrar: registrar, port: port)
        // if status == .success {
        //    isSIPRegistered = true
        //    print("SIP-Account erfolgreich registriert")
        // } else {
        //    print("Fehler bei der SIP-Registrierung")
        // }
        
        // Hier nehmen wir an, die Registrierung war erfolgreich:
        isSIPRegistered = true
        print("SIP-Account erfolgreich registriert")
        
        // Wenn der SIP-Account registriert ist, kannst du den CallManager informieren:
        accStatusListener(registered: true)
    }
    
    // Listener für den Registrierungsstatus (SIP)
    func accStatusListener(registered: Bool) {
        if registered {
            isSIPRegistered = true
            print("SIP ist nun registriert.")
        } else {
            isSIPRegistered = false
            print("SIP ist nicht registriert.")
            checkAndRegisterSIP() // Versuche, SIP zu registrieren, falls es nicht registriert ist.
        }
    }
    // Halten eines Anrufs
    public func setCallOnHold(id: UUID, onHold: Bool) {
        print("Set call (onHold ? "on" : "off") hold with ID: (id)")

        let holdAction = CXSetHeldCallAction(call: id, onHold: onHold)
        let transaction = CXTransaction(action: holdAction)

        // Führe die Transaktion zur CallKit-Anfrage aus
        callController?.request(transaction) { error in
            if let error = error {
                print("Error setting call on hold: (error)")
            } else {
                print("Call (onHold ? "held" : "unheld") successfully with ID: (id)")
                self.delegate?.callDidHold(isOnHold: onHold)
            }
        }
    }
    


    public func reportOutgoingCall(id: UUID, handle: String) {
        print("Reporting outgoing call with ID: (id) and handle: (handle)")

        // Setze die Call-ID und benachrichtige den Delegate
        self.activeCallId = id.uuidString
        print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "Kein Wert")")

        // Benachrichtige den Delegate, dass die Call-ID gesetzt wurde
        if let activeCallId = self.activeCallId {
            self.delegate?.didUpdateActiveCallId(activeCallId)
        }

        // CallKit verwenden, um den Anruf zu melden
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)  // Setze den remoteHandle auf die Telefonnummer
        update.hasVideo = false

        // CallKit Provider melden den ausgehenden Anruf
        provider?.reportOutgoingCall(with: id, startedConnectingAt: nil)

        // Call-ID speichern oder andere Logik
        self.activeCalls[id] = id
        print("Outgoing call reported with ID: (id)")

        // Jetzt den CallKit-Startaufruf durchführen
        if let callController = self.callController {
            // Erstelle den CXHandle mit der Telefonnummer
            let remoteHandle = CXHandle(type: .phoneNumber, value: handle)

            // Erstelle die StartCallAction mit der Call-ID und dem Remote-Handle
            let startCallAction = CXStartCallAction(call: id, handle: remoteHandle)

            // Wenn du Video unterstützen möchtest, kannst du hier `isVideo` auf true setzen
            startCallAction.isVideo = false

            // Erstelle eine Transaktion mit der StartCallAction
            let transaction = CXTransaction(action: startCallAction)

            // Führe die Anfrage zur CallKit-Transaktion durch
            callController.request(transaction) { error in
                if let error = error {
                    print("Fehler beim Starten des Anrufs: (error)")
                } else {
                    print("Anruf erfolgreich gestartet: (id)")
                    
                }
            }
        } else {
            print("CallController ist nicht verfügbar")
        }

        // Callback nach erfolgreichem Report
        self.onOutgoingCall?(id)
    }


    
    

    // Methode zum Melden eines eingehenden Anrufs
    public func reportIncomingCall(id: UUID, handle: String) {
        print("Reporting call with ID: (id) and handle: (handle)")
        
        // Bevor wir den Anruf melden, prüfen wir, ob SIP registriert ist.
        checkAndRegisterSIP() // Überprüfe und registriere SIP, wenn nötig.
        
        currentCallID = id  // Speichere die aktuelle UUID
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: handle)

        provider?.reportNewIncomingCall(with: id, update: update) { error in
            if let error = error {
                print("Error reporting incoming call: (error)")
            } else {
                self.activeCalls[id] = id // Speichere die Call-ID in activeCalls
                print("Call reported with ID: (id)")
                print("Active calls after reporting: (self.activeCalls)")

                // Callback aufrufen
                self.onIncomingCall?(id)
                print("onIncomingCall callback triggered with ID: (id)")

                // Setze die aktive Call-ID hier
                DispatchQueue.main.async {
                    self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
                }
            }
        }
    }
    
    

    // Antwort auf einen Anruf
    public func answerCall(id: UUID, completion: @escaping (Bool) -> Void) {
        print("Active calls before answering: (self.activeCalls)")
        guard let _ = activeCalls[id] else {
            print("Call with ID (id) not found!")
            completion(false)
            return
        }

        let answerCallAction = CXAnswerCallAction(call: id)
        let transaction = CXTransaction(action: answerCallAction)

        callController?.request(transaction) { error in
            if let error = error {
                print("Error answering call: (error)")
                completion(false)
            } else {
                print("Call answered with ID: (id)")
                completion(true)
            }
        }
    }

    // Beenden eines Anrufs
    public func endCall(id: UUID) {
        print("Active calls before ending: (self.activeCalls)") // Debugging-Ausgabe

        // Überprüfe, ob der Anruf in activeCalls vorhanden ist
        guard let uuid = activeCalls[id] else {
            print("Call with ID (id) not found!")  // Wenn der Anruf nicht gefunden wird
            return
        }

        // Der Anruf ist gefunden, er wird nun beendet
        let endCallAction = CXEndCallAction(call: uuid)
        let transaction = CXTransaction(action: endCallAction)

        // Führe die EndCall-Aktion aus
        callController?.request(transaction) { error in
            if let error = error {
                print("Error ending call: (error)")
            } else {
                print("Call ended with ID: (id)")
                // Entferne den Anruf erst nach Abschluss der EndCall-Aktion
                // Entferne nicht sofort, sondern vertraue auf den CXProvider Delegate
            }
        }
    }

    // Callback zum Setzen der aktiven Call-ID
    func onIncomingCall(id: UUID) {
        DispatchQueue.main.async {
            self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
            print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "keine ID")")
        }
    }
    
    // Callback zum Setzen der aktiven Call-ID
    func onOutgoingCall(id: UUID) {
        DispatchQueue.main.async {
            self.activeCallId = id.uuidString // Setze die aktive Call-ID im CallManager
            print("Aktuelle Call-ID im CallManager: (self.activeCallId ?? "keine ID")")
        }
    }





    func providerDidReset(_ provider: CXProvider) {
        print("Provider did reset. Clearing all active calls.")
        activeCalls.removeAll()
    }
    
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        
        print("Provider is handling call answer for ID: (action.callUUID)")
        // Überprüfe, ob der Anruf in activeCalls vorhanden ist
        if let _ = activeCalls[action.callUUID] {
            print("Anruf mit ID (action.callUUID) gefunden, Anruf wird beantwortet.")
            
            action.fulfill()  // Markiere die Aktion als erledigt
            CPPWrapper().answerCall() // Hier führst du die Logik zum Beantworten des Anrufs aus
        } else {
            print("Kein aktiver Anruf gefunden mit ID: (action.callUUID)")
            action.fail()  // Falls der Anruf nicht gefunden wird, fehlschlagen
        }
    }
    
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        print("Provider is handling call end for ID: (action.callUUID)")
        
        // Überprüfe, ob der Anruf in activeCalls vorhanden ist, bevor er beendet wird
        if let _ = activeCalls[action.callUUID] {
            playEndCallSound()
            print("Anruf mit ID (action.callUUID) wird beendet.")
            CPPWrapper().hangupCall() // Hier führst du die Logik zum Beenden des Anrufs aus
            
            activeCalls.removeValue(forKey: action.callUUID)  // Entferne den Anruf aus der aktiven Liste
            action.fulfill()  // Markiere die Aktion als erledigt
        } else {
            print("Kein aktiver Anruf gefunden mit ID: (action.callUUID)")
            action.fail()  // Falls der Anruf nicht gefunden wird, fehlschlagen
        }
    }
    
    var audioPlayer: AVAudioPlayer?
    

    func playEndCallSound() {
        // Versuche, die MP3-Datei aus den Ressourcen zu laden
        guard let soundURL = Bundle.main.url(forResource: "endCall", withExtension: "mp3") else {
            print("Fehler: Ton-Datei 'endCall.mp3' nicht gefunden.")
            return
        }
        
        // Versuche, den AudioPlayer zu erstellen und die Audiodatei abzuspielen
        do {
            audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
            
            // Setze die Audio-Player-Optionen (z.B. Lautstärke oder Wiederholung, falls gewünscht)
            audioPlayer?.volume = 1.0 // Maximale Lautstärke
            audioPlayer?.numberOfLoops = 0 // Die Datei soll nur einmal abgespielt werden
            
            // Starte das Abspielen des Sounds
            audioPlayer?.play()
            
            print("End-Call Ton wird abgespielt.")
        } catch let error as NSError {
            // Fehlerbehandlung falls das Abspielen der Datei fehlschlägt
            print("Fehler beim Erstellen des AudioPlayers: (error.localizedDescription)")
        }
    }
    
    func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
        guard let confSlotUUID = activeCalls[action.callUUID] else {
            print("No active call found for UUID (action.callUUID)")
            action.fail()
            return
        }

        let confSlotId = abs(confSlotUUID.hashValue % Int(PJSUA_MAX_CONF_PORTS))
        guard confSlotId >= 0 && confSlotId < Int(PJSUA_MAX_CONF_PORTS) else {
            print("Calculated confSlotId (confSlotId) is out of valid range.")
            action.fail()
            return
        }

        let slotId = Int32(confSlotId)

        // Überprüfung des gewünschten Status
        if action.isMuted {
            // Nur stummschalten, wenn es aktuell nicht stumm ist
            if muteStatus[slotId] != true {
                CPPWrapper().muteMicrophoneWrapper(slotId)
                muteStatus[slotId] = true
                print("Muted microphone for confSlotId: (slotId)")
            } else {
                print("Microphone is already muted for confSlotId: (slotId)")
            }
        } else {
            // Überprüfung, ob der Anruf aktiv ist und ob das Entstummen erlaubt ist
            if let activeCall = activeCalls[action.callUUID], activeCall == confSlotUUID {
                // Nur entstummen, wenn es aktuell stumm ist
                if muteStatus[slotId] != false {
                    CPPWrapper().unmuteMicrophoneWrapper(slotId)
                    muteStatus[slotId] = false
                    print("Unmuted microphone for confSlotId: (slotId)")
                } else {
                    print("Microphone is already unmuted for confSlotId: (slotId)")
                }
            } else {
                print("Skipping unmute action as the call is no longer active.")
            }
        }

        // Informiere den Delegate über die Aktion
        delegate?.callDidMute(isMuted: action.isMuted)
        action.fulfill()
    }

}

#IncomingViewController.swift:

import CallKit

class IncomingViewController: UIViewController {
    @IBOutlet weak var holdButton: UIButton!
    var holdFlag: Bool = false
    var incomingCallId: String = ""
    @IBOutlet weak var callTitle: UILabel!
    var activeCallId: String?  // Diese Variable wird jetzt von callManager verwaltet
    var callManager = CallManager.shared
    var id = UUID()
    var isMuted = false
    @IBOutlet weak var activeCallTitle: UILabel!
    @IBOutlet weak var muteButton: UIButton!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        activeCallTitle.text = callManager.activeCallId  // Zugriff auf activeCallId von callManager
        callTitle.text = incomingCallId
        
        // Beobachter für den Proximity-Sensor
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(proximityChanged),
                                               name: UIDevice.proximityStateDidChangeNotification,
                                               object: nil)
        
        // CPPWrapper zum Handling des Anrufstatus
        CPPWrapper().call_listener_wrapper(call_status_listener_swift)
        
    }

    // Beobachter-Methode für den Proximity-Sensor
    @objc func proximityChanged(notification: Notification) {
        if UIDevice.current.proximityState {
            // Der Proximity Sensor ist aktiv (z.B. das Handy wird ans Ohr gehalten)
            print("Proximity sensor activated - Display ausschalten")
            // Hier kannst du den Bildschirm deaktivieren, z.B. durch dimmen oder Ausschalten
        } else {
            // Der Proximity Sensor ist nicht aktiv (z.B. das Handy wird entfernt)
            print("Proximity sensor deactivated - Display wieder einschalten")
            // Hier kannst du das Display wieder aktivieren
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Proximity-Sensor deaktivieren
        UIDevice.current.isProximityMonitoringEnabled = false
        
        // Stummschaltung bleibt erhalten
        if isMuted {
            // Wenn der Anruf stummgeschaltet ist, Mute-Status nicht ändern
            print("Anruf war stummgeschaltet. Beibehalten.")
        } else {
            print("Anruf war nicht stummgeschaltet.")
        }
        
        // Sofortige Ausführung der Aktionen
        guard let callId = self.callManager.activeCallId, let uuid = UUID(uuidString: callId) else {
            print("Keine gültige Call-ID zum Auflegen gefunden")
            return
        }
        
        // Den CallManager benachrichtigen, dass der Anruf beendet wird
        self.callManager.endCall(id: uuid)

        // Beobachter entfernen
        NotificationCenter.default.removeObserver(self,
                                                  name: UIDevice.proximityStateDidChangeNotification,
                                                  object: nil)

        
        
    }

    @IBAction func hangupClick(_ sender: UIButton) {
        // Überprüfe, ob eine gültige Call-ID vorhanden ist
        guard let callId = callManager.activeCallId, let uuid = UUID(uuidString: callId) else {
            print("Keine gültige Call-ID zum Auflegen gefunden")
            return
        }
        
        // Den CallManager benachrichtigen, dass der Anruf beendet wird
        callManager.endCall(id: uuid)  // Call beenden mit der UUID des Anrufs

        // Dismiss den ViewController, um den Bildschirm zu schließen
        self.dismiss(animated: false, completion: nil)
    }

    @IBAction func answerClick(_ sender: UIButton) {
        // Den CallKit Anruf akzeptieren
        guard let callId = UUID(uuidString: incomingCallId) else {
            print("Ungültige Call ID")
            return
        }

        // CallManager verwenden, um den Anruf zu starten
        callManager.answerCall(id: callId, completion: { success in
            if success {
                // Nur den Proximity-Sensor aktivieren, wenn der Anruf erfolgreich angenommen wurde
                UIDevice.current.isProximityMonitoringEnabled = true
                print("Anruf erfolgreich angenommen: (callId)")
            } else {
                print("Fehler beim Annehmen des Anrufs mit ID: (callId)")
            }
        })
    }

    
    @IBAction func holdClick(_ sender: Any) {
        // Überprüfe, ob holdButton korrekt verbunden ist
        guard let button = holdButton else {
            print("Error: holdButton is not connected.")
            return
        }
        
        // Das Systembild "pause" verwenden (System-Image)
        guard let pauseImage = UIImage(systemName: "pause") else {
            print("Error: pause system image is missing.")
            return
        }
        
        // Überprüfe, ob der CallManager eine aktive Call-ID hat
        guard let callUUIDString = CallManager.shared.activeCallId,
              let callUUID = UUID(uuidString: callUUIDString) else {
            print("Error: Keine gültige aktive Call-ID gefunden.")
            return
        }
        
        // Anruf halten oder wieder aufnehmen
        if holdFlag == false {
            // Anruf auf "Hold" setzen
            CallManager.shared.setCallOnHold(id: callUUID, onHold: true)
            CPPWrapper().holdCall()

            // Button-Titel ändern
            button.setTitle("Fortsetzen", for: .normal)

            // Bildfarbe auf blau ändern (Tint Color anpassen)
            button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .normal)
            button.tintColor = UIColor.systemBlue
        } else {
            // Anruf wieder aufnehmen
            CallManager.shared.setCallOnHold(id: callUUID, onHold: false)
            CPPWrapper().unholdCall()
            // Button-Titel zurück zu "Halten"
            button.setTitle("Halten", for: .normal)

            // Bildfarbe zurück auf schwarz ändern (Tint Color auf schwarz setzen)
            button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .normal)
            button.tintColor = UIColor.label // Setze die Farbe auf Schwarz
        }
        holdFlag.toggle()
    }
    
    @IBAction func muteButton(_ sender: Any) {
        let callManager = CallManager.shared

        // Überprüfen, ob der Sender ein UIButton ist
        guard let button = sender as? UIButton else {
            print("Error: Sender is not a UIButton.")
            return
        }

        // Holen Sie die aktive Call UUID
        guard let activeCallUUID = callManager.activeCalls.keys.first else {
            print("No active call found to toggle mute!")
            return
        }

        // Toggle den Zustand
        isMuted.toggle()

        // CallKit-Aktion erstellen
        let muteAction = CXSetMutedCallAction(call: activeCallUUID, muted: isMuted)
        let transaction = CXTransaction(action: muteAction)

        // CallKit-Anfrage senden
        callManager.callController?.request(transaction) { error in
            if let error = error {
                print("Failed to toggle mute: (error)")
                self.isMuted.toggle() // Rückgängig machen, falls ein Fehler auftritt
            } else {
                print(self.isMuted ? "Call muted." : "Call unmuted.")
                self.updateMuteButtonUI() // UI aktualisieren
            }
        }
    }

    func updateMuteButtonUI() {
        DispatchQueue.main.async {
            if self.isMuted {
                self.muteButton.setTitle("Stumm ein", for: .normal)
                self.muteButton.setImage(UIImage(systemName: "mic.slash.fill"), for: .normal)
                self.muteButton.tintColor = UIColor.systemBlue // Optional: Blau für stummgeschaltet
            } else {
                self.muteButton.setTitle("Stumm ein", for: .normal)
                self.muteButton.setImage(UIImage(systemName: "mic.fill"), for: .normal)
                self.muteButton.tintColor = UIColor.label // Optional: Schwarz für entstummt
            }
        }
    }


}

When the app crashes, I see the following highlighted line of code:

#media.cpp

{
    PJSUA2_CHECK_EXPR( pjsua_conf_connect(id, sink.id) );
}

Error:

libc++abi: terminating due to uncaught exception of type pj::Error

How can I solve this? Thank you.

New contributor

BouncedBy is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật