client: support writing to ONVIF back channels (#101) (#462)

This commit is contained in:
Alessandro Ros
2023-11-15 13:20:29 +01:00
committed by GitHub
parent aaef8c29a7
commit f78b04cf4e
9 changed files with 414 additions and 92 deletions

View File

@@ -20,6 +20,7 @@ Features:
* Switch transport protocol automatically
* Read selected media streams
* Pause or seek without disconnecting from the server
* Write to ONVIF back channels
* Get PTS (relative) timestamp of incoming packets
* Get NTP (absolute) timestamp of incoming packets
* Record (write)
@@ -58,6 +59,7 @@ Features:
* [client-play-options](examples/client-play-options/main.go)
* [client-play-pause](examples/client-play-pause/main.go)
* [client-play-to-record](examples/client-play-to-record/main.go)
* [client-play-backchannel](examples/client-play-backchannel/main.go)
* [client-play-format-av1](examples/client-play-format-av1/main.go)
* [client-play-format-g711](examples/client-play-format-g711/main.go)
* [client-play-format-g722](examples/client-play-format-g722/main.go)

View File

@@ -245,6 +245,8 @@ type Client struct {
UserAgent string
// disable automatic RTCP sender reports.
DisableRTCPSenderReports bool
// explicitly request back channels to the server.
RequestBackChannels bool
// pointer to a variable that stores received bytes.
BytesReceived *uint64
// pointer to a variable that stores sent bytes.
@@ -301,6 +303,8 @@ type Client struct {
lastDescribeURL *base.URL
baseURL *base.URL
effectiveTransport *Transport
backChannelSetupped bool
stdChannelSetupped bool
medias map[*description.Media]*clientMedia
tcpCallbackByChannel map[int]readFunc
lastRange *headers.Range
@@ -662,9 +666,16 @@ func (c *Client) doClose() {
}
if c.nconn != nil && c.baseURL != nil {
header := base.Header{}
if c.backChannelSetupped {
header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"}
}
c.do(&base.Request{ //nolint:errcheck
Method: base.Teardown,
URL: c.baseURL,
Header: header,
}, true)
}
@@ -696,6 +707,8 @@ func (c *Client) reset() {
c.useGetParameter = false
c.baseURL = nil
c.effectiveTransport = nil
c.backChannelSetupped = false
c.stdChannelSetupped = false
c.medias = nil
c.tcpCallbackByChannel = nil
}
@@ -776,13 +789,13 @@ func (c *Client) trySwitchingProtocol2(medi *description.Media, baseURL *base.UR
func (c *Client) startReadRoutines() {
// allocate writer here because it's needed by RTCP receiver / sender
if c.state == clientStatePlay {
if c.state == clientStateRecord || c.backChannelSetupped {
c.writer.allocateBuffer(c.WriteQueueSize)
} else {
// when reading, buffer is only used to send RTCP receiver reports,
// that are much smaller than RTP packets and are sent at a fixed interval.
// decrease RAM consumption by allocating less buffers.
c.writer.allocateBuffer(8)
} else {
c.writer.allocateBuffer(c.WriteQueueSize)
}
c.timeDecoder = rtptime.NewGlobalDecoder()
@@ -791,7 +804,7 @@ func (c *Client) startReadRoutines() {
cm.start()
}
if c.state == clientStatePlay {
if c.state == clientStatePlay && c.stdChannelSetupped {
c.keepaliveTimer = time.NewTimer(c.keepalivePeriod)
switch *c.effectiveTransport {
@@ -991,7 +1004,7 @@ func (c *Client) isInTCPTimeout() bool {
func (c *Client) doCheckTimeout() error {
if *c.effectiveTransport == TransportUDP ||
*c.effectiveTransport == TransportUDPMulticast {
if c.checkTimeoutInitial {
if c.checkTimeoutInitial && !c.backChannelSetupped {
c.checkTimeoutInitial = false
if c.atLeastOneUDPPacketHasBeenReceived() {
@@ -1092,12 +1105,18 @@ func (c *Client) doDescribe(u *base.URL) (*description.Session, *base.Response,
return nil, nil, err
}
header := base.Header{
"Accept": base.HeaderValue{"application/sdp"},
}
if c.RequestBackChannels {
header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"}
}
res, err := c.do(&base.Request{
Method: base.Describe,
URL: u,
Header: base.Header{
"Accept": base.HeaderValue{"application/sdp"},
},
Header: header,
}, false)
if err != nil {
return nil, nil, err
@@ -1334,12 +1353,18 @@ func (c *Client) doSetup(
return nil, err
}
header := base.Header{
"Transport": th.Marshal(),
}
if medi.IsBackChannel {
header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"}
}
res, err := c.do(&base.Request{
Method: base.Setup,
URL: mediaURL,
Header: base.Header{
"Transport": th.Marshal(),
},
Header: header,
}, false)
if err != nil {
cm.close()
@@ -1509,6 +1534,12 @@ func (c *Client) doSetup(
c.baseURL = baseURL
c.effectiveTransport = &desiredTransport
if medi.IsBackChannel {
c.backChannelSetupped = true
} else {
c.stdChannelSetupped = true
}
if c.state == clientStateInitial {
c.state = clientStatePrePlay
}
@@ -1590,12 +1621,18 @@ func (c *Client) doPlay(ra *headers.Range) (*base.Response, error) {
}
}
header := base.Header{
"Range": ra.Marshal(),
}
if c.backChannelSetupped {
header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"}
}
res, err := c.do(&base.Request{
Method: base.Play,
URL: c.baseURL,
Header: base.Header{
"Range": ra.Marshal(),
},
Header: header,
}, false)
if err != nil {
c.stopReadRoutines()

View File

@@ -20,7 +20,7 @@ type clientFormat struct {
udpReorderer *rtpreorderer.Reorderer // play
tcpLossDetector *rtplossdetector.LossDetector // play
rtcpReceiver *rtcpreceiver.RTCPReceiver // play
rtcpSender *rtcpsender.RTCPSender // record
rtcpSender *rtcpsender.RTCPSender // record or back channel
onPacketRTP OnPacketRTPFunc
}
@@ -33,7 +33,17 @@ func newClientFormat(cm *clientMedia, forma format.Format) *clientFormat {
}
func (ct *clientFormat) start() {
if ct.cm.c.state == clientStatePlay {
if ct.cm.c.state == clientStateRecord || ct.cm.media.IsBackChannel {
ct.rtcpSender = rtcpsender.New(
ct.format.ClockRate(),
ct.cm.c.senderReportPeriod,
ct.cm.c.timeNow,
func(pkt rtcp.Packet) {
if !ct.cm.c.DisableRTCPSenderReports {
ct.cm.c.WritePacketRTCP(ct.cm.media, pkt) //nolint:errcheck
}
})
} else {
if ct.cm.udpRTPListener != nil {
ct.udpReorderer = rtpreorderer.New()
} else {
@@ -54,16 +64,6 @@ func (ct *clientFormat) start() {
if err != nil {
panic(err)
}
} else {
ct.rtcpSender = rtcpsender.New(
ct.format.ClockRate(),
ct.cm.c.senderReportPeriod,
ct.cm.c.timeNow,
func(pkt rtcp.Packet) {
if !ct.cm.c.DisableRTCPSenderReports {
ct.cm.c.WritePacketRTCP(ct.cm.media, pkt) //nolint:errcheck
}
})
}
}

View File

@@ -93,12 +93,12 @@ func (cm *clientMedia) start() {
cm.writePacketRTPInQueue = cm.writePacketRTPInQueueUDP
cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueUDP
if cm.c.state == clientStatePlay {
cm.udpRTPListener.readFunc = cm.readRTPUDPPlay
cm.udpRTCPListener.readFunc = cm.readRTCPUDPPlay
} else {
if cm.c.state == clientStateRecord || cm.media.IsBackChannel {
cm.udpRTPListener.readFunc = cm.readRTPUDPRecord
cm.udpRTCPListener.readFunc = cm.readRTCPUDPRecord
} else {
cm.udpRTPListener.readFunc = cm.readRTPUDPPlay
cm.udpRTCPListener.readFunc = cm.readRTCPUDPPlay
}
} else {
cm.writePacketRTPInQueue = cm.writePacketRTPInQueueTCP
@@ -108,12 +108,12 @@ func (cm *clientMedia) start() {
cm.c.tcpCallbackByChannel = make(map[int]readFunc)
}
if cm.c.state == clientStatePlay {
cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readRTPTCPPlay
cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readRTCPTCPPlay
} else {
if cm.c.state == clientStateRecord || cm.media.IsBackChannel {
cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readRTPTCPRecord
cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readRTCPTCPRecord
} else {
cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readRTPTCPPlay
cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readRTCPTCPPlay
}
cm.tcpRTPFrame = &base.InterleavedFrame{Channel: cm.tcpChannel}

View File

@@ -3337,3 +3337,189 @@ func TestClientPlayPacketNTP(t *testing.T) {
<-recv
}
func TestClientPlayBackChannel(t *testing.T) {
l, err := net.Listen("tcp", "localhost:8554")
require.NoError(t, err)
defer l.Close()
serverDone := make(chan struct{})
defer func() { <-serverDone }()
go func() {
defer close(serverDone)
nconn, err := l.Accept()
require.NoError(t, err)
defer nconn.Close()
conn := conn.NewConn(nconn)
req, err := conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Options, req.Method)
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
"Public": base.HeaderValue{strings.Join([]string{
string(base.Describe),
string(base.Setup),
string(base.Play),
}, ", ")},
},
})
require.NoError(t, err)
req, err = conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Describe, req.Method)
require.Equal(t, base.HeaderValue{"www.onvif.org/ver20/backchannel"}, req.Header["Require"])
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
"Content-Type": base.HeaderValue{"application/sdp"},
"Content-Base": base.HeaderValue{"rtsp://localhost:8554/teststream/"},
},
Body: mediasToSDP([]*description.Media{
testH264Media,
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G711{}},
IsBackChannel: true,
},
}),
})
require.NoError(t, err)
req, err = conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Setup, req.Method)
require.Equal(t, base.HeaderValue(nil), req.Header["Require"])
var inTH headers.Transport
err = inTH.Unmarshal(req.Header["Transport"])
require.NoError(t, err)
th := headers.Transport{
Delivery: deliveryPtr(headers.TransportDeliveryUnicast),
Protocol: headers.TransportProtocolTCP,
InterleavedIDs: inTH.InterleavedIDs,
}
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
"Transport": th.Marshal(),
},
})
require.NoError(t, err)
req, err = conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Setup, req.Method)
require.Equal(t, base.HeaderValue{"www.onvif.org/ver20/backchannel"}, req.Header["Require"])
err = inTH.Unmarshal(req.Header["Transport"])
require.NoError(t, err)
th = headers.Transport{
Delivery: deliveryPtr(headers.TransportDeliveryUnicast),
Protocol: headers.TransportProtocolTCP,
InterleavedIDs: inTH.InterleavedIDs,
}
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
Header: base.Header{
"Transport": th.Marshal(),
},
})
require.NoError(t, err)
req, err = conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Play, req.Method)
require.Equal(t, base.HeaderValue{"www.onvif.org/ver20/backchannel"}, req.Header["Require"])
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
})
require.NoError(t, err)
f, err := conn.ReadInterleavedFrame()
require.NoError(t, err)
require.Equal(t, 2, f.Channel)
f, err = conn.ReadInterleavedFrame()
require.NoError(t, err)
require.Equal(t, 3, f.Channel)
packets, err := rtcp.Unmarshal(f.Payload)
require.NoError(t, err)
sr, ok := packets[0].(*rtcp.SenderReport)
require.Equal(t, true, ok)
require.Equal(t, uint32(0x38F27A2F), sr.SSRC)
require.Equal(t, uint32(1), sr.PacketCount)
require.Equal(t, uint32(4), sr.OctetCount)
err = conn.WriteInterleavedFrame(&base.InterleavedFrame{
Channel: 0,
Payload: testRTPPacketMarshaled,
}, make([]byte, 1024))
require.NoError(t, err)
req, err = conn.ReadRequest()
require.NoError(t, err)
require.Equal(t, base.Teardown, req.Method)
require.Equal(t, base.HeaderValue{"www.onvif.org/ver20/backchannel"}, req.Header["Require"])
err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusOK,
})
require.NoError(t, err)
}()
c := Client{
RequestBackChannels: true,
Transport: transportPtr(TransportTCP),
senderReportPeriod: 500 * time.Millisecond,
receiverReportPeriod: 750 * time.Millisecond,
}
u, err := base.ParseURL("rtsp://localhost:8554/teststream")
require.NoError(t, err)
err = c.Start(u.Scheme, u.Host)
require.NoError(t, err)
defer c.Close()
sd, _, err := c.Describe(u)
require.NoError(t, err)
err = c.SetupAll(sd.BaseURL, sd.Medias)
require.NoError(t, err)
recv := make(chan struct{})
c.OnPacketRTP(sd.Medias[0], sd.Medias[0].Formats[0], func(pkt *rtp.Packet) {
close(recv)
})
_, err = c.Play(nil)
require.NoError(t, err)
err = c.WritePacketRTP(sd.Medias[1], &rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 8,
CSRC: []uint32{},
SSRC: 0x38F27A2F,
},
Payload: []byte{1, 2, 3, 4},
})
require.NoError(t, err)
<-recv
}

