-
Notifications
You must be signed in to change notification settings - Fork 238
feat: add opt-in Opus codec support for SIP media negotiation #738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thetillhoff
wants to merge
13
commits into
livekit:main
Choose a base branch
from
thetillhoff:feature/opus-codec-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
c6f658f
Add Opus codec support for SIP media negotiation
thetillhoff e249a17
feat: add EnableOpus config flag
thetillhoff 3654be0
fix: make Opus opt-in and replace panic with error log
thetillhoff dc081ea
feat: wire EnableOpus config flag to SetOpusEnabled
thetillhoff e20be80
test: add resolveSDPName and codecSet unit tests
thetillhoff 09768a8
test: add SDP negotiation tests for Opus codec
thetillhoff 6b955b6
docs: document enable_opus config flag
thetillhoff bfb5ef8
fix: update TestMediaPort string assertions for 3-part SDP names and …
thetillhoff 5ebde3c
test: skip Opus in generic TestMediaPort harness
thetillhoff 8e49c7b
fix: list Opus in defaultCodecs init map as disabled
thetillhoff e6fe0c2
feat: add OpusConfig and encoder options (bitrate, complexity, FEC)
thetillhoff 8308ca2
Merge remote-tracking branch 'origin/main' into feature/opus-codec-su…
thetillhoff 2448a3d
fix: drop PR reference from Opus encoder TODO comment
thetillhoff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // Copyright 2024 LiveKit, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build cgo | ||
|
|
||
| package sip | ||
|
|
||
| import ( | ||
| "sync/atomic" | ||
|
|
||
| msdk "github.com/livekit/media-sdk" | ||
| "github.com/livekit/media-sdk/opus" | ||
| "github.com/livekit/protocol/logger" | ||
| ) | ||
|
|
||
| var opusEncodeOpts atomic.Pointer[OpusEncodeOptions] | ||
|
|
||
| // SetOpusOptions configures the Opus encoder. Call before or after enabling; | ||
| // takes effect on the next encoder instantiation (i.e. next call). | ||
| func SetOpusOptions(opts OpusEncodeOptions) { | ||
| opusEncodeOpts.Store(&opts) | ||
| } | ||
|
|
||
| func init() { | ||
| msdk.RegisterCodec(msdk.NewAudioCodec(msdk.CodecInfo{ | ||
| SDPName: OpusSDPName, | ||
| SampleRate: 48000, | ||
| RTPClockRate: 48000, | ||
| RTPIsStatic: false, | ||
| Priority: 10, | ||
| Disabled: true, | ||
| FileExt: "opus", | ||
| }, opusDecode, opusEncode)) | ||
| } | ||
|
|
||
| // SetOpusEnabled toggles Opus in both the per-call default codec set and the | ||
| // global media-sdk codec set. Call once during Service.Start. | ||
| func SetOpusEnabled(enabled bool) { | ||
| defaultCodecs.SetEnabled(OpusSDPName, enabled) | ||
| msdk.CodecSetEnabled(OpusSDPName, enabled) | ||
| } | ||
|
|
||
| func opusDecode(w msdk.PCM16Writer) msdk.WriteCloser[opus.Sample] { | ||
| dec, err := opus.Decode(w, 1, logger.GetLogger()) | ||
| if err != nil { | ||
| logger.GetLogger().Errorw("opus decode init failed", err) | ||
| return nil | ||
| } | ||
| return dec | ||
| } | ||
|
|
||
| func opusEncode(w msdk.WriteCloser[opus.Sample]) msdk.PCM16Writer { | ||
| // TODO: apply opusEncodeOpts once livekit/media-sdk#69 (EncodeWith) merges. | ||
| enc, err := opus.Encode(w, 1, logger.GetLogger()) | ||
| if err != nil { | ||
| logger.GetLogger().Errorw("opus encode init failed", err) | ||
| return nil | ||
| } | ||
| return enc | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // Copyright 2024 LiveKit, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build !cgo | ||
|
|
||
| package sip | ||
|
|
||
| // SetOpusEnabled is a no-op in non-CGo builds; Opus requires libopus. | ||
| func SetOpusEnabled(_ bool) {} | ||
|
|
||
| // SetOpusOptions is a no-op in non-CGo builds; Opus requires libopus. | ||
| func SetOpusOptions(_ OpusEncodeOptions) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| // Copyright 2024 LiveKit, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| //go:build cgo | ||
|
|
||
| package sip | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| msdk "github.com/livekit/media-sdk" | ||
| "github.com/livekit/media-sdk/g711" | ||
| "github.com/livekit/media-sdk/g722" | ||
| "github.com/livekit/media-sdk/sdp" | ||
| "github.com/livekit/protocol/livekit" | ||
| ) | ||
|
|
||
| func TestResolveSDPName(t *testing.T) { | ||
| t.Run("two-part name resolves to three-part", func(t *testing.T) { | ||
| got := resolveSDPName("opus/48000") | ||
| require.Equal(t, OpusSDPName, got) | ||
| }) | ||
| t.Run("exact three-part match returns empty", func(t *testing.T) { | ||
| got := resolveSDPName("opus/48000/2") | ||
| require.Empty(t, got) | ||
| }) | ||
| t.Run("unknown codec returns empty", func(t *testing.T) { | ||
| got := resolveSDPName("unknown/8000") | ||
| require.Empty(t, got) | ||
| }) | ||
| } | ||
|
|
||
| func TestCodecSetWithOpus(t *testing.T) { | ||
| enableOpusForTest(t) | ||
|
|
||
| m := &livekit.SIPMediaConfig{ | ||
| OnlyListedCodecs: true, | ||
| Codecs: []*livekit.SIPCodec{ | ||
| {Name: "opus", Rate: 48000}, | ||
| }, | ||
| } | ||
| s, err := codecSet(m) | ||
| require.NoError(t, err) | ||
| require.True(t, s.IsEnabledByName(OpusSDPName), | ||
| "codecSet should enable opus/48000/2 when opus/48000 is listed") | ||
| } | ||
|
|
||
| // enableOpusForTest turns Opus on for a test and restores disabled state after. | ||
| func enableOpusForTest(t *testing.T) { | ||
| t.Helper() | ||
| SetOpusEnabled(true) | ||
| t.Cleanup(func() { SetOpusEnabled(false) }) | ||
| } | ||
|
|
||
| // TestOpusDisabledByDefault verifies that without calling SetOpusEnabled, | ||
| // Opus is absent from defaultCodecs — so existing deployments are unaffected. | ||
| func TestOpusDisabledByDefault(t *testing.T) { | ||
| c := sdp.CodecByNameWith(defaultCodecs, OpusSDPName, nil) | ||
| require.Nil(t, c, "opus must not appear in defaultCodecs by default") | ||
| } | ||
|
|
||
| // TestOpusRegistered verifies the codec is present in msdk.Codecs(), uses a | ||
| // dynamic payload type, and runs at the correct 48 kHz clock rate. | ||
| func TestOpusRegistered(t *testing.T) { | ||
| enableOpusForTest(t) | ||
|
|
||
| c := sdp.CodecByNameWith(defaultCodecs, OpusSDPName, nil) | ||
| require.NotNil(t, c, "opus codec must be present in defaultCodecs when enabled") | ||
|
|
||
| _, ok := c.(msdk.AudioCodec) | ||
| require.True(t, ok, "opus codec must implement AudioCodec") | ||
|
|
||
| info := c.Info() | ||
| require.Equal(t, OpusSDPName, info.SDPName) | ||
| require.Equal(t, 48000, info.SampleRate) | ||
| require.Equal(t, 48000, info.RTPClockRate) | ||
| require.False(t, info.RTPIsStatic, "opus must use a dynamic payload type") | ||
| } | ||
|
|
||
| // TestOpusInSDPOffer verifies that after enabling Opus, an SDP offer contains | ||
| // an rtpmap line advertising opus/48000/2. | ||
| func TestOpusInSDPOffer(t *testing.T) { | ||
| enableOpusForTest(t) | ||
|
|
||
| _, md, err := sdp.OfferMediaWith(defaultCodecs, 12345, sdp.EncryptionNone) | ||
| require.NoError(t, err) | ||
|
|
||
| var found bool | ||
| for _, a := range md.Attributes { | ||
| if a.Key == "rtpmap" && strings.Contains(strings.ToLower(a.Value), "opus/48000/2") { | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
| require.True(t, found, "SDP offer should contain an rtpmap line for opus/48000/2") | ||
| } | ||
|
|
||
| // TestOpusPreferredOverG722 verifies codec selection picks Opus (priority 10) | ||
| // over G722 (priority -5) and G711 (priority -10/-20) when all are offered. | ||
| func TestOpusPreferredOverG722(t *testing.T) { | ||
| enableOpusForTest(t) | ||
|
|
||
| opusC, ok := sdp.CodecByNameWith(defaultCodecs, OpusSDPName, nil).(msdk.AudioCodec) | ||
| require.True(t, ok, "opus must be an AudioCodec") | ||
|
|
||
| ulawC, ok := sdp.CodecByNameWith(defaultCodecs, g711.ULawSDPName, nil).(msdk.AudioCodec) | ||
| require.True(t, ok, "PCMU must be an AudioCodec") | ||
|
|
||
| g722C, ok := sdp.CodecByNameWith(defaultCodecs, g722.SDPName, nil).(msdk.AudioCodec) | ||
| require.True(t, ok, "G722 must be an AudioCodec") | ||
|
|
||
| desc := sdp.MediaDesc{ | ||
| Codecs: []sdp.CodecInfo{ | ||
| {Type: 0, Codec: ulawC}, | ||
| {Type: 9, Codec: g722C}, | ||
| {Type: 111, Codec: opusC}, | ||
| }, | ||
| } | ||
| got, err := sdp.SelectAudio(desc, false) | ||
| require.NoError(t, err) | ||
| require.Equal(t, OpusSDPName, got.Codec.Info().SDPName, | ||
| "Opus should win priority-based codec selection") | ||
| require.Equal(t, byte(111), got.Type, | ||
| "peer-assigned payload type 111 must be honored") | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.