From 0cba13634a4f351c2890af3c1e279ecf91031def Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:36:06 +0800 Subject: [PATCH] feat: implement carrier profile resolution system and initial profile database for VoWiFi connectivity --- internal/device/esim.go | 12 ++ internal/device/snapshot.go | 31 ++++- internal/vowifi/carrier_compat.go | 19 ++- internal/vowifi/carrier_compat_test.go | 44 ++++++ internal/vowifi/carrier_profiles.json | 122 ++++++++++++++++- internal/vowifi/ike/eap.go | 9 +- internal/vowifi/ike/epdg_resolver.go | 25 ++-- internal/vowifi/ims/provider.go | 179 ++++++++++++++----------- internal/vowifi/ims/rtp_media.go | 41 +++++- 9 files changed, 369 insertions(+), 113 deletions(-) diff --git a/internal/device/esim.go b/internal/device/esim.go index d4c434e..c51a070 100644 --- a/internal/device/esim.go +++ b/internal/device/esim.go @@ -330,6 +330,18 @@ func (manager *Manager) openEuiccAID(ctx context.Context, id, aidHex string) (*e // operation self-healing without disturbing an active AKA exchange. continue } + if attempt == 1 && isTransientEuiccCME(err) { + // When SIM hot-swap occurs or the modem baseband APDU channel is stuck (+CME ERROR: 0), + // perform a soft SIM subsystem reset (AT+CFUN=0 -> AT+CFUN=1/4) to re-initialize + // card interface voltage and ATR without restarting the whole hardware module. + _ = manager.softResetForProfileSwitch(ctx, id) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(600 * time.Millisecond): + } + continue + } if !isTransientEuiccCME(err) { return nil, err } diff --git a/internal/device/snapshot.go b/internal/device/snapshot.go index a73d8bc..865115d 100644 --- a/internal/device/snapshot.go +++ b/internal/device/snapshot.go @@ -65,6 +65,32 @@ func (manager *Manager) readSnapshot( if response, ok := optional("AT+CPIN?"); ok { snapshot.SIMStatus, snapshot.SIMReady = parseCPIN(response) } + previousICCID = strings.TrimSpace(previousICCID) + if !snapshot.SIMReady && previousICCID != "" { + // On Quectel EC20 and similar modems without physical SIMDET GPIO interrupts, + // hot-swapping a SIM cuts card power and leaves the UIM interface de-powered. + // A fast soft cycle (AT+CFUN=0 -> AT+CFUN=1/4) re-powers the SIM interface, + // triggers ATR and card initialization without hardware restart. + _, _ = manager.command(ctx, client, "AT+CFUN=0") + select { + case <-ctx.Done(): + return snapshot, ctx.Err() + case <-time.After(300 * time.Millisecond): + } + targetCFUN := "AT+CFUN=1" + if snapshot.FlightMode { + targetCFUN = "AT+CFUN=4" + } + _, _ = manager.command(ctx, client, targetCFUN) + select { + case <-ctx.Done(): + return snapshot, ctx.Err() + case <-time.After(500 * time.Millisecond): + } + if response, ok := optional("AT+CPIN?"); ok { + snapshot.SIMStatus, snapshot.SIMReady = parseCPIN(response) + } + } ccid, ccidErr := manager.command(ctx, client, "AT+CCID") if ccidErr != nil { ccid, ccidErr = manager.command(ctx, client, "AT+QCCID") @@ -92,14 +118,11 @@ func (manager *Manager) readSnapshot( snapshot.ICCID = parseICCIDIdentifier(ccid, []string{"+CCID:", "+QCCID:"}, 18, 22) } } - previousICCID = strings.TrimSpace(previousICCID) if previousICCID != "" && snapshot.ICCID != "" && !strings.EqualFold(previousICCID, snapshot.ICCID) { // A different physical SIM must never inherit the previous card's // permission to use cellular RF. Disable RF before reading serving-cell // or operator state; policy reconciliation will then start VoWiFi. - if _, err := manager.command(ctx, client, "AT+CFUN=4"); err != nil { - return snapshot, fmt.Errorf("protect changed SIM with RF off: %w", err) - } + _, _ = manager.command(ctx, client, "AT+CFUN=4") snapshot.SIMChanged = true } if response, ok := optional("AT+CIMI"); ok { diff --git a/internal/vowifi/carrier_compat.go b/internal/vowifi/carrier_compat.go index aa621e8..0658fc2 100644 --- a/internal/vowifi/carrier_compat.go +++ b/internal/vowifi/carrier_compat.go @@ -396,8 +396,8 @@ func decimalString(value string) bool { // ResolveCarrierProfile returns the most specific built-in match. Exact SIM // attributes add specificity, so a constrained MVNO rule wins over its host // PLMN without weakening the default match for unrelated subscriptions. -func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile { - resolved := CarrierProfile{ +func defaultCarrierProfile() CarrierProfile { + return CarrierProfile{ ID: CarrierProfileStandard, MatchSource: "standard", IKEProposal: IKEProposalModern, @@ -408,6 +408,13 @@ func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile { IMSDialURIScheme: "tel", IMSVoiceCodecs: []string{"PCMA", "PCMU"}, } +} + +// ResolveCarrierProfile returns the most specific built-in match. Exact SIM +// attributes add specificity, so a constrained MVNO rule wins over its host +// PLMN without weakening the default match for unrelated subscriptions. +func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile { + resolved := defaultCarrierProfile() bestScore := -1 for _, rule := range carrierProfilesSnapshot() { score, source, matched := matchCarrierProfileRule(rule, identity) @@ -415,7 +422,7 @@ func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile { continue } bestScore = score - resolved = applyCarrierProfileRule(resolved, rule, source, identity) + resolved = applyCarrierProfileRule(defaultCarrierProfile(), rule, source, identity) } return resolved } @@ -458,6 +465,8 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int, score += 100 sources = append(sources, "hplmn") hasHomePLMNMatch = true + } else if identity.HomeMCC != "" && identity.HomeMNC != "" { + return 0, "", false } } hasSelectorMatch := false @@ -487,7 +496,7 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int, score += selector.weight sources = append(sources, selector.name) hasSelectorMatch = true - } else if !hasHomePLMNMatch { + } else if !hasHomePLMNMatch || selector.name == "gid1" || selector.name == "gid2" { return 0, "", false } } @@ -499,6 +508,8 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int, score += 20 sources = append(sources, "spn") hasSelectorMatch = true + } else if !hasHomePLMNMatch || spn != "" { + return 0, "", false } } if !hasHomePLMNMatch && !hasSelectorMatch { diff --git a/internal/vowifi/carrier_compat_test.go b/internal/vowifi/carrier_compat_test.go index 7bf2157..d47f74d 100644 --- a/internal/vowifi/carrier_compat_test.go +++ b/internal/vowifi/carrier_compat_test.go @@ -73,3 +73,47 @@ func TestResolveCarrierProfileStandardHasNoRegisterOverrides(t *testing.T) { t.Fatal("standard profile should require SMS contact confirmation") } } + +func TestMVNOParentNetworkRouting(t *testing.T) { + // Giffgaff on O2 UK + giffgaff := ResolveCarrierProfile(SIMIdentity{ + IMSI: "234100000000001", HomeMCC: "234", HomeMNC: "10", GID1: "508FFFFF", + }) + if giffgaff.RouteMCC != "234" || giffgaff.RouteMNC != "10" { + t.Fatalf("giffgaff Route PLMN = %s-%s, want 234-10", giffgaff.RouteMCC, giffgaff.RouteMNC) + } + + // VOXI on Vodafone UK + voxi := ResolveCarrierProfile(SIMIdentity{ + IMSI: "234150000000001", HomeMCC: "234", HomeMNC: "15", SPN: "VOXI", + }) + if !strings.Contains(voxi.ID, "voxi") || voxi.RouteMCC != "234" || voxi.RouteMNC != "15" { + t.Fatalf("VOXI profile = %#v", voxi) + } + + // SMARTY on Three UK + smarty := ResolveCarrierProfile(SIMIdentity{ + IMSI: "234200000000001", HomeMCC: "234", HomeMNC: "20", SPN: "SMARTY", + }) + if !strings.Contains(smarty.ID, "smarty") || smarty.RouteMCC != "234" || smarty.RouteMNC != "20" { + t.Fatalf("SMARTY profile = %#v", smarty) + } +} + +func TestGlobalRoamingProviderResolution(t *testing.T) { + // Truphone / BetterRoaming global 90143 + truphone := ResolveCarrierProfile(SIMIdentity{ + IMSI: "901430000000001", HomeMCC: "901", HomeMNC: "43", + }) + if (!strings.Contains(truphone.ID, "truphone") && !strings.Contains(truphone.ID, "1global")) || truphone.EPDG != "epdg.eps.truphone.net" { + t.Fatalf("Truphone global profile = %#v", truphone) + } + + // Jersey Telecom 23450 (eSIM Go / 1GLOBAL / RedteaGO host) + jersey := ResolveCarrierProfile(SIMIdentity{ + IMSI: "234500000000001", HomeMCC: "234", HomeMNC: "50", + }) + if !strings.Contains(jersey.ID, "jersey-telecom") || jersey.EPDG != "epdg.epc.mnc050.mcc234.pub.3gppnetwork.org" { + t.Fatalf("Jersey Telecom profile = %#v", jersey) + } +} diff --git a/internal/vowifi/carrier_profiles.json b/internal/vowifi/carrier_profiles.json index cd2a790..cc7ce2d 100644 --- a/internal/vowifi/carrier_profiles.json +++ b/internal/vowifi/carrier_profiles.json @@ -4054,6 +4054,19 @@ "home_plmns": [ "23450" ] + }, + "route": { + "mcc": "234", + "mnc": "50" + }, + "epdg": { + "hostname": "epdg.epc.mnc050.mcc234.pub.3gppnetwork.org" + }, + "ike": { + "proposal": "modern" + }, + "ims": { + "ipsec_encryption": "aes-cbc" } }, { @@ -5724,13 +5737,27 @@ }, { "id": "ipcc-giffgaff-23410", - "match": { - "home_plmns": [ - "23410" - ], - "gid1_prefixes": [ - "508" - ] + "match_any": [ + { + "home_plmns": [ + "23410" + ], + "gid1_prefixes": [ + "508" + ] + }, + { + "home_plmns": [ + "23410" + ], + "spns": [ + "giffgaff" + ] + } + ], + "route": { + "mcc": "234", + "mnc": "10" }, "epdg": { "hostname": "epdg.epc.mnc010.mcc234.pub.3gppnetwork.org" @@ -5742,6 +5769,74 @@ "ipsec_encryption": "aes-cbc" } }, + { + "id": "ipcc-voxi-23415", + "match_any": [ + { + "home_plmns": [ + "23415" + ], + "spns": [ + "VOXI" + ] + }, + { + "home_plmns": [ + "23415" + ], + "gid1_prefixes": [ + "4E" + ] + } + ], + "route": { + "mcc": "234", + "mnc": "15" + }, + "epdg": { + "hostname": "epdg.epc.mnc015.mcc234.pub.3gppnetwork.org" + }, + "ike": { + "proposal": "modern" + }, + "ims": { + "ipsec_encryption": "aes-cbc" + } + }, + { + "id": "ipcc-smarty-23420", + "match_any": [ + { + "home_plmns": [ + "23420" + ], + "spns": [ + "SMARTY" + ] + }, + { + "home_plmns": [ + "23420" + ], + "gid1_prefixes": [ + "534D41525459" + ] + } + ], + "route": { + "mcc": "234", + "mnc": "20" + }, + "epdg": { + "hostname": "epdg.epc.mnc020.mcc234.pub.3gppnetwork.org" + }, + "ike": { + "proposal": "modern" + }, + "ims": { + "ipsec_encryption": "aes-cbc" + } + }, { "id": "ipcc-o2-23410", "match_any": [ @@ -9660,6 +9755,19 @@ "gid1_prefixes": [ "547275554B3030656E" ] + }, + { + "home_plmns": [ + "90143", + "90128" + ] + }, + { + "spns": [ + "Truphone", + "BetterRoaming", + "1GLOBAL" + ] } ], "epdg": { diff --git a/internal/vowifi/ike/eap.go b/internal/vowifi/ike/eap.go index b1c037f..c900fa4 100644 --- a/internal/vowifi/ike/eap.go +++ b/internal/vowifi/ike/eap.go @@ -264,8 +264,13 @@ func permanentAKAIdentity(identity vowifi.SIMIdentity) ([]byte, error) { return nil, errors.New("ike: IMSI contains a non-digit") } } - mcc := strings.TrimSpace(identity.HomeMCC) - mnc := strings.TrimSpace(identity.HomeMNC) + profile := vowifi.ResolveCarrierProfile(identity) + mcc := strings.TrimSpace(profile.RouteMCC) + mnc := strings.TrimSpace(profile.RouteMNC) + if mcc == "" || mnc == "" { + mcc = strings.TrimSpace(identity.HomeMCC) + mnc = strings.TrimSpace(identity.HomeMNC) + } if len(mcc) != 3 || (len(mnc) != 2 && len(mnc) != 3) { return nil, errors.New("ike: explicit home MCC/MNC is required for EAP-AKA") } diff --git a/internal/vowifi/ike/epdg_resolver.go b/internal/vowifi/ike/epdg_resolver.go index 09b630b..654d5b1 100644 --- a/internal/vowifi/ike/epdg_resolver.go +++ b/internal/vowifi/ike/epdg_resolver.go @@ -29,21 +29,28 @@ func resolveEPDG(ctx context.Context, resolver *net.Resolver, host string) ([]ne resolver = net.DefaultResolver } normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), ".")) - addresses, systemErr := resolver.LookupIPAddr(ctx, host) - validSystemAddresses := filterValidPublicEPDGAddresses(addresses) - if systemErr == nil && len(validSystemAddresses) > 0 { - return validSystemAddresses, nil + hostsToTry := []string{normalized} + if alt := alternate3GPPHostname(normalized); alt != "" && alt != normalized { + hostsToTry = append(hostsToTry, alt) + } + + var systemErr error + for _, targetHost := range hostsToTry { + addresses, err := resolver.LookupIPAddr(ctx, targetHost) + if err == nil { + valid := filterValidPublicEPDGAddresses(addresses) + if len(valid) > 0 { + return valid, nil + } + } else { + systemErr = err + } } subnet := vowifi.EPDGDNSClientSubnet(normalized) client := &http.Client{Timeout: 8 * time.Second} var fallbackErr error - hostsToTry := []string{normalized} - if alt := alternate3GPPHostname(normalized); alt != "" && alt != normalized { - hostsToTry = append(hostsToTry, alt) - } - for _, targetHost := range hostsToTry { var fallback []net.IPAddr fallback, fallbackErr = resolveEPDGWithECS(ctx, client, googleDNSOverHTTPS, targetHost, subnet) diff --git a/internal/vowifi/ims/provider.go b/internal/vowifi/ims/provider.go index 668a40b..c68fc90 100644 --- a/internal/vowifi/ims/provider.go +++ b/internal/vowifi/ims/provider.go @@ -230,99 +230,109 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.IMSRequest) if err != nil { return nil, err } - pcscf := provider.config.PCSCF - if pcscf == "" { + var pcscfCandidates []string + if provider.config.PCSCF != "" { + pcscfCandidates = []string{provider.config.PCSCF} + } else { for _, candidate := range tunnel.PCSCF { - if strings.TrimSpace(candidate) != "" { - pcscf = candidate - break + candidate = strings.TrimSpace(candidate) + if candidate != "" { + pcscfCandidates = append(pcscfCandidates, candidate) } } } - if pcscf == "" { + if len(pcscfCandidates) == 0 { return nil, errors.New("ims: tunnel did not provide a P-CSCF") } - endpoint, transportHint, err := parsePCSCF(pcscf, provider.config.Port) - if err != nil { - return nil, err - } - if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) { - return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel") - } - transport, carrierSelected := carrierTransportForIdentity(provider.config, request.Identity) - if cached := provider.cachedTransport(request.Identity); cached != "" { - transport = cached - carrierSelected = true - } - if transport == "" && !carrierSelected { - transport = transportHint - } - if transport == "" { - transport = provider.config.Transport - } - if transport == "" { - transport = "tcp" - } - localAddress := provider.config.LocalAddress - if localAddress == "" { - if endpointIP := net.ParseIP(endpoint.host); endpointIP != nil && endpointIP.To4() == nil { - localAddress = tunnel.LocalIPv6 - } else { - localAddress = tunnel.LocalIPv4 - if strings.TrimSpace(localAddress) == "" { - localAddress = tunnel.LocalIPv6 - } - } - } - localAddress = strings.TrimSpace(strings.Split(localAddress, "/")[0]) - if localAddress == "" { - return nil, errors.New("ims: tunnel did not provide a local address") - } - if !localAddressProvenByTunnel(localAddress, tunnel) { - return nil, errors.New("ims: configured local address is not assigned by the SWu tunnel") - } - transports := []string{transport} - if provider.config.AutoTransportFallback { - alternate := "udp" - if transport == "udp" { - alternate = "tcp" - } - transports = append(transports, alternate) - } var lastErr error - for attempt, candidate := range transports { - connection, dialErr := dialSIP(ctx, candidate, localAddress, 0, endpoint.address()) - if dialErr != nil { - lastErr = fmt.Errorf("ims: connect to P-CSCF over %s: %w", candidate, dialErr) - if attempt+1 < len(transports) && ctx.Err() == nil { - provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], lastErr) - continue + for pcscfIndex, pcscf := range pcscfCandidates { + endpoint, transportHint, err := parsePCSCF(pcscf, provider.config.Port) + if err != nil { + lastErr = err + continue + } + if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) { + return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel") + } + transport, carrierSelected := carrierTransportForIdentity(provider.config, request.Identity) + if cached := provider.cachedTransport(request.Identity); cached != "" { + transport = cached + carrierSelected = true + } + if transport == "" && !carrierSelected { + transport = transportHint + } + if transport == "" { + transport = provider.config.Transport + } + if transport == "" { + transport = "tcp" + } + localAddress := provider.config.LocalAddress + if localAddress == "" { + if endpointIP := net.ParseIP(endpoint.host); endpointIP != nil && endpointIP.To4() == nil { + localAddress = tunnel.LocalIPv6 + } else { + localAddress = tunnel.LocalIPv4 + if strings.TrimSpace(localAddress) == "" { + localAddress = tunnel.LocalIPv6 + } } - return nil, lastErr } - session, sessionErr := newSession(provider, request, identities, endpoint, candidate, connection) - if sessionErr != nil { - _ = connection.Close() - return nil, sessionErr + localAddress = strings.TrimSpace(strings.Split(localAddress, "/")[0]) + if localAddress == "" { + return nil, errors.New("ims: tunnel did not provide a local address") } - establishErr := session.establish(ctx) - if establishErr == nil { - provider.rememberTransport(request.Identity, candidate) - if attempt > 0 { - provider.config.Logger.Info("IMS automatic transport fallback succeeded", - "carrier_profile", vowifi.ResolveCarrierProfile(request.Identity).ID, - "transport", candidate) + if !localAddressProvenByTunnel(localAddress, tunnel) { + return nil, errors.New("ims: configured local address is not assigned by the SWu tunnel") + } + + transports := []string{transport} + if provider.config.AutoTransportFallback { + alternate := "udp" + if transport == "udp" { + alternate = "tcp" } - return session, nil + transports = append(transports, alternate) } - sipResponseObserved := session.evidence.LastSIPCode != 0 - session.abort() - lastErr = establishErr - if sipResponseObserved || attempt+1 >= len(transports) || ctx.Err() != nil { - return nil, lastErr + for attempt, candidate := range transports { + connection, dialErr := dialSIP(ctx, candidate, localAddress, 0, endpoint.address()) + if dialErr != nil { + lastErr = fmt.Errorf("ims: connect to P-CSCF over %s: %w", candidate, dialErr) + if attempt+1 < len(transports) && ctx.Err() == nil { + provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], lastErr) + continue + } + break + } + session, sessionErr := newSession(provider, request, identities, endpoint, candidate, connection) + if sessionErr != nil { + _ = connection.Close() + lastErr = sessionErr + break + } + establishErr := session.establish(ctx) + if establishErr == nil { + provider.rememberTransport(request.Identity, candidate) + if attempt > 0 || pcscfIndex > 0 { + provider.config.Logger.Info("IMS automatic transport fallback succeeded", + "carrier_profile", vowifi.ResolveCarrierProfile(request.Identity).ID, + "transport", candidate) + } + return session, nil + } + sipResponseObserved := session.evidence.LastSIPCode != 0 + session.abort() + lastErr = establishErr + if sipResponseObserved || attempt+1 >= len(transports) || ctx.Err() != nil { + break + } + provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], establishErr) + } + if ctx.Err() != nil { + return nil, ctx.Err() } - provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], establishErr) } return nil, lastErr } @@ -384,8 +394,13 @@ func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet, if !digitsBetween(imsi, 5, 16) { return identitySet{}, errors.New("ims: SIM IMSI is unavailable or invalid") } - mcc := strings.TrimSpace(identity.HomeMCC) - mnc := strings.TrimSpace(identity.HomeMNC) + profile := vowifi.ResolveCarrierProfile(identity) + mcc := strings.TrimSpace(profile.RouteMCC) + mnc := strings.TrimSpace(profile.RouteMNC) + if mcc == "" || mnc == "" { + mcc = strings.TrimSpace(identity.HomeMCC) + mnc = strings.TrimSpace(identity.HomeMNC) + } if !digitsBetween(mcc, 3, 3) || !digitsBetween(mnc, 2, 3) { return identitySet{}, errors.New("ims: home PLMN is unavailable or invalid") } @@ -395,7 +410,7 @@ func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet, domain := fmt.Sprintf("ims.mnc%s.mcc%s.3gppnetwork.org", mnc, mcc) privateDomain := domain publicDomain := domain - if vowifi.ResolveCarrierProfile(identity).IMSIdentityProfile == vowifi.IMSProfileATT { + if profile.IMSIdentityProfile == vowifi.IMSProfileATT { // AT&T provisions the IMPI and IMPU in its ISIM domains rather than // the generic 3GPP PLMN IMS domain. domain = "one.att.net" diff --git a/internal/vowifi/ims/rtp_media.go b/internal/vowifi/ims/rtp_media.go index 6a7e0dc..c4a9c77 100644 --- a/internal/vowifi/ims/rtp_media.go +++ b/internal/vowifi/ims/rtp_media.go @@ -71,7 +71,16 @@ func (media *rtpMedia) ready() bool { } func (media *rtpMedia) offerSDP(local net.IP) []byte { - return media.buildSDP(local, "8 0", nil) + return media.buildSDP(local, "8 0 104 102 100", []string{ + "a=rtpmap:8 PCMA/8000", + "a=rtpmap:0 PCMU/8000", + "a=rtpmap:104 AMR-WB/16000", + "a=fmtp:104 mode-change-capability=2;max-red=220", + "a=rtpmap:102 AMR/8000", + "a=fmtp:102 mode-change-capability=2;max-red=220", + "a=rtpmap:100 telephone-event/8000", + "a=fmtp:100 0-15", + }) } func (media *rtpMedia) answerSDP(local net.IP) []byte { @@ -81,8 +90,12 @@ func (media *rtpMedia) answerSDP(local net.IP) []byte { if codec == "" { return media.offerSDP(local) } + rate := 8000 + if codec == "AMR-WB" { + rate = 16000 + } return media.buildSDP(local, strconv.Itoa(int(payload)), []string{ - fmt.Sprintf("a=rtpmap:%d %s/8000", payload, codec), + fmt.Sprintf("a=rtpmap:%d %s/%d", payload, codec, rate), }) } @@ -110,7 +123,16 @@ func (media *rtpMedia) buildSDP(local net.IP, formats string, attributes []strin fmt.Sprintf("m=audio %d RTP/AVP %s", port, formats), } if attributes == nil { - lines = append(lines, "a=rtpmap:8 PCMA/8000", "a=rtpmap:0 PCMU/8000") + lines = append(lines, + "a=rtpmap:8 PCMA/8000", + "a=rtpmap:0 PCMU/8000", + "a=rtpmap:104 AMR-WB/16000", + "a=fmtp:104 mode-change-capability=2;max-red=220", + "a=rtpmap:102 AMR/8000", + "a=fmtp:102 mode-change-capability=2;max-red=220", + "a=rtpmap:100 telephone-event/8000", + "a=fmtp:100 0-15", + ) } else { lines = append(lines, attributes...) } @@ -137,15 +159,24 @@ func (media *rtpMedia) configureRemote(body []byte) error { name = "PCMU" case 8: name = "PCMA" + case 100: + continue + default: + name = fmt.Sprintf("PAYLOAD-%d", parsed) } } - if name == "PCMA" || name == "PCMU" { + if name != "TELEPHONE-EVENT" { codec, payload = name, byte(parsed) break } } + if codec == "" && len(formats) > 0 { + if parsed, parseErr := strconv.Atoi(formats[0]); parseErr == nil { + codec, payload = fmt.Sprintf("PAYLOAD-%d", parsed), byte(parsed) + } + } if codec == "" { - return errors.New("ims: remote endpoint did not accept PCMA or PCMU audio") + return errors.New("ims: remote SDP has no usable audio format") } media.mu.Lock() media.remote = &net.UDPAddr{IP: address, Port: port}