Skip to content

Commit ffe36f6

Browse files
committed
Refactor codec interface, allow using codecs without RTP.
1 parent 0362101 commit ffe36f6

File tree

12 files changed

+277
-143
lines changed

12 files changed

+277
-143
lines changed

audio.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2024 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package media
16+
17+
// AudioCodec is an audio codec implementation that can encode/decode bytes<->PCM.
18+
//
19+
// Each audio codec also implements AudioFrameCodec that can encode/decode bytes to a native Frame format of the codec.
20+
type AudioCodec interface {
21+
Codec
22+
// EncodeBytes creates a PCM->bytes encoder using the codec.
23+
EncodeBytes(w BytesWriter) PCM16Writer
24+
// DecodeBytes creates a bytes->PCM decoder using the codec.
25+
DecodeBytes(w PCM16Writer) BytesWriter
26+
}
27+
28+
// AudioFrameCodec is an audio codec implementation that can encode/decode bytes to/from a native Frame format of the codec.
29+
type AudioFrameCodec[S BytesFrame] interface {
30+
AudioCodec
31+
// Encode creates a Frame->bytes encoder using the codec.
32+
// The frame is in a native format of the codec.
33+
Encode(w WriteCloser[S]) PCM16Writer
34+
// Decode creates a bytes->Frame decoder using the codec.
35+
// The frame is in a native format of the codec.
36+
Decode(w PCM16Writer) WriteCloser[S]
37+
}
38+
39+
type AudioDecodeFunc[S Frame] func(w PCM16Writer) WriteCloser[S]
40+
type AudioEncodeFunc[S Frame] func(w WriteCloser[S]) PCM16Writer
41+
42+
// NewAudioCodec creates an audio codec with a given encode and decode implementations.
43+
func NewAudioCodec[S BytesFrame](
44+
info CodecInfo,
45+
decode AudioDecodeFunc[S],
46+
encode AudioEncodeFunc[S],
47+
) AudioFrameCodec[S] {
48+
if info.SampleRate <= 0 {
49+
panic("invalid sample rate")
50+
}
51+
if info.RTPClockRate == 0 {
52+
info.RTPClockRate = info.SampleRate
53+
}
54+
return &audioCodec[S]{
55+
info: info,
56+
encode: encode,
57+
decode: decode,
58+
}
59+
}
60+
61+
type audioCodec[S BytesFrame] struct {
62+
info CodecInfo
63+
decode AudioDecodeFunc[S]
64+
encode AudioEncodeFunc[S]
65+
}
66+
67+
func (c *audioCodec[S]) Info() CodecInfo {
68+
return c.info
69+
}
70+
71+
func (c *audioCodec[S]) Encode(w WriteCloser[S]) PCM16Writer {
72+
return c.encode(w)
73+
}
74+
75+
func (c *audioCodec[S]) Decode(w PCM16Writer) WriteCloser[S] {
76+
return c.decode(w)
77+
}
78+
79+
func (c *audioCodec[S]) EncodeBytes(w BytesWriter) PCM16Writer {
80+
bw := EncodeBytes[S](w, c.info.SampleRate)
81+
return c.encode(bw)
82+
}
83+
84+
func (c *audioCodec[S]) DecodeBytes(w PCM16Writer) BytesWriter {
85+
pw := c.decode(w)
86+
return DecodeBytes[S](pw)
87+
}