View File

@@ -0,0 +1,122 @@
package main
import (
"log"
"net"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/pion/rtp"
)
// This example shows how to
// 1. generate RTP/G711 packets with GStreamer
// 2. connect to a RTSP server, find a back channel that supports G711
// 3. route the packets from GStreamer to the channel
func findPCMUBackChannel(desc *description.Session) *description.Media {
for _, media := range desc.Medias {
if media.IsBackChannel {
for _, forma := range media.Formats {
if g711, ok := forma.(*format.G711); ok {
if g711.MULaw {
return media
}
}
}
}
}
return nil
}
func main() {
// open a listener to receive RTP/G711 packets
pc, err := net.ListenPacket("udp", "localhost:9000")
if err != nil {
panic(err)
}
defer pc.Close()
log.Println("Waiting for a RTP/G711 stream on UDP port 9000 - you can generate one with GStreamer:\n\n" +
"* audio from a test sine:\n\n" +
"gst-launch-1.0 audiotestsrc freq=300 ! audioconvert ! audioresample ! audio/x-raw,rate=8000" +
" ! mulawenc ! rtppcmupay ! udpsink host=127.0.0.1 port=9000\n\n" +
"* audio from a file:\n\n" +
"gst-launch-1.0 filesrc location=my_file.mp4 ! decodebin ! audioconvert ! audioresample ! audio/x-raw,rate=8000" +
" ! mulawenc ! rtppcmupay ! udpsink host=127.0.0.1 port=9000\n\n" +
"* audio from a microphone:\n\n" +
"gst-launch-1.0 pulsesrc ! audioconvert ! audioresample ! audio/x-raw,rate=8000" +
" ! mulawenc ! rtppcmupay ! udpsink host=127.0.0.1 port=9000\n")
// wait for first packet
buf := make([]byte, 2048)
n, _, err := pc.ReadFrom(buf)
if err != nil {
panic(err)
}
log.Println("stream connected")
c := gortsplib.Client{
RequestBackChannels: true,
}
// parse URL
u, err := base.ParseURL("rtsp://localhost:8554/mystream")
if err != nil {
panic(err)
}
// connect to the server
err = c.Start(u.Scheme, u.Host)
if err != nil {
panic(err)
}
defer c.Close()
// find published medias
desc, _, err := c.Describe(u)
if err != nil {
panic(err)
}
// find the back channel
medi := findPCMUBackChannel(desc)
if medi == nil {
panic("media not found")
}
// setup a single media
_, err = c.Setup(desc.BaseURL, medi, 0, 0)
if err != nil {
panic(err)
}
// start playing
_, err = c.Play(nil)
if err != nil {
panic(err)
}
var pkt rtp.Packet
for {
// parse RTP packet
err = pkt.Unmarshal(buf[:n])
if err != nil {
panic(err)
}
// route RTP packet to the server
err = c.WritePacketRTP(medi, &pkt)
if err != nil {
panic(err)
}
// read another RTP packet from source
n, _, err = pc.ReadFrom(buf)
if err != nil {
panic(err)
}
}
}

