Support G.711 A-law and U-law (#3849)

* More codecs

* clean up

* clean up

* add to unprocessed for nil mime type

* enhance tests to check for audio codec preferences also
This commit is contained in:
Raja Subramanian
2025-08-13 14:49:07 +05:30
committed by GitHub
parent fa5f4ef33c
commit a370bb2054
5 changed files with 236 additions and 109 deletions
+2
View File
@@ -355,6 +355,8 @@ var DefaultConfig = Config{
Room: RoomConfig{
AutoCreate: true,
EnabledCodecs: []CodecSpec{
{Mime: mime.MimeTypePCMU.String()},
{Mime: mime.MimeTypePCMA.String()},
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeRED.String()},
{Mime: mime.MimeTypeVP8.String()},
+28 -1
View File
@@ -64,6 +64,33 @@ func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedbac
}
}
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypePCMU.String(),
ClockRate: 8000,
Channels: 1,
RTCPFeedback: rtcpFeedback.Audio,
},
PayloadType: 0,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypePCMA.String(),
ClockRate: 8000,
Channels: 1,
RTCPFeedback: rtcpFeedback.Audio,
},
PayloadType: 8,
},
} {
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
}
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
@@ -131,7 +158,7 @@ func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedbac
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
MimeType: mime.MimeTypeH265.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
+119 -78
View File
@@ -470,93 +470,134 @@ func TestDisablePublishCodec(t *testing.T) {
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
}
func TestPreferVideoCodecForPublisher(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i := 0; i < 2; i++ {
// publish h264 track without client preferred codec
trackCid := fmt.Sprintf("preferh264video%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Codec: "h264",
Cid: trackCid,
},
func TestPreferMediaCodecForPublisher(t *testing.T) {
testCases := []struct {
name string
mediaKind string
trackBaseCid string
preferredCodec string
addTrack *livekit.AddTrackRequest
mimeTypeStringChecker func(string) bool
mimeTypeCodecStringChecker func(string) bool
transceiverMimeType mime.MimeType
}{
{
name: "video",
mediaKind: "video",
trackBaseCid: "preferH264Video",
preferredCodec: "h264",
addTrack: &livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
},
})
mimeTypeStringChecker: mime.IsMimeTypeStringH264,
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringH264,
transceiverMimeType: mime.MimeTypeVP8,
},
{
name: "audio",
mediaKind: "audio",
trackBaseCid: "preferPCMAAudio",
preferredCodec: "pcma",
addTrack: &livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
Name: "audio",
Source: livekit.TrackSource_MICROPHONE,
},
mimeTypeStringChecker: mime.IsMimeTypeStringPCMA,
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringPCMA,
transceiverMimeType: mime.MimeTypeOpus,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
if i > 0 {
// the negotiated codecs order could be updated by first negotiation, reorder to make h264 not preferred
for mime.IsMimeTypeStringH264(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
// h264 should not be preferred
require.False(t, mime.IsMimeTypeStringH264(codecs[0].MimeType), "codecs", codecs)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
offerId := uint32(23)
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerId uint32
var answerReceived atomic.Bool
var answerIdReceived atomic.Uint32
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer, answerId = signalling.FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
answerIdReceived.Store(answerId)
for i := 0; i < 2; i++ {
// publish preferred track without client using setCodecPreferences()
trackCid := fmt.Sprintf("%s-%d", tc.trackBaseCid, i)
req := utils.CloneProto(tc.addTrack)
req.SimulcastCodecs = []*livekit.SimulcastCodec{
{
Codec: tc.preferredCodec,
Cid: trackCid,
},
}
}
return nil
})
participant.HandleOffer(sdp, offerId)
participant.AddTrack(req)
require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: tc.transceiverMimeType.String()}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
var h264Preferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var videoSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "video" {
if videoSectionIndex == i {
codecs, err := lksdp.CodecsFromMediaDescription(m)
require.NoError(t, err)
if mime.IsMimeTypeCodecStringH264(codecs[0].Name) {
h264Preferred = true
break
if i > 0 {
// the negotiated codecs order could be updated by first negotiation,
// reorder to make tested preferred codec not preferred
for tc.mimeTypeStringChecker(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
videoSectionIndex++
}
}
// preferred codec should not be preferred in `offer`
require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)
require.Truef(t, h264Preferred, "h264 should be preferred for video section %d, answer sdp: \n%s", i, answer.SDP)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
offerId := uint32(23)
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerId uint32
var answerReceived atomic.Bool
var answerIdReceived atomic.Uint32
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer, answerId = signalling.FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(sdp, offerId)
require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)
var havePreferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var mediaSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == tc.mediaKind {
if mediaSectionIndex == i {
codecs, err := lksdp.CodecsFromMediaDescription(m)
require.NoError(t, err)
if tc.mimeTypeCodecStringChecker(codecs[0].Name) {
havePreferred = true
break
}
}
mediaSectionIndex++
}
}
require.Truef(t, havePreferred, "%s should be preferred for %s section %d, answer sdp: \n%s", tc.preferredCodec, tc.mediaKind, i, answer.SDP)
}
})
}
}
+71 -30
View File
@@ -152,8 +152,13 @@ func (p *ParticipantImpl) setCodecPreferencesForPublisher(
unmatchAudios []*sdp.MediaDescription,
unmatchVideos []*sdp.MediaDescription,
) *sdp.SessionDescription {
parsedOffer = p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unmatchAudios)
parsedOffer = p.setCodecPreferencesVideoForPublisher(parsedOffer, unmatchVideos)
parsedOffer, unprocessedUnmatchAudios := p.setCodecPreferencesForPublisherMedia(
parsedOffer,
unmatchAudios,
livekit.TrackType_AUDIO,
)
parsedOffer = p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unprocessedUnmatchAudios)
parsedOffer, _ = p.setCodecPreferencesForPublisherMedia(parsedOffer, unmatchVideos, livekit.TrackType_VIDEO)
return parsedOffer
}
@@ -231,14 +236,17 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
return parsedOffer
}
func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(
func (p *ParticipantImpl) setCodecPreferencesForPublisherMedia(
parsedOffer *sdp.SessionDescription,
unmatchVideos []*sdp.MediaDescription,
) *sdp.SessionDescription {
// unmatched video is pending for publish, set codec preference
for _, unmatchVideo := range unmatchVideos {
streamID, ok := lksdp.ExtractStreamID(unmatchVideo)
unmatches []*sdp.MediaDescription,
trackType livekit.TrackType,
) (*sdp.SessionDescription, []*sdp.MediaDescription) {
unprocessed := make([]*sdp.MediaDescription, 0, len(unmatches))
// unmatched media is pending for publish, set codec preference
for _, unmatch := range unmatches {
streamID, ok := lksdp.ExtractStreamID(unmatch)
if !ok {
unprocessed = append(unprocessed, unmatch)
continue
}
@@ -248,13 +256,15 @@ func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(
if mt != nil {
info = mt.ToProto()
} else {
_, info, _, _, _ = p.getPendingTrack(streamID, livekit.TrackType_VIDEO, false)
_, info, _, _, _ = p.getPendingTrack(streamID, trackType, false)
}
if info == nil {
p.pendingTracksLock.RUnlock()
unprocessed = append(unprocessed, unmatch)
continue
}
var mimeType string
for _, c := range info.Codecs {
if c.Cid == streamID || c.SdpCid == streamID {
@@ -267,35 +277,66 @@ func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(
}
p.pendingTracksLock.RUnlock()
if mimeType != "" {
codecs, err := lksdp.CodecsFromMediaDescription(unmatchVideo)
if err != nil {
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatchVideo,
"parsedOffer", parsedOffer,
)
continue
}
if mimeType == "" {
unprocessed = append(unprocessed, unmatch)
continue
}
var preferredCodecs, leftCodecs []string
for _, c := range codecs {
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
}
}
codecs, err := lksdp.CodecsFromMediaDescription(unmatch)
if err != nil {
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatch,
"parsedOffer", parsedOffer,
)
unprocessed = append(unprocessed, unmatch)
continue
}
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats[:0], preferredCodecs...)
var codecIdx int
var preferredCodecs, leftCodecs []string
for idx, c := range codecs {
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
codecIdx = idx
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
}
}
// could not find preferred mime in the offer
if len(preferredCodecs) == 0 {
unprocessed = append(unprocessed, unmatch)
continue
}
unmatch.MediaName.Formats = append(unmatch.MediaName.Formats[:0], preferredCodecs...)
if trackType == livekit.TrackType_VIDEO {
// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
if p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer() {
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats, leftCodecs...)
unmatch.MediaName.Formats = append(unmatch.MediaName.Formats, leftCodecs...)
}
} else {
// ensure nack enabled for audio in publisher offer
var nackFound bool
for _, attr := range unmatch.Attributes {
if attr.Key == "rtcp-fb" && strings.Contains(attr.Value, fmt.Sprintf("%d nack", codecs[codecIdx].PayloadType)) {
nackFound = true
break
}
}
if !nackFound {
unmatch.Attributes = append(unmatch.Attributes, sdp.Attribute{
Key: "rtcp-fb",
Value: fmt.Sprintf("%d nack", codecs[codecIdx].PayloadType),
})
}
unmatch.MediaName.Formats = append(unmatch.MediaName.Formats, leftCodecs...)
}
}
return parsedOffer
return parsedOffer, unprocessed
}
// configure publisher answer for audio track's dtx and stereo settings
+16
View File
@@ -166,6 +166,14 @@ func IsMimeTypeCodecStringRED(codec string) bool {
return NormalizeMimeTypeCodec(codec) == MimeTypeCodecRED
}
func IsMimeTypeCodecStringPCMA(codec string) bool {
return NormalizeMimeTypeCodec(codec) == MimeTypeCodecPCMA
}
func IsMimeTypeCodecStringPCMU(codec string) bool {
return NormalizeMimeTypeCodec(codec) == MimeTypeCodecPCMU
}
func IsMimeTypeCodecStringH264(codec string) bool {
return NormalizeMimeTypeCodec(codec) == MimeTypeCodecH264
}
@@ -336,6 +344,14 @@ func IsMimeTypeStringOpus(mime string) bool {
return NormalizeMimeType(mime) == MimeTypeOpus
}
func IsMimeTypeStringPCMA(mime string) bool {
return NormalizeMimeType(mime) == MimeTypePCMA
}
func IsMimeTypeStringPCMU(mime string) bool {
return NormalizeMimeType(mime) == MimeTypePCMU
}
func IsMimeTypeStringRTX(mime string) bool {
return NormalizeMimeType(mime) == MimeTypeRTX
}