bytes.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2024 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package media
16+
17+
import (
18+
"bufio"
19+
"fmt"
20+
"io"
21+
)
22+
23+
type BytesFrame interface {
24+
~[]byte
25+
Frame
26+
}
27+
28+
// BytesWriter is similar to io.WriteCloser, but intentionally breaks API compatibility.
29+
//
30+
// This is done to emphasize that BytesWriter implementations are aware of the frame boundaries,
31+
// and will only use buffer sizes that match the frame size.
32+
type BytesWriter interface {
33+
String() string
34+
WriteRaw(frame []byte) error
35+
Close() error
36+
}
37+
38+
// NewBytesWriter creates a BytesWriter that writes to a standard io.WriteCloser.
39+
//
40+
// This process will erase the frame boundaries. Implement BytesWriter directly to preserve frame boundaries.
41+
func NewBytesWriter(w io.WriteCloser) BytesWriter {
42+
return &fileWriter{
43+
w: w,
44+
bw: bufio.NewWriter(w),
45+
}
46+
}
47+
48+
type fileWriter struct {
49+
w io.WriteCloser
50+
bw *bufio.Writer
51+
}
52+
53+
func (w *fileWriter) String() string {
54+
return "FileWriter"
55+
}
56+
57+
func (w *fileWriter) WriteRaw(data []byte) error {
58+
_, err := w.bw.Write(data)
59+
return err
60+
}
61+
62+
func (w *fileWriter) Close() error {
63+
if err := w.bw.Flush(); err != nil {
64+
_ = w.w.Close()
65+
return err
66+
}
67+
return w.w.Close()
68+
}
69+
70+
// NewFileWriter creates a new frame writer that encodes frame to a binary stream.
71+
//
72+
// This process will erase the frame boundaries. Use EncodeBytes to preserve frame boundaries.
73+
func NewFileWriter[T Frame](w io.WriteCloser, sampleRate int) WriteCloser[T] {
74+
bw := NewBytesWriter(w)
75+
return EncodeBytes[T](bw, sampleRate)
76+
}
77+
78+
// EncodeBytes creates a writer that converts every frame write to a single binary Write call on a standard io.WriteCloser.
79+
//
80+
// If preserving frame boundaries is not required, using NewFileWriter would be more efficient.
81+
func EncodeBytes[S Frame](w BytesWriter, sampleRate int) WriteCloser[S] {
82+
return &byteEncoder[S]{w: w, sampleRate: sampleRate}
83+
}
84+
85+
type byteEncoder[S Frame] struct {
86+
w BytesWriter
87+
sampleRate int
88+
buf []byte
89+
}
90+
91+
func (w *byteEncoder[S]) String() string {
92+
return fmt.Sprintf("ByteEncoder(%d) -> %s", w.sampleRate, w.w.String())
93+
}
94+
95+
func (w *byteEncoder[S]) SampleRate() int {
96+
return w.sampleRate
97+
}
98+
99+
func (w *byteEncoder[S]) WriteSample(sample S) error {
100+
if sz := sample.Size(); cap(w.buf) < sz {
101+
w.buf = make([]byte, sz)
102+
} else {
103+
w.buf = w.buf[:sz]
104+
}
105+
n, err := sample.CopyTo(w.buf)
106+
if err != nil {
107+
return err
108+
}
109+
return w.w.WriteRaw(w.buf[:n])
110+
}
111+
112+
func (w *byteEncoder[T]) Close() error {
113+
w.buf = nil
114+
return w.w.Close()
115+
}
116+
117+
// DecodeBytes creates a writer that converts every binary write from a standard io.WriteCloser to a frame write.
118+
//
119+
// Note that directly reading from a file is not possible in this case, as the frame boundaries are unknown.
120+
func DecodeBytes[S BytesFrame](w WriteCloser[S]) BytesWriter {
121+
return &byteDecoder[S]{w: w}
122+
}
123+
124+
type byteDecoder[S BytesFrame] struct {
125+
w WriteCloser[S]
126+
}
127+
128+
func (w *byteDecoder[S]) String() string {
129+
return fmt.Sprintf("ByteDecoder -> %s", w.w.String())
130+
}
131+
132+
func (w *byteDecoder[S]) SampleRate() int {
133+
return w.w.SampleRate()
134+
}
135+
136+
func (w *byteDecoder[S]) WriteRaw(sample []byte) error {
137+
return w.w.WriteSample(S(sample))
138+
}
139+
140+
func (w *byteDecoder[T]) Close() error {
141+
return w.w.Close()
142+
}

codecs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func EnabledCodecs() []Codec {
9191
return out
9292
}
9393