View File

@@ -41,20 +41,13 @@ func getAttribute(attributes []psdp.Attribute, key string) string {
return ""
}
func getDirection(attributes []psdp.Attribute) MediaDirection {
func isBackChannel(attributes []psdp.Attribute) bool {
for _, attr := range attributes {
switch attr.Key {
case "sendonly":
return MediaDirectionSendonly
case "recvonly":
return MediaDirectionRecvonly
case "sendrecv":
return MediaDirectionSendrecv
if attr.Key == "sendonly" {
return true
}
}
return ""
return false
}
func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string {
@@ -116,20 +109,10 @@ func isAlphaNumeric(v string) bool {
return true
}
// MediaDirection is the direction of a media stream.
type MediaDirection string
// standard directions.
const (
MediaDirectionSendonly MediaDirection = "sendonly"
MediaDirectionRecvonly MediaDirection = "recvonly"
MediaDirectionSendrecv MediaDirection = "sendrecv"
)
// MediaType is the type of a media stream.
type MediaType string
// standard media stream types.
// media types.
const (
MediaTypeVideo MediaType = "video"
MediaTypeAudio MediaType = "audio"
@@ -145,8 +128,8 @@ type Media struct {
// Media ID (optional).
ID string
// Direction of the stream (optional).
Direction MediaDirection
// Whether this media is a back channel.
IsBackChannel bool
// Control attribute.
Control string
@@ -164,7 +147,7 @@ func (m *Media) Unmarshal(md *psdp.MediaDescription) error {
return fmt.Errorf("invalid mid: %v", m.ID)
}
m.Direction = getDirection(md.Attributes)
m.IsBackChannel = isBackChannel(md.Attributes)
m.Control = getAttribute(md.Attributes, "control")
m.Formats = nil
@@ -211,9 +194,9 @@ func (m Media) Marshal() *psdp.MediaDescription {
})
}
if m.Direction != "" {
if m.IsBackChannel {
md.Attributes = append(md.Attributes, psdp.Attribute{
Key: string(m.Direction),
Key: "sendonly",
})
}

View File

@@ -51,7 +51,6 @@ var casesSession = []struct {
"a=rtpmap:97 H264/90000\r\n" +
"a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
"m=audio 0 RTP/AVP 0\r\n" +
"a=recvonly\r\n" +
"a=control:rtsp://10.0.100.50/profile5/media.smp/trackID=a\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"m=application 0 RTP/AVP 107\r\n" +
@@ -70,9 +69,8 @@ var casesSession = []struct {
}},
},
{
Type: MediaTypeAudio,
Direction: MediaDirectionRecvonly,
Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
Type: MediaTypeAudio,
Control: "rtsp://10.0.100.50/profile5/media.smp/trackID=a",
Formats: []format.Format{&format.G711{
MULaw: true,
}},
@@ -121,7 +119,6 @@ var casesSession = []struct {
"a=rtpmap:97 H264/90000\r\n" +
"a=fmtp:97 packetization-mode=1; profile-level-id=640028; sprop-parameter-sets=Z2QAKKy0A8ARPyo=,aO4Bniw=\r\n" +
"m=audio 0 RTP/AVP 0\r\n" +
"a=recvonly\r\n" +
"a=control:trackID=2\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"m=application 0 RTP/AVP 107\r\n" +
@@ -140,9 +137,8 @@ var casesSession = []struct {
}},
},
{
Type: MediaTypeAudio,
Direction: MediaDirectionRecvonly,
Control: "trackID=2",
Type: MediaTypeAudio,
Control: "trackID=2",
Formats: []format.Format{&format.G711{
MULaw: true,
}},
@@ -303,9 +299,9 @@ var casesSession = []struct {
Title: ``,
Medias: []*Media{
{
ID: "audio",
Type: MediaTypeAudio,
Direction: MediaDirectionSendonly,
ID: "audio",
Type: MediaTypeAudio,
IsBackChannel: true,
Formats: []format.Format{
&format.Opus{
PayloadTyp: 111,
@@ -371,9 +367,9 @@ var casesSession = []struct {
},
},
{
ID: "video",
Type: MediaTypeVideo,
Direction: MediaDirectionSendonly,
ID: "video",
Type: MediaTypeVideo,
IsBackChannel: true,
Formats: []format.Format{
&format.VP8{
PayloadTyp: 96,
@@ -500,11 +496,9 @@ var casesSession = []struct {
"c=IN IP4 0.0.0.0\r\n" +
"t=0 0\r\n" +
"m=video 0 RTP/AVP 26\r\n" +
"a=recvonly\r\n" +
"a=control:rtsp://192.168.0.1/video\r\n" +
"a=rtpmap:26 JPEG/90000\r\n" +
"m=audio 0 RTP/AVP 0\r\n" +
"a=recvonly\r\n" +
"a=control:rtsp://192.168.0.1/audio\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"m=audio 0 RTP/AVP 0\r\n" +
@@ -515,22 +509,20 @@ var casesSession = []struct {
Title: `RTSP Session with audiobackchannel`,
Medias: []*Media{
{
Type: MediaTypeVideo,
Direction: MediaDirectionRecvonly,
Control: "rtsp://192.168.0.1/video",
Formats: []format.Format{&format.MJPEG{}},
Type: MediaTypeVideo,
Control: "rtsp://192.168.0.1/video",
Formats: []format.Format{&format.MJPEG{}},
},
{
Type: MediaTypeAudio,
Direction: MediaDirectionRecvonly,
Control: "rtsp://192.168.0.1/audio",
Formats: []format.Format{&format.G711{MULaw: true}},
Type: MediaTypeAudio,
Control: "rtsp://192.168.0.1/audio",
Formats: []format.Format{&format.G711{MULaw: true}},
},
{
Type: MediaTypeAudio,
Direction: MediaDirectionSendonly,
Control: "rtsp://192.168.0.1/audioback",
Formats: []format.Format{&format.G711{MULaw: true}},
Type: MediaTypeAudio,
IsBackChannel: true,
Control: "rtsp://192.168.0.1/audioback",
Formats: []format.Format{&format.G711{MULaw: true}},
},
},
},

View File

@@ -32,9 +32,9 @@ func serverSideDescription(d *description.Session, contentBase *base.URL) *descr
for i, medi := range d.Medias {
mc := &description.Media{
Type: medi.Type,
ID: medi.ID,
// Direction: skipped for the moment
Type: medi.Type,
ID: medi.ID,
IsBackChannel: medi.IsBackChannel,
// we have to use trackID=number in order to support clients
// like the Grandstream GXV3500.
Control: "trackID=" + strconv.FormatInt(int64(i), 10),