From 01bd966ff8d72a81e13524430e802578575a0afe Mon Sep 17 00:00:00 2001 From: cnderrauber Date: Wed, 21 Jan 2026 16:11:08 +0800 Subject: [PATCH] Add silent frame for pcmu/a (#4258) * Add silent frame for pcmu/a * use red * solve comment --- pkg/sfu/downtrack.go | 106 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 17 deletions(-) diff --git a/pkg/sfu/downtrack.go b/pkg/sfu/downtrack.go index f604278f4..849b9bad5 100644 --- a/pkg/sfu/downtrack.go +++ b/pkg/sfu/downtrack.go @@ -139,6 +139,56 @@ var ( 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } + // PCMU (G.711 ยต-law) silence frame - 0xff represents zero amplitude + // 160 samples = 20ms at 8kHz sample rate + PCMUSilenceFrame = []byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + } + + // PCMA (G.711 A-law) silence frame - 0xd5 represents zero amplitude + // 160 samples = 20ms at 8kHz sample rate + PCMASilenceFrame = []byte{ + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, 0xd5, + } + dummyAbsSendTimeExt, _ = rtp.NewAbsSendTimeExtension(mono.Now()).Marshal() dummyTransportCCExt, _ = rtp.TransportCCExtension{TransportSequence: 12345}.Marshal() ) @@ -1697,9 +1747,13 @@ func (d *DownTrack) writeBlankFrameRTP(duration float32, generation uint32) chan var getBlankFrame func(bool) ([]byte, error) switch mimeType { case mime.MimeTypeOpus: - getBlankFrame = d.getOpusBlankFrame + getBlankFrame = d.getAudioBlankFrameFunc(OpusSilenceFrame) case mime.MimeTypeRED: getBlankFrame = d.getOpusRedBlankFrame + case mime.MimeTypePCMU: + getBlankFrame = d.getAudioBlankFrameFunc(PCMUSilenceFrame) + case mime.MimeTypePCMA: + getBlankFrame = d.getAudioBlankFrameFunc(PCMASilenceFrame) case mime.MimeTypeVP8: getBlankFrame = d.getVP8BlankFrame case mime.MimeTypeH264: @@ -1710,7 +1764,8 @@ func (d *DownTrack) writeBlankFrameRTP(duration float32, generation uint32) chan } frameRate := uint32(30) - if mimeType == mime.MimeTypeOpus || mimeType == mime.MimeTypeRED { + if mimeType == mime.MimeTypeOpus || mimeType == mime.MimeTypeRED || + mimeType == mime.MimeTypePCMU || mimeType == mime.MimeTypePCMA { frameRate = 50 } @@ -1801,15 +1856,17 @@ func (d *DownTrack) maybeAddTrailer(buf []byte) int { return len(d.params.Trailer) } -func (d *DownTrack) getOpusBlankFrame(_frameEndNeeded bool) ([]byte, error) { +func (d *DownTrack) getAudioBlankFrameFunc(silentPayload []byte) func(_frameEndNeeded bool) ([]byte, error) { // silence frame // Used shortly after muting to ensure residual noise does not keep // generating noise at the decoder after the stream is stopped // i. e. comfort noise generation actually not producing something comfortable. - payload := make([]byte, 1000) - copy(payload[0:], OpusSilenceFrame) - trailerLen := d.maybeAddTrailer(payload[len(OpusSilenceFrame):]) - return payload[:len(OpusSilenceFrame)+trailerLen], nil + return func(_frameEndNeeded bool) ([]byte, error) { + payload := make([]byte, 1000) + copy(payload[0:], silentPayload) + trailerLen := d.maybeAddTrailer(payload[len(silentPayload):]) + return payload[:len(silentPayload)+trailerLen], nil + } } func (d *DownTrack) getOpusRedBlankFrame(_frameEndNeeded bool) ([]byte, error) { @@ -2443,8 +2500,8 @@ func (d *DownTrack) sendPaddingOnMute() { if d.kind == webrtc.RTPCodecTypeVideo { d.sendPaddingOnMuteForVideo() - } else if d.Mime() == mime.MimeTypeOpus { - d.sendSilentFrameOnMuteForOpus() + } else { + d.sendSilentFrameOnMuteForAudio() } } @@ -2462,7 +2519,29 @@ func (d *DownTrack) sendPaddingOnMuteForVideo() { } } -func (d *DownTrack) sendSilentFrameOnMuteForOpus() { +func (d *DownTrack) sendSilentFrameOnMuteForAudio() { + var ( + payload []byte + err error + ) + switch d.Mime() { + case mime.MimeTypeOpus: + payload, err = d.getAudioBlankFrameFunc(OpusSilenceFrame)(false) + case mime.MimeTypeRED: + payload, err = d.getOpusRedBlankFrame(false) + case mime.MimeTypePCMU: + payload, err = d.getAudioBlankFrameFunc(PCMUSilenceFrame)(false) + case mime.MimeTypePCMA: + payload, err = d.getAudioBlankFrameFunc(PCMASilenceFrame)(false) + default: + d.params.Logger.Infow("unsupported mime type for silent frame on mute", "mimeType", d.Mime()) + return + } + if err != nil { + d.params.Logger.Warnw("could not get blank frame", err) + return + } + frameRate := uint32(50) frameDuration := time.Duration(1000/frameRate) * time.Millisecond numFrames := frameRate * uint32(maxPaddingOnMuteDuration/time.Second) @@ -2491,13 +2570,6 @@ func (d *DownTrack) sendSilentFrameOnMuteForOpus() { SSRC: d.ssrc, } d.addDummyExtensions(hdr) - - payload, err := d.getOpusBlankFrame(false) - if err != nil { - d.params.Logger.Warnw("could not get blank frame", err) - return - } - headerSize := hdr.MarshalSize() d.rtpStats.Update( mono.UnixNano(),