94+
// RegisterCodec registers the codec.
9495
func RegisterCodec(c Codec) {
9596
codecs = append(codecs, c)
9697
if info := c.Info(); info.Disabled {
@@ -101,6 +102,7 @@ func RegisterCodec(c Codec) {
101102
}
102103
}
103104

105+
// NewCodec creates a generic codec definition without a specific implementation.
104106
func NewCodec(info CodecInfo) Codec {
105107
if info.SampleRate <= 0 {
106108
panic("invalid sample rate")

g711/alaw.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ import (
2121
prtp "github.com/pion/rtp"
2222

2323
"github.com/livekit/media-sdk"
24-
"github.com/livekit/media-sdk/rtp"
2524
)
2625

2726
const ALawSDPName = "PCMA/8000"
2827

2928
func init() {
30-
media.RegisterCodec(rtp.NewAudioCodec(media.CodecInfo{
29+
media.RegisterCodec(media.NewAudioCodec(media.CodecInfo{
3130
SDPName: ALawSDPName,
3231
SampleRate: 8000,
3332
RTPDefType: prtp.PayloadTypePCMA,

g711/g711_test.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,6 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14-
// Copyright 2024 LiveKit, Inc.
15-
//
16-
// Licensed under the Apache License, Version 2.0 (the "License");
17-
// you may not use this file except in compliance with the License.
18-
// You may obtain a copy of the License at
19-
//
20-
// http://www.apache.org/licenses/LICENSE-2.0
21-
//
22-
// Unless required by applicable law or agreed to in writing, software
23-
// distributed under the License is distributed on an "AS IS" BASIS,
24-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25-
// See the License for the specific language governing permissions and
26-
// limitations under the License.
2714

2815
package g711
2916

g711/ulaw.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ import (
2121
prtp "github.com/pion/rtp"
2222

2323
"github.com/livekit/media-sdk"
24-
"github.com/livekit/media-sdk/rtp"
2524
)
2625

2726
const ULawSDPName = "PCMU/8000"
2827

2928
func init() {
30-
media.RegisterCodec(rtp.NewAudioCodec(media.CodecInfo{
29+
media.RegisterCodec(media.NewAudioCodec(media.CodecInfo{
3130
SDPName: ULawSDPName,
3231
SampleRate: 8000,
3332
RTPDefType: prtp.PayloadTypePCMU,

g722/g722.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
prtp "github.com/pion/rtp"
2525

2626
"github.com/livekit/media-sdk"
27-
"github.com/livekit/media-sdk/rtp"
2827
)
2928

3029
const SDPName = "G722/8000"
@@ -35,7 +34,7 @@ var (
3534
)
3635

3736
func init() {
38-
media.RegisterCodec(rtp.NewAudioCodec(media.CodecInfo{
37+
media.RegisterCodec(media.NewAudioCodec(media.CodecInfo{
3938
SDPName: SDPName,
4039
SampleRate: 16000,
4140
RTPClockRate: 8000,

media.go

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
package media
1616

1717
import (
18-
"bufio"
1918
"fmt"
20-
"io"
2119
"strings"
2220
"sync/atomic"
2321
"time"
@@ -209,51 +207,3 @@ func (s MultiWriter[T]) Close() error {
209207
}
210208
return last
211209
}
212-
213-
func NewFileWriter[T Frame](w io.WriteCloser, sampleRate int) WriteCloser[T] {
214-
return &fileWriter[T]{
215-
w: w,
216-
bw: bufio.NewWriter(w),
217-
sampleRate: sampleRate,
218-
}
219-
}
220-
221-
type fileWriter[T Frame] struct {
222-
w io.WriteCloser
223-
bw *bufio.Writer
224-
sampleRate int
225-
buf []byte
226-
}
227-
228-
func (w *fileWriter[T]) String() string {
229-
return fmt.Sprintf("RawFile(%d)", w.sampleRate)
230-
}
231-
232-
func (w *fileWriter[T]) SampleRate() int {
233-
return w.sampleRate
234-
}
235-
236-
func (w *fileWriter[T]) WriteSample(sample T) error {
237-
if sz := sample.Size(); cap(w.buf) < sz {
238-
w.buf = make([]byte, sz)
239-
} else {
240-
w.buf = w.buf[:sz]
241-
}
242-
n, err := sample.CopyTo(w.buf)
243-
if err != nil {
244-
return err
245-
}
246-
_, err = w.bw.Write(w.buf[:n])
247-
return err
248-
}
249-
250-
func (w *fileWriter[T]) Close() error {
251-
if err := w.bw.Flush(); err != nil {
252-
_ = w.w.Close()
253-
return err
254-
}
255-
if err := w.w.Close(); err != nil {
256-
return err
257-
}
258-
return nil
259-
}

0 commit comments

Comments
 (0)