Skip to content

Commit 8c5ab03

Browse files
committed
Add initial implementation of Hold/Resume call
1 parent 75ff0fc commit 8c5ab03

File tree

2 files changed

+431
-0
lines changed

2 files changed

+431
-0
lines changed

pkg/sip/inbound.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
"github.com/frostbyte73/core"
3434
"github.com/icholy/digest"
35+
psdp "github.com/pion/sdp/v3"
3536
"github.com/pkg/errors"
3637

3738
"github.com/livekit/media-sdk/dtmf"
@@ -859,6 +860,7 @@ func (c *inboundCall) runMediaConn(offerData []byte, enc livekit.SIPMediaEncrypt
859860
if err != nil {
860861
return nil, err
861862
}
863+
c.cc.nextSDPVersion = answer.SDP.Origin.SessionVersion + 1
862864
c.mon.SDPSize(len(answerData), false)
863865
c.log.Debugw("SDP answer", "sdp", string(answerData))
864866

@@ -1245,6 +1247,48 @@ func (c *inboundCall) transferCall(ctx context.Context, transferTo string, heade
12451247

12461248
}
12471249

1250+
func (c *inboundCall) holdCall(ctx context.Context) error {
1251+
c.log.Infow("holding inbound call")
1252+
1253+
// Disable media timeout during hold to prevent call termination
1254+
if c.media != nil {
1255+
c.media.EnableTimeout(false)
1256+
c.log.Infow("media timeout disabled for hold")
1257+
}
1258+
1259+
err := c.cc.holdCall(ctx)
1260+
if err != nil {
1261+
c.log.Infow("inbound call failed to hold", "error", err)
1262+
// Re-enable timeout if hold failed
1263+
if c.media != nil {
1264+
c.media.EnableTimeout(true)
1265+
}
1266+
return err
1267+
}
1268+
1269+
c.log.Infow("inbound call held")
1270+
return nil
1271+
}
1272+
1273+
func (c *inboundCall) unholdCall(ctx context.Context) error {
1274+
c.log.Infow("unholding inbound call")
1275+
1276+
err := c.cc.unholdCall(ctx)
1277+
if err != nil {
1278+
c.log.Infow("inbound call failed to unhold", "error", err)
1279+
return err
1280+
}
1281+
1282+
// Re-enable media timeout after unhold
1283+
if c.media != nil {
1284+
c.media.EnableTimeout(true)
1285+
c.log.Infow("media timeout re-enabled after unhold")
1286+
}
1287+
1288+
c.log.Infow("inbound call unheld")
1289+
return nil
1290+
}
1291+
12481292
func (s *Server) newInbound(log logger.Logger, id LocalTag, contact URI, invite *sip.Request, inviteTx sip.ServerTransaction, getHeaders setHeadersFunc) *sipInbound {
12491293
c := &sipInbound{
12501294
log: log,
@@ -1296,6 +1340,7 @@ type sipInbound struct {
12961340
ringing chan struct{}
12971341
acked core.Fuse
12981342
setHeaders setHeadersFunc
1343+
nextSDPVersion uint64
12991344
}
13001345

13011346
func (c *sipInbound) ValidateInvite() error {
@@ -1744,3 +1789,169 @@ func (c *sipInbound) CloseWithStatus(code sip.StatusCode, status string) {
17441789
c.drop()
17451790
}
17461791
}
1792+
1793+
func (c *sipInbound) setMediaDirection(sdpData []byte, direction string) ([]byte, error) {
1794+
1795+
if len(sdpData) == 0 {
1796+
return sdpData, nil
1797+
}
1798+
1799+
// Parse SDP using the base Parse function (works for both offers and answers)
1800+
desc, err := sdp.Parse(sdpData)
1801+
if err != nil {
1802+
return nil, fmt.Errorf("failed to parse SDP: %w", err)
1803+
}
1804+
1805+
// Modify direction attributes in each media description
1806+
for _, mediaDesc := range desc.SDP.MediaDescriptions {
1807+
if mediaDesc == nil {
1808+
continue
1809+
}
1810+
1811+
// Find and remove existing direction attributes
1812+
var newAttributes []psdp.Attribute
1813+
for _, attr := range mediaDesc.Attributes {
1814+
// Keep all attributes except direction-related ones
1815+
if attr.Key != "sendrecv" && attr.Key != "sendonly" &&
1816+
attr.Key != "recvonly" && attr.Key != "inactive" {
1817+
newAttributes = append(newAttributes, attr)
1818+
}
1819+
}
1820+
1821+
// Add the new direction attribute
1822+
newAttributes = append(newAttributes, psdp.Attribute{
1823+
Key: direction,
1824+
Value: "",
1825+
})
1826+
1827+
mediaDesc.Attributes = newAttributes
1828+
}
1829+
1830+
// Set session version to current value plus current unix timestamp
1831+
desc.SDP.Origin.SessionVersion = c.nextSDPVersion
1832+
c.nextSDPVersion += 1
1833+
1834+
// Marshal back to bytes
1835+
modifiedSDP, err := desc.SDP.Marshal()
1836+
if err != nil {
1837+
return nil, fmt.Errorf("failed to marshal modified SDP: %w", err)
1838+
}
1839+
1840+
return modifiedSDP, nil
1841+
}
1842+
1843+
func (c *sipInbound) holdCall(ctx context.Context) error {
1844+
c.mu.Lock()
1845+
1846+
if c.invite == nil || c.inviteOk == nil {
1847+
c.mu.Unlock()
1848+
return psrpc.NewErrorf(psrpc.FailedPrecondition, "can't hold non established call")
1849+
}
1850+
1851+
// Create INVITE with SDP modified for hold (a=sendonly)
1852+
req := sip.NewRequest(sip.INVITE, c.invite.Recipient)
1853+
c.setCSeq(req)
1854+
1855+
// Copy headers from original INVITE
1856+
req.AppendHeader(c.invite.From())
1857+
req.AppendHeader(c.invite.To())
1858+
req.AppendHeader(c.invite.CallID())
1859+
req.AppendHeader(c.contact)
1860+
req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
1861+
req.AppendHeader(sip.NewHeader("Allow", "INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, MESSAGE, OPTIONS, INFO, SUBSCRIBE"))
1862+
1863+
// Modify SDP to set direction to sendonly (hold)
1864+
sdpOffer := c.inviteOk.Body()
1865+
if len(sdpOffer) > 0 {
1866+
modifiedSDP, err := c.setMediaDirection(sdpOffer, "sendonly")
1867+
if err != nil {
1868+
return err
1869+
}
1870+
req.SetBody(modifiedSDP)
1871+
}
1872+
1873+
c.swapSrcDst(req)
1874+
c.mu.Unlock()
1875+
1876+
// Send the INVITE request
1877+
tx, err := c.Transaction(req)
1878+
if err != nil {
1879+
return err
1880+
}
1881+
defer tx.Terminate()
1882+
1883+
resp, err := sipResponse(ctx, tx, c.s.closing.Watch(), nil)
1884+
if err != nil {
1885+
return err
1886+
}
1887+
1888+
if resp.StatusCode != sip.StatusOK {
1889+
return &livekit.SIPStatus{Code: livekit.SIPStatusCode(resp.StatusCode)}
1890+
}
1891+
1892+
// Send ACK for the hold INVITE
1893+
ack := sip.NewAckRequest(req, resp, nil)
1894+
if err := c.WriteRequest(ack); err != nil {
1895+
return err
1896+
}
1897+
1898+
return nil
1899+
}
1900+
1901+
func (c *sipInbound) unholdCall(ctx context.Context) error {
1902+
c.mu.Lock()
1903+
1904+
if c.invite == nil || c.inviteOk == nil {
1905+
c.mu.Unlock()
1906+
return psrpc.NewErrorf(psrpc.FailedPrecondition, "can't unhold non established call")
1907+
}
1908+
1909+
// Create INVITE with SDP modified for unhold (a=sendrecv)
1910+
req := sip.NewRequest(sip.INVITE, c.invite.Recipient)
1911+
c.setCSeq(req)
1912+
1913+
// Copy headers from original INVITE
1914+
req.AppendHeader(c.invite.From())
1915+
req.AppendHeader(c.invite.To())
1916+
req.AppendHeader(c.invite.CallID())
1917+
req.AppendHeader(c.contact)
1918+
req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
1919+
req.AppendHeader(sip.NewHeader("Allow", "INVITE, ACK, CANCEL, BYE, NOTIFY, REFER, MESSAGE, OPTIONS, INFO, SUBSCRIBE"))
1920+
1921+
// Modify SDP to set direction to sendrecv (unhold)
1922+
sdpOffer := c.inviteOk.Body()
1923+
if len(sdpOffer) > 0 {
1924+
modifiedSDP, err := c.setMediaDirection(sdpOffer, "sendrecv")
1925+
if err != nil {
1926+
return err
1927+
}
1928+
req.SetBody(modifiedSDP)
1929+
}
1930+
1931+
c.swapSrcDst(req)
1932+
c.mu.Unlock()
1933+
1934+
// Send the INVITE request
1935+
tx, err := c.Transaction(req)
1936+
if err != nil {
1937+
return err
1938+
}
1939+
defer tx.Terminate()
1940+
1941+
resp, err := sipResponse(ctx, tx, c.s.closing.Watch(), nil)
1942+
if err != nil {
1943+
return err
1944+
}
1945+
1946+
if resp.StatusCode != sip.StatusOK {
1947+
return &livekit.SIPStatus{Code: livekit.SIPStatusCode(resp.StatusCode)}
1948+
}
1949+
1950+
// Send ACK for the unhold INVITE
1951+
ack := sip.NewAckRequest(req, resp, nil)
1952+
if err := c.WriteRequest(ack); err != nil {
1953+
return err
1954+
}
1955+
1956+
return nil
1957+
}

0 commit comments

Comments
 (0)