mirror of
https://github.com/harshabose/client.git
synced 2025-09-26 19:31:20 +08:00
Initial clean commit
This commit is contained in:
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool
|
||||
*.out
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
third_party/
|
||||
|
||||
# Environment files
|
||||
*.env
|
||||
!.env.example
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Windows
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Linux
|
||||
*~
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
177
bwestimator.go
Normal file
177
bwestimator.go
Normal file
@@ -0,0 +1,177 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/interceptor/pkg/cc"
|
||||
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasource"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/transcode"
|
||||
)
|
||||
|
||||
type subscriber struct {
|
||||
id string // unique identifier
|
||||
priority mediasource.Priority
|
||||
callback transcode.UpdateBitrateCallBack
|
||||
}
|
||||
|
||||
type BWEController struct {
|
||||
estimator cc.BandwidthEstimator
|
||||
interval time.Duration
|
||||
subs []subscriber
|
||||
mux sync.RWMutex
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func createBWController(ctx context.Context) *BWEController {
|
||||
return &BWEController{
|
||||
subs: make([]subscriber, 0),
|
||||
estimator: nil,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (bwc *BWEController) Start() {
|
||||
go bwc.loop()
|
||||
}
|
||||
|
||||
func (bwc *BWEController) Subscribe(id string, priority mediasource.Priority, callback transcode.UpdateBitrateCallBack) error {
|
||||
bwc.mux.Lock()
|
||||
defer bwc.mux.Unlock()
|
||||
|
||||
for _, sub := range bwc.subs {
|
||||
if sub.id == id {
|
||||
return errors.New("subscriber already exists")
|
||||
}
|
||||
}
|
||||
|
||||
bwc.subs = append(bwc.subs, subscriber{
|
||||
id: id,
|
||||
priority: priority,
|
||||
callback: callback,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSubscribers returns a copy of subscribers for safe iteration
|
||||
func (bwc *BWEController) getSubscribers() []subscriber {
|
||||
bwc.mux.RLock()
|
||||
defer bwc.mux.RUnlock()
|
||||
|
||||
// Return a copy to avoid holding the lock during iteration
|
||||
subs := make([]subscriber, len(bwc.subs))
|
||||
copy(subs, bwc.subs)
|
||||
return subs
|
||||
}
|
||||
|
||||
// calculateTotalPriority calculates sum of all subscriber priorities
|
||||
func (bwc *BWEController) calculateTotalPriority(subs []subscriber) mediasource.Priority {
|
||||
var totalPriority = mediasource.Level0
|
||||
|
||||
for _, sub := range subs {
|
||||
totalPriority += sub.priority
|
||||
}
|
||||
|
||||
return totalPriority
|
||||
}
|
||||
|
||||
func (bwc *BWEController) loop() {
|
||||
ticker := time.NewTicker(bwc.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-bwc.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if bwc.estimator == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
subs := bwc.getSubscribers()
|
||||
if len(subs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
totalPriority := bwc.calculateTotalPriority(subs)
|
||||
if totalPriority == mediasource.Level0 {
|
||||
continue // No active priorities
|
||||
}
|
||||
|
||||
totalBitrate, err := bwc.getBitrate()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process each subscriber
|
||||
for _, sub := range subs {
|
||||
if sub.priority == mediasource.Level0 {
|
||||
continue
|
||||
}
|
||||
|
||||
bitrate := int64(float64(totalBitrate) * float64(sub.priority) / float64(totalPriority))
|
||||
go bwc.sendBitrateUpdate(len(subs), sub.callback, bitrate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bwc *BWEController) sendBitrateUpdate(n int, callback transcode.UpdateBitrateCallBack, bitrate int64) {
|
||||
timeout := bwc.interval
|
||||
if n > 0 {
|
||||
timeout = bwc.interval / time.Duration(n)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(bwc.ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
done <- callback(bitrate)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
fmt.Printf("bitrate update callback failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
fmt.Println("bitrate update callback timed out")
|
||||
return
|
||||
}
|
||||
fmt.Println("bitrate update callback cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
func (bwc *BWEController) getBitrate() (int, error) {
|
||||
if bwc.estimator == nil {
|
||||
return 0, errors.New("estimator is nil")
|
||||
}
|
||||
return bwc.estimator.GetTargetBitrate(), nil
|
||||
}
|
||||
|
||||
func (bwc *BWEController) Unsubscribe(id string) error {
|
||||
bwc.mux.Lock()
|
||||
defer bwc.mux.Unlock()
|
||||
|
||||
for i, sub := range bwc.subs {
|
||||
if sub.id == id {
|
||||
// Remove the subscriber by swapping with the last element
|
||||
bwc.subs[i] = bwc.subs[len(bwc.subs)-1]
|
||||
bwc.subs = bwc.subs[:len(bwc.subs)-1]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("subscriber not found")
|
||||
}
|
149
client.go
Normal file
149
client.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/interceptor/pkg/cc"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/harshabose/tools/pkg/multierr"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
peerConnections map[string]*PeerConnection
|
||||
mediaEngine *webrtc.MediaEngine
|
||||
settingsEngine *webrtc.SettingEngine
|
||||
interceptorRegistry *interceptor.Registry
|
||||
estimatorChan chan cc.BandwidthEstimator
|
||||
api *webrtc.API
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewClient(
|
||||
ctx context.Context, cancel context.CancelFunc,
|
||||
mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry,
|
||||
settings *webrtc.SettingEngine, options ...ClientOption,
|
||||
) (*Client, error) {
|
||||
if mediaEngine == nil {
|
||||
mediaEngine = &webrtc.MediaEngine{}
|
||||
}
|
||||
if interceptorRegistry == nil {
|
||||
interceptorRegistry = &interceptor.Registry{}
|
||||
}
|
||||
if settings == nil {
|
||||
settings = &webrtc.SettingEngine{}
|
||||
}
|
||||
|
||||
settings.DetachDataChannels()
|
||||
|
||||
peerConnections := &Client{
|
||||
mediaEngine: mediaEngine,
|
||||
interceptorRegistry: interceptorRegistry,
|
||||
settingsEngine: settings,
|
||||
peerConnections: make(map[string]*PeerConnection),
|
||||
estimatorChan: make(chan cc.BandwidthEstimator, 10),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(peerConnections); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
peerConnections.api = webrtc.NewAPI(webrtc.WithMediaEngine(peerConnections.mediaEngine), webrtc.WithInterceptorRegistry(peerConnections.interceptorRegistry), webrtc.WithSettingEngine(*peerConnections.settingsEngine))
|
||||
|
||||
return peerConnections, nil
|
||||
}
|
||||
|
||||
func NewClientFromConfig(
|
||||
ctx context.Context, cancel context.CancelFunc,
|
||||
mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry,
|
||||
settings *webrtc.SettingEngine, config *ClientConfig,
|
||||
) (*Client, error) {
|
||||
return NewClient(
|
||||
ctx, cancel, mediaEngine, interceptorRegistry, settings,
|
||||
config.ToOptions()..., // The magic spread operator!
|
||||
)
|
||||
}
|
||||
|
||||
func (client *Client) CreatePeerConnection(label string, config webrtc.Configuration, options ...PeerConnectionOption) (*PeerConnection, error) {
|
||||
var err error
|
||||
|
||||
if _, exists := client.peerConnections[label]; exists {
|
||||
return nil, errors.New("peer connection already exists")
|
||||
}
|
||||
|
||||
// TODO: CHANGE THE SIGNATURE; SENDING A CANCEL FUNC IS IDIOTIC
|
||||
if client.peerConnections[label], err = CreatePeerConnection(client.ctx, client.cancel, label, client.api, config, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.peerConnections[label], nil
|
||||
}
|
||||
|
||||
func (client *Client) CreatePeerConnectionFromConfig(config PeerConnectionConfig) (*PeerConnection, error) {
|
||||
pc, err := client.CreatePeerConnection(config.Name, config.RTCConfig, config.ToOptions()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.CreateDataChannels(pc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.CreateMediaSources(pc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := config.CreateMediaSinks(pc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetPeerConnection(label string) (*PeerConnection, error) {
|
||||
if _, exists := client.peerConnections[label]; !exists {
|
||||
return nil, errors.New("peer connection not found")
|
||||
}
|
||||
return client.peerConnections[label], nil
|
||||
}
|
||||
|
||||
func (client *Client) WaitUntilClosed() {
|
||||
<-client.ctx.Done()
|
||||
}
|
||||
|
||||
func (client *Client) Connect(category string) error {
|
||||
var merr error
|
||||
for _, pc := range client.peerConnections {
|
||||
if err := pc.Connect(category); err != nil {
|
||||
merr = multierr.Append(merr, err)
|
||||
}
|
||||
}
|
||||
|
||||
return merr
|
||||
}
|
||||
|
||||
func (client *Client) ClosePeerConnection(label string) error {
|
||||
pc, err := client.GetPeerConnection(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pc.Close()
|
||||
}
|
||||
|
||||
func (client *Client) Close() error {
|
||||
for _, peerConnection := range client.peerConnections {
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
35
client_cgo.go
Normal file
35
client_cgo.go
Normal file
@@ -0,0 +1,35 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func (client *Client) CreatePeerConnectionWithBWEstimator(label string, config webrtc.Configuration, options ...PeerConnectionOption) (*PeerConnection, error) {
|
||||
var err error
|
||||
|
||||
if _, exists := client.peerConnections[label]; exists {
|
||||
return nil, errors.New("peer connection already exists")
|
||||
}
|
||||
|
||||
// TODO: CHANGE THE SIGNATURE; SENDING A CANCEL FUNC IS IDIOTIC
|
||||
if client.peerConnections[label], err = CreatePeerConnection(client.ctx, client.cancel, label, client.api, config, options...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: THIS WEIRD CHANNEL BASED APPROACH OF SETTING BW CONTROLLER IS REQUIRED BECAUSE OF THE
|
||||
// TODO: THE WEIRD DESIGN OF CC INTERCEPTOR IN PION. TRACK THE ISSUE WITH "https://github.com/pion/webrtc/issues/3053"
|
||||
if client.peerConnections[label].bwController != nil {
|
||||
select {
|
||||
case estimator := <-client.estimatorChan:
|
||||
client.peerConnections[label].bwController.estimator = estimator
|
||||
client.peerConnections[label].bwController.interval = 50 * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
return client.peerConnections[label], nil
|
||||
}
|
52
client_constants.go
Normal file
52
client_constants.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/interceptor/pkg/nack"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
H264PayloadType webrtc.PayloadType = 102
|
||||
H264RTXPayloadType webrtc.PayloadType = 103
|
||||
VP8PayloadType webrtc.PayloadType = 96
|
||||
VP8RTXPayloadType webrtc.PayloadType = 97
|
||||
OpusPayloadType webrtc.PayloadType = 111
|
||||
)
|
||||
|
||||
type NACKGeneratorOptions []nack.GeneratorOption
|
||||
|
||||
var (
|
||||
NACKGeneratorLowLatency NACKGeneratorOptions = []nack.GeneratorOption{nack.GeneratorSize(256), nack.GeneratorSkipLastN(2), nack.GeneratorMaxNacksPerPacket(1), nack.GeneratorInterval(10 * time.Millisecond)}
|
||||
NACKGeneratorDefault NACKGeneratorOptions = []nack.GeneratorOption{nack.GeneratorSize(512), nack.GeneratorSkipLastN(5), nack.GeneratorMaxNacksPerPacket(2), nack.GeneratorInterval(50 * time.Millisecond)}
|
||||
NACKGeneratorHighQuality NACKGeneratorOptions = []nack.GeneratorOption{nack.GeneratorSize(4096), nack.GeneratorSkipLastN(10), nack.GeneratorMaxNacksPerPacket(5), nack.GeneratorInterval(100 * time.Millisecond)}
|
||||
NACKGeneratorLowBandwidth NACKGeneratorOptions = []nack.GeneratorOption{nack.GeneratorSize(256), nack.GeneratorSkipLastN(15), nack.GeneratorMaxNacksPerPacket(1), nack.GeneratorInterval(200 * time.Millisecond)}
|
||||
)
|
||||
|
||||
type NACKResponderOptions []nack.ResponderOption
|
||||
|
||||
var (
|
||||
NACKResponderLowLatency NACKResponderOptions = []nack.ResponderOption{nack.ResponderSize(256)}
|
||||
NACKResponderDefault NACKResponderOptions = []nack.ResponderOption{nack.ResponderSize(1024)}
|
||||
NACKResponderHighQuality NACKResponderOptions = []nack.ResponderOption{nack.ResponderSize(4096)}
|
||||
NACKResponderLowBandwidth NACKResponderOptions = []nack.ResponderOption{nack.ResponderSize(256)}
|
||||
)
|
||||
|
||||
type TWCCSenderInterval time.Duration
|
||||
|
||||
const (
|
||||
TWCCIntervalLowLatency = TWCCSenderInterval(100 * time.Millisecond)
|
||||
TWCCIntervalDefault = TWCCSenderInterval(200 * time.Millisecond)
|
||||
TWCCIntervalHighQuality = TWCCSenderInterval(300 * time.Millisecond)
|
||||
TWCCIntervalLowBandwidth = TWCCSenderInterval(500 * time.Millisecond)
|
||||
)
|
||||
|
||||
type RTCPReportInterval time.Duration
|
||||
|
||||
const (
|
||||
RTCPReportIntervalLowLatency = RTCPReportInterval(1 * time.Second)
|
||||
RTCPReportIntervalDefault = RTCPReportInterval(3 * time.Second)
|
||||
RTCPReportIntervalHighQuality = RTCPReportInterval(2 * time.Second)
|
||||
RTCPReportIntervalLowBandwidth = RTCPReportInterval(10 * time.Second)
|
||||
)
|
248
client_options.go
Normal file
248
client_options.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pion/interceptor/pkg/cc"
|
||||
"github.com/pion/interceptor/pkg/flexfec"
|
||||
"github.com/pion/interceptor/pkg/gcc"
|
||||
"github.com/pion/interceptor/pkg/jitterbuffer"
|
||||
"github.com/pion/interceptor/pkg/nack"
|
||||
"github.com/pion/interceptor/pkg/report"
|
||||
"github.com/pion/interceptor/pkg/twcc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasource"
|
||||
)
|
||||
|
||||
type ClientOption = func(*Client) error
|
||||
|
||||
func WithH264MediaEngine(clockrate uint32, packetisationMode mediasource.PacketisationMode, profileLevelID mediasource.ProfileLevel, sps, pps string) ClientOption {
|
||||
return func(client *Client) error {
|
||||
RTCPFeedback := []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBGoogREMB}, {Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"}, {Type: webrtc.TypeRTCPFBNACK}, {Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"}}
|
||||
if err := client.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: clockrate,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: fmt.Sprintf("level-asymmetry-allowed=1;packetization-mode=%d;profile-level-id=%s;sprop-parameter-sets=%s,%s", packetisationMode, profileLevelID, sps, pps),
|
||||
RTCPFeedback: RTCPFeedback,
|
||||
},
|
||||
PayloadType: H264PayloadType,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeRTX,
|
||||
ClockRate: clockrate,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: fmt.Sprintf("apt=%d", H264PayloadType),
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: H264RTXPayloadType,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVP8MediaEngine(clockrate uint32) ClientOption {
|
||||
return func(client *Client) error {
|
||||
RTCPFeedback := []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBGoogREMB}, {Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"}, {Type: webrtc.TypeRTCPFBNACK}, {Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"}}
|
||||
if err := client.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeVP8,
|
||||
ClockRate: clockrate,
|
||||
RTCPFeedback: RTCPFeedback,
|
||||
SDPFmtpLine: fmt.Sprintf(""),
|
||||
},
|
||||
PayloadType: VP8PayloadType,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeRTX,
|
||||
ClockRate: clockrate,
|
||||
RTCPFeedback: nil,
|
||||
SDPFmtpLine: fmt.Sprintf("apt=%d", VP8PayloadType),
|
||||
},
|
||||
PayloadType: VP8RTXPayloadType,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaultMediaEngine() ClientOption {
|
||||
return func(client *Client) error {
|
||||
if err := client.mediaEngine.RegisterDefaultCodecs(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaultInterceptorRegistry() ClientOption {
|
||||
return func(client *Client) error {
|
||||
if err := webrtc.RegisterDefaultInterceptors(client.mediaEngine, client.interceptorRegistry); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpusMediaEngine(samplerate uint32, channelLayout uint16, stereo mediasource.StereoType) ClientOption {
|
||||
return func(client *Client) error {
|
||||
if err := client.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeOpus,
|
||||
ClockRate: samplerate,
|
||||
Channels: channelLayout,
|
||||
RTCPFeedback: nil,
|
||||
SDPFmtpLine: fmt.Sprintf("minptime=10;useinbandfec=1;stereo=%d", stereo),
|
||||
},
|
||||
PayloadType: OpusPayloadType,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithNACKInterceptor(generatorOptions NACKGeneratorOptions, responderOptions NACKResponderOptions) ClientOption {
|
||||
return func(client *Client) error {
|
||||
generator, err := nack.NewGeneratorInterceptor(generatorOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responder, err := nack.NewResponderInterceptor(responderOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBNACK}, webrtc.RTPCodecTypeVideo)
|
||||
client.mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"}, webrtc.RTPCodecTypeVideo)
|
||||
client.interceptorRegistry.Add(responder)
|
||||
client.interceptorRegistry.Add(generator)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTWCCSenderInterceptor(interval TWCCSenderInterval) ClientOption {
|
||||
return func(client *Client) error {
|
||||
client.mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC}, webrtc.RTPCodecTypeVideo)
|
||||
if err := client.mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC}, webrtc.RTPCodecTypeAudio)
|
||||
if err := client.mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generator, err := twcc.NewSenderInterceptor(twcc.SendInterval(time.Duration(interval)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.interceptorRegistry.Add(generator)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WARN: DO NOT USE THIS, PION HAS SOME ISSUE WITH THIS WHICH MAKES THE ONTRACK CALLBACK NOT FIRE
|
||||
|
||||
func WithJitterBufferInterceptor() ClientOption {
|
||||
return func(client *Client) error {
|
||||
var (
|
||||
jitterBuffer *jitterbuffer.InterceptorFactory
|
||||
err error
|
||||
)
|
||||
|
||||
if jitterBuffer, err = jitterbuffer.NewInterceptor(); err != nil {
|
||||
return err
|
||||
}
|
||||
client.interceptorRegistry.Add(jitterBuffer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRTCPReportsInterceptor(interval RTCPReportInterval) ClientOption {
|
||||
return func(client *Client) error {
|
||||
receiver, err := report.NewReceiverInterceptor(report.ReceiverInterval(time.Duration(interval)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sender, err := report.NewSenderInterceptor(report.SenderInterval(time.Duration(interval)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.interceptorRegistry.Add(receiver)
|
||||
client.interceptorRegistry.Add(sender)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WARN: DO NOT USE FLEXFEC YET, AS THE FECOPTION ARE NOT YET IMPLEMENTED
|
||||
|
||||
func WithFLEXFECInterceptor() ClientOption {
|
||||
return func(client *Client) error {
|
||||
var (
|
||||
fecInterceptor *flexfec.FecInterceptorFactory
|
||||
err error
|
||||
)
|
||||
|
||||
// NOTE: Pion's FLEXFEC does not implement FecOption yet, if needed, someone needs to contribute to the repo
|
||||
if fecInterceptor, err = flexfec.NewFecInterceptor(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.interceptorRegistry.Add(fecInterceptor)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithSimulcastExtensionHeaders() ClientOption {
|
||||
return func(client *Client) error {
|
||||
return webrtc.ConfigureSimulcastExtensionHeaders(client.mediaEngine)
|
||||
}
|
||||
}
|
||||
|
||||
func WithBandwidthControlInterceptor(initialBitrate, minimumBitrate, maximumBitrate uint64, interval time.Duration) ClientOption {
|
||||
return func(client *Client) error {
|
||||
congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) {
|
||||
return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(int(initialBitrate)), gcc.SendSideBWEMinBitrate(int(minimumBitrate)), gcc.SendSideBWEMaxBitrate(int(maximumBitrate)))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) {
|
||||
client.estimatorChan <- estimator
|
||||
})
|
||||
|
||||
client.interceptorRegistry.Add(congestionController)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithTWCCHeaderExtensionSender() ClientOption {
|
||||
return func(client *Client) error {
|
||||
return webrtc.ConfigureTWCCHeaderExtensionSender(client.mediaEngine, client.interceptorRegistry)
|
||||
}
|
||||
}
|
469
config.go
Normal file
469
config.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/datachannel"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasink"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasource"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
Name string
|
||||
// Media configuration
|
||||
H264 *H264Config `json:"h264,omitempty"`
|
||||
VP8 *VP8Config `json:"vp8,omitempty"`
|
||||
Opus *OpusConfig `json:"opus,omitempty"`
|
||||
|
||||
// Interceptor configurations
|
||||
NACK *NACKPreset `json:"nack,omitempty"`
|
||||
RTCPReports *RTCPReportsPreset `json:"rtcp_reports,omitempty"`
|
||||
TWCC *TWCCPreset `json:"twcc,omitempty"`
|
||||
Bandwidth *BandwidthConfig `json:"bandwidth,omitempty"`
|
||||
|
||||
// Feature flags
|
||||
SimulcastExtensions bool `json:"simulcast_extensions,omitempty"`
|
||||
TWCCHeaderExtension bool `json:"twcc_header_extension,omitempty"`
|
||||
}
|
||||
|
||||
type H264Config struct {
|
||||
ClockRate uint32 `json:"clock_rate"`
|
||||
PacketisationMode mediasource.PacketisationMode `json:"packetisation_mode"`
|
||||
ProfileLevel mediasource.ProfileLevel `json:"profile_level"`
|
||||
SPSBase64 string `json:"sps_base64"`
|
||||
PPSBase64 string `json:"pps_base64"`
|
||||
}
|
||||
|
||||
type VP8Config struct {
|
||||
ClockRate uint32 `json:"clock_rate"`
|
||||
}
|
||||
|
||||
type OpusConfig struct {
|
||||
SampleRate uint32 `json:"sample_rate"`
|
||||
ChannelLayout uint16 `json:"channel_layout"`
|
||||
Stereo mediasource.StereoType `json:"stereo"`
|
||||
}
|
||||
|
||||
type NACKPreset string
|
||||
type RTCPReportsPreset string
|
||||
type TWCCPreset string
|
||||
|
||||
const (
|
||||
NACKLowLatency NACKPreset = "low_latency"
|
||||
NACKDefault NACKPreset = "default"
|
||||
NACKHighQuality NACKPreset = "high_quality"
|
||||
NACKLowBandwidth NACKPreset = "low_bandwidth"
|
||||
|
||||
RTCPReportsLowLatency RTCPReportsPreset = "low_latency"
|
||||
RTCPReportsDefault RTCPReportsPreset = "default"
|
||||
RTCPReportsHighQuality RTCPReportsPreset = "high_quality"
|
||||
RTCPReportsLowBandwidth RTCPReportsPreset = "low_bandwidth"
|
||||
|
||||
TWCCLowLatency TWCCPreset = "low_latency"
|
||||
TWCCDefault TWCCPreset = "default"
|
||||
TWCCHighQuality TWCCPreset = "high_quality"
|
||||
TWCCLowBandwidth TWCCPreset = "low_bandwidth"
|
||||
)
|
||||
|
||||
type BandwidthConfig struct {
|
||||
Initial uint64 `json:"initial"`
|
||||
Minimum uint64 `json:"minimum"`
|
||||
Maximum uint64 `json:"maximum"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
}
|
||||
|
||||
type optionBuilder struct {
|
||||
options []ClientOption
|
||||
}
|
||||
|
||||
func (ob *optionBuilder) add(option ClientOption) *optionBuilder {
|
||||
if option != nil {
|
||||
ob.options = append(ob.options, option)
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
var (
|
||||
nackGeneratorPresets = map[NACKPreset]NACKGeneratorOptions{
|
||||
NACKLowLatency: NACKGeneratorLowLatency,
|
||||
NACKDefault: NACKGeneratorDefault,
|
||||
NACKHighQuality: NACKGeneratorHighQuality,
|
||||
NACKLowBandwidth: NACKGeneratorLowBandwidth,
|
||||
}
|
||||
|
||||
nackResponderPresets = map[NACKPreset]NACKResponderOptions{
|
||||
NACKLowLatency: NACKResponderLowLatency,
|
||||
NACKDefault: NACKResponderDefault,
|
||||
NACKHighQuality: NACKResponderHighQuality,
|
||||
NACKLowBandwidth: NACKResponderLowBandwidth,
|
||||
}
|
||||
|
||||
rtcpReportsPresets = map[RTCPReportsPreset]RTCPReportInterval{
|
||||
RTCPReportsLowLatency: RTCPReportIntervalLowLatency,
|
||||
RTCPReportsDefault: RTCPReportIntervalDefault,
|
||||
RTCPReportsHighQuality: RTCPReportIntervalHighQuality,
|
||||
RTCPReportsLowBandwidth: RTCPReportIntervalLowBandwidth,
|
||||
}
|
||||
|
||||
twccPresets = map[TWCCPreset]TWCCSenderInterval{
|
||||
TWCCLowLatency: TWCCIntervalLowLatency,
|
||||
TWCCDefault: TWCCIntervalDefault,
|
||||
TWCCHighQuality: TWCCIntervalHighQuality,
|
||||
TWCCLowBandwidth: TWCCIntervalLowBandwidth,
|
||||
}
|
||||
)
|
||||
|
||||
func (c *ClientConfig) ToOptions() []ClientOption {
|
||||
builder := &optionBuilder{}
|
||||
|
||||
return builder.
|
||||
add(c.h264Option()).
|
||||
add(c.vp8Option()).
|
||||
add(c.opusOption()).
|
||||
add(c.nackOption()).
|
||||
add(c.rtcpReportsOption()).
|
||||
add(c.twccOption()).
|
||||
add(c.bandwidthOption()).
|
||||
add(c.simulcastOption()).
|
||||
add(c.twccHeaderOption()).
|
||||
options
|
||||
}
|
||||
|
||||
func (c *ClientConfig) h264Option() ClientOption {
|
||||
if c.H264 == nil {
|
||||
return nil
|
||||
}
|
||||
return WithH264MediaEngine(
|
||||
c.H264.ClockRate,
|
||||
c.H264.PacketisationMode,
|
||||
c.H264.ProfileLevel,
|
||||
c.H264.SPSBase64,
|
||||
c.H264.PPSBase64,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) vp8Option() ClientOption {
|
||||
if c.VP8 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WithVP8MediaEngine(c.VP8.ClockRate)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) opusOption() ClientOption {
|
||||
if c.Opus == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WithOpusMediaEngine(
|
||||
c.Opus.SampleRate,
|
||||
c.Opus.ChannelLayout,
|
||||
c.Opus.Stereo,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) nackOption() ClientOption {
|
||||
if c.NACK == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
generator, generatorExists := nackGeneratorPresets[*c.NACK]
|
||||
responder, responderExists := nackResponderPresets[*c.NACK]
|
||||
|
||||
if !generatorExists || !responderExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WithNACKInterceptor(generator, responder)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) rtcpReportsOption() ClientOption {
|
||||
if c.RTCPReports == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
interval, exists := rtcpReportsPresets[*c.RTCPReports]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WithRTCPReportsInterceptor(interval)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) twccOption() ClientOption {
|
||||
if c.TWCC == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
interval, exists := twccPresets[*c.TWCC]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WithTWCCSenderInterceptor(interval)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) bandwidthOption() ClientOption {
|
||||
if c.Bandwidth == nil {
|
||||
return nil
|
||||
}
|
||||
return WithBandwidthControlInterceptor(
|
||||
c.Bandwidth.Initial,
|
||||
c.Bandwidth.Minimum,
|
||||
c.Bandwidth.Maximum,
|
||||
c.Bandwidth.Interval,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ClientConfig) simulcastOption() ClientOption {
|
||||
if !c.SimulcastExtensions {
|
||||
return nil
|
||||
}
|
||||
return WithSimulcastExtensionHeaders()
|
||||
}
|
||||
|
||||
func (c *ClientConfig) twccHeaderOption() ClientOption {
|
||||
if !c.TWCCHeaderExtension {
|
||||
return nil
|
||||
}
|
||||
return WithTWCCHeaderExtensionSender()
|
||||
}
|
||||
|
||||
type PeerConnectionConfig struct {
|
||||
// Basic settings
|
||||
Name string `json:"name"`
|
||||
|
||||
// RTC and Singnaling Control
|
||||
FirebaseOfferSignal *bool `json:"firebase_offer_signal,omitempty"`
|
||||
FirebaseOfferAnswer *bool `json:"firebase_offer_answer"`
|
||||
RTCConfig webrtc.Configuration `json:"rtc_config"`
|
||||
|
||||
// Declarative resource definitions
|
||||
DataChannels []DataChannelSpec `json:"data_channels_specs,omitempty"`
|
||||
MediaSources []MediaSourceSpec `json:"media_sources_specs,omitempty"`
|
||||
MediaSinks []MediaSinkSpec `json:"media_sinks_specs,omitempty"`
|
||||
}
|
||||
|
||||
type DataChannelSpec struct {
|
||||
Label string `json:"label"`
|
||||
ID *uint16 `json:"id,omitempty"`
|
||||
Ordered *bool `json:"ordered,omitempty"`
|
||||
Protocol *string `json:"protocol,omitempty"`
|
||||
Negotiated *bool `json:"negotiated,omitempty"`
|
||||
MaxPacketLifeTime *uint16 `json:"max_packet_life_time,omitempty"`
|
||||
MaxRetransmits *uint16 `json:"max_retransmits,omitempty"`
|
||||
}
|
||||
|
||||
type MediaSourceSpec struct {
|
||||
Name string `json:"name"`
|
||||
H264 *H264TrackConfig `json:"h264,omitempty"`
|
||||
VP8 *VP8TrackConfig `json:"vp8,omitempty"`
|
||||
Opus *OpusTrackConfig `json:"opus,omitempty"`
|
||||
Priority *mediasource.Priority `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
type trackOptionBuilder struct {
|
||||
options []mediasource.TrackOption
|
||||
}
|
||||
|
||||
func (ob *trackOptionBuilder) add(option mediasource.TrackOption) *trackOptionBuilder {
|
||||
if option != nil {
|
||||
ob.options = append(ob.options, option)
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
func (c *MediaSourceSpec) withTrackOption() mediasource.TrackOption {
|
||||
if c.H264 != nil {
|
||||
return mediasource.WithH264Track(c.H264.ClockRate, c.H264.PacketisationMode, c.H264.ProfileLevel)
|
||||
}
|
||||
|
||||
if c.VP8 != nil {
|
||||
return mediasource.WithVP8Track(c.VP8.ClockRate)
|
||||
}
|
||||
|
||||
if c.Opus != nil {
|
||||
return mediasource.WithOpusTrack(c.Opus.Samplerate, c.Opus.ChannelLayout, c.Opus.Stereo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MediaSourceSpec) ToOptions() []mediasource.TrackOption {
|
||||
builder := trackOptionBuilder{}
|
||||
|
||||
return builder.add(c.withTrackOption()).add(c.withPriority()).options
|
||||
}
|
||||
|
||||
func (c *MediaSourceSpec) withPriority() mediasource.TrackOption {
|
||||
if c.Priority == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return mediasource.WithPriority(*c.Priority)
|
||||
}
|
||||
|
||||
// MediaSinkSpec defines a media sink to create
|
||||
type MediaSinkSpec struct {
|
||||
Name string `json:"name"`
|
||||
H264 *H264TrackConfig `json:"h264,omitempty"`
|
||||
VP8 *VP8TrackConfig `json:"vp8,omitempty"`
|
||||
Opus *OpusTrackConfig `json:"opus,omitempty"`
|
||||
}
|
||||
|
||||
type sinkOptionBuilder struct {
|
||||
options []mediasink.SinkOption
|
||||
}
|
||||
|
||||
func (ob *sinkOptionBuilder) add(option mediasink.SinkOption) *sinkOptionBuilder {
|
||||
if option != nil {
|
||||
ob.options = append(ob.options, option)
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
func (c *MediaSinkSpec) withTrackOption() mediasink.SinkOption {
|
||||
if c.H264 != nil {
|
||||
return mediasink.WithH264Track(c.H264.ClockRate)
|
||||
}
|
||||
|
||||
if c.VP8 != nil {
|
||||
return mediasink.WithVP8Track(c.VP8.ClockRate)
|
||||
}
|
||||
|
||||
if c.Opus != nil {
|
||||
return mediasink.WithOpusTrack(c.Opus.Samplerate, c.Opus.ChannelLayout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MediaSinkSpec) ToOptions() []mediasink.SinkOption {
|
||||
builder := sinkOptionBuilder{}
|
||||
|
||||
return builder.add(c.withTrackOption()).options
|
||||
}
|
||||
|
||||
type H264TrackConfig struct {
|
||||
ClockRate uint32 `json:"clock_rate"`
|
||||
PacketisationMode mediasource.PacketisationMode `json:"packetisation_mode"`
|
||||
ProfileLevel mediasource.ProfileLevel `json:"profile_level"`
|
||||
}
|
||||
|
||||
type VP8TrackConfig struct {
|
||||
ClockRate uint32 `json:"clock_rate"`
|
||||
}
|
||||
|
||||
type OpusTrackConfig struct {
|
||||
Samplerate uint32 `json:"samplerate"`
|
||||
ChannelLayout uint16 `json:"channel_layout"`
|
||||
Stereo mediasource.StereoType `json:"stereo"`
|
||||
}
|
||||
|
||||
type pcOptionBuilder struct {
|
||||
options []PeerConnectionOption
|
||||
}
|
||||
|
||||
func (ob *pcOptionBuilder) add(option PeerConnectionOption) *pcOptionBuilder {
|
||||
if option != nil {
|
||||
ob.options = append(ob.options, option)
|
||||
}
|
||||
return ob
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) ToOptions() []PeerConnectionOption {
|
||||
builder := pcOptionBuilder{}
|
||||
|
||||
return builder.
|
||||
add(c.withFirebaseOfferSignal()).
|
||||
add(c.withFirebaseOfferAnswer()).
|
||||
add(c.withDataChannels()).
|
||||
add(c.withMediaSource()).
|
||||
add(c.withMediaSinks()).
|
||||
options
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) withFirebaseOfferSignal() PeerConnectionOption {
|
||||
if c.FirebaseOfferSignal == nil {
|
||||
return nil
|
||||
}
|
||||
return WithFirebaseOfferSignal
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) withFirebaseOfferAnswer() PeerConnectionOption {
|
||||
if c.FirebaseOfferAnswer == nil {
|
||||
return nil
|
||||
}
|
||||
return WithFirebaseAnswerSignal
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) withMediaSource() PeerConnectionOption {
|
||||
if len(c.MediaSources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return WithMediaSources()
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) withMediaSinks() PeerConnectionOption {
|
||||
if len(c.MediaSinks) == 0 {
|
||||
return nil
|
||||
}
|
||||
return WithMediaSinks()
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) withDataChannels() PeerConnectionOption {
|
||||
if len(c.DataChannels) == 0 {
|
||||
return nil
|
||||
}
|
||||
return WithDataChannels()
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) CreateDataChannels(pc *PeerConnection) error {
|
||||
if len(c.DataChannels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, config := range c.DataChannels {
|
||||
if _, err := pc.CreateDataChannel(config.Label, datachannel.WithDataChannelInit(&webrtc.DataChannelInit{
|
||||
Ordered: config.Ordered,
|
||||
MaxPacketLifeTime: config.MaxPacketLifeTime,
|
||||
MaxRetransmits: config.MaxRetransmits,
|
||||
Protocol: config.Protocol,
|
||||
Negotiated: config.Negotiated,
|
||||
ID: config.ID,
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) CreateMediaSources(pc *PeerConnection) error {
|
||||
if len(c.MediaSources) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, config := range c.MediaSources {
|
||||
if _, err := pc.CreateMediaSource(c.Name, config.ToOptions()...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PeerConnectionConfig) CreateMediaSinks(pc *PeerConnection) error {
|
||||
if len(c.MediaSinks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, config := range c.MediaSinks {
|
||||
if _, err := pc.CreateMediaSink(c.Name, config.ToOptions()...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
18
config_cgo.go
Normal file
18
config_cgo.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
// func (c *PeerConnectionConfig) withBandwidthControl() PeerConnectionOption {
|
||||
// if len(c.BWSubscriptions) == 0 {
|
||||
// return nil
|
||||
// }
|
||||
// return WithBandwidthControl()
|
||||
// }
|
||||
|
||||
// func (c *PeerConnectionConfig) ToOptions() []PeerConnectionOption {
|
||||
// builder := pcOptionBuilder{}
|
||||
//
|
||||
// return builder.
|
||||
// add(c.withFirebaseOfferSignal()).
|
||||
// options
|
||||
// }
|
180
file_answer.go
Normal file
180
file_answer.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// FileAnswerSignal implements BaseSignal interface for file-based signaling (answer side)
|
||||
type FileAnswerSignal struct {
|
||||
peerConnection *PeerConnection
|
||||
ctx context.Context
|
||||
offerPath string
|
||||
answerPath string
|
||||
}
|
||||
|
||||
// CreateFileAnswerSignal creates a new FileAnswerSignal
|
||||
func CreateFileAnswerSignal(ctx context.Context, peerConnection *PeerConnection, offerPath string, answerPath string) *FileAnswerSignal {
|
||||
return &FileAnswerSignal{
|
||||
peerConnection: peerConnection,
|
||||
ctx: ctx,
|
||||
offerPath: offerPath,
|
||||
answerPath: answerPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect implements the BaseSignal interface
|
||||
func (signal *FileAnswerSignal) Connect(category, connectionLabel string) error {
|
||||
// Use category and connectionLabel to create unique filenames if needed
|
||||
if category != "" && connectionLabel != "" {
|
||||
signal.offerPath = filepath.Join(signal.offerPath, category, connectionLabel, "offer.txt")
|
||||
signal.answerPath = filepath.Join(signal.answerPath, category, connectionLabel, "answer.txt")
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(signal.answerPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for offer file to exist
|
||||
offer, err := signal.waitForOffer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process the offer
|
||||
return signal.processOffer(offer)
|
||||
}
|
||||
|
||||
// waitForOffer waits for the offer file to appear and loads it
|
||||
func (signal *FileAnswerSignal) waitForOffer() (webrtc.SessionDescription, error) {
|
||||
var offer webrtc.SessionDescription
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
timeout := time.NewTimer(5 * time.Minute)
|
||||
defer timeout.Stop()
|
||||
|
||||
fmt.Printf("Waiting for offer file at %s...\n", signal.offerPath)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Check if offer file exists
|
||||
if _, err := os.Stat(signal.offerPath); err == nil {
|
||||
// Load offer from file
|
||||
offer, err := signal.loadSDPFromFile(signal.offerPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading offer: %v, retrying...\n", err)
|
||||
continue
|
||||
}
|
||||
return offer, nil
|
||||
}
|
||||
case <-timeout.C:
|
||||
return offer, errors.New("timeout waiting for offer file")
|
||||
case <-signal.ctx.Done():
|
||||
return offer, errors.New("context canceled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processOffer processes the offer and creates an answer
|
||||
func (signal *FileAnswerSignal) processOffer(offer webrtc.SessionDescription) error {
|
||||
// SetInputOption remote description
|
||||
if err := signal.peerConnection.peerConnection.SetRemoteDescription(offer); err != nil {
|
||||
return fmt.Errorf("error setting remote description: %w", err)
|
||||
}
|
||||
|
||||
// Create answer
|
||||
answer, err := signal.peerConnection.peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating answer: %w", err)
|
||||
}
|
||||
|
||||
// SetInputOption local description
|
||||
if err := signal.peerConnection.peerConnection.SetLocalDescription(answer); err != nil {
|
||||
return fmt.Errorf("error setting local description: %w", err)
|
||||
}
|
||||
|
||||
// Wait for ICE gathering to complete
|
||||
timer := time.NewTicker(30 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-webrtc.GatheringCompletePromise(signal.peerConnection.peerConnection):
|
||||
fmt.Println("ICE Gathering complete")
|
||||
case <-timer.C:
|
||||
return errors.New("failed to gather ICE candidates within 30 seconds")
|
||||
}
|
||||
|
||||
// Save answer to file
|
||||
if err := signal.saveSDPToFile(signal.peerConnection.peerConnection.LocalDescription(), signal.answerPath); err != nil {
|
||||
return fmt.Errorf("error saving answer to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Answer saved to %s\n", signal.answerPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements the BaseSignal interface
|
||||
func (signal *FileAnswerSignal) Close() error {
|
||||
// Nothing to close for file-based signaling
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveSDPToFile saves a SessionDescription to a file
|
||||
func (signal *FileAnswerSignal) saveSDPToFile(sdp *webrtc.SessionDescription, filename string) error {
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Encode SDP
|
||||
b, err := json.Marshal(sdp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(b)
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(filename, []byte(encoded), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("SDP saved to %s (%d bytes)\n", filename, len(encoded))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSDPFromFile loads a SessionDescription from a file
|
||||
func (signal *FileAnswerSignal) loadSDPFromFile(filename string) (webrtc.SessionDescription, error) {
|
||||
var sdp webrtc.SessionDescription
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return sdp, fmt.Errorf("error reading %s: %w", filename, err)
|
||||
}
|
||||
|
||||
encoded := string(data)
|
||||
b, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return sdp, fmt.Errorf("base64 decode error: %w", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &sdp); err != nil {
|
||||
return sdp, fmt.Errorf("JSON unmarshal error: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("SDP loaded from %s (%d bytes)\n", filename, len(data))
|
||||
return sdp, nil
|
||||
}
|
181
file_offer.go
Normal file
181
file_offer.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// FileOfferSignal implements BaseSignal interface for file-based signaling (offer side)
|
||||
type FileOfferSignal struct {
|
||||
peerConnection *PeerConnection
|
||||
ctx context.Context
|
||||
offerPath string
|
||||
answerPath string
|
||||
}
|
||||
|
||||
// CreateFileOfferSignal creates a new FileOfferSignal
|
||||
func CreateFileOfferSignal(ctx context.Context, peerConnection *PeerConnection, offerPath string, answerPath string) *FileOfferSignal {
|
||||
return &FileOfferSignal{
|
||||
peerConnection: peerConnection,
|
||||
ctx: ctx,
|
||||
offerPath: offerPath,
|
||||
answerPath: answerPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect implements the BaseSignal interface
|
||||
func (signal *FileOfferSignal) Connect(category, connectionLabel string) error {
|
||||
// Use category and connectionLabel to create unique filenames if needed
|
||||
if category != "" && connectionLabel != "" {
|
||||
signal.offerPath = filepath.Join(signal.offerPath, category, connectionLabel, "offer.txt")
|
||||
signal.answerPath = filepath.Join(signal.answerPath, category, connectionLabel, "answer.txt")
|
||||
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(signal.offerPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove existing answer file if it exists
|
||||
if _, err := os.Stat(signal.offerPath); err == nil {
|
||||
if err := os.Remove(signal.offerPath); err != nil {
|
||||
return fmt.Errorf("error removing existing offer file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove existing answer file if it exists
|
||||
if _, err := os.Stat(signal.answerPath); err == nil {
|
||||
if err := os.Remove(signal.answerPath); err != nil {
|
||||
return fmt.Errorf("error removing existing answer file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create offer
|
||||
offer, err := signal.peerConnection.peerConnection.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating offer: %w", err)
|
||||
}
|
||||
|
||||
if err := signal.peerConnection.peerConnection.SetLocalDescription(offer); err != nil {
|
||||
return fmt.Errorf("error setting local description: %w", err)
|
||||
}
|
||||
|
||||
// Wait for ICE gathering to complete
|
||||
timer := time.NewTicker(30 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-webrtc.GatheringCompletePromise(signal.peerConnection.peerConnection):
|
||||
fmt.Println("ICE Gathering complete")
|
||||
case <-timer.C:
|
||||
return errors.New("failed to gather ICE candidates within 30 seconds")
|
||||
}
|
||||
|
||||
// Save offer to file
|
||||
if err := signal.saveSDPToFile(signal.peerConnection.peerConnection.LocalDescription(), signal.offerPath); err != nil {
|
||||
return fmt.Errorf("error saving offer to file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Offer saved to %s. Waiting for answer...\n", signal.offerPath)
|
||||
|
||||
// Wait for answer file
|
||||
return signal.waitForAnswer()
|
||||
}
|
||||
|
||||
// waitForAnswer waits for the answer file to appear and processes it
|
||||
func (signal *FileOfferSignal) waitForAnswer() error {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
timeout := time.NewTimer(5 * time.Minute)
|
||||
defer timeout.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Check if answer file exists
|
||||
if _, err := os.Stat(signal.answerPath); err == nil {
|
||||
// Load answer from file
|
||||
answer, err := signal.loadSDPFromFile(signal.answerPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading answer: %v, retrying...\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// SetInputOption remote description
|
||||
if err = signal.peerConnection.peerConnection.SetRemoteDescription(answer); err != nil {
|
||||
return fmt.Errorf("error setting remote description: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Answer processed successfully")
|
||||
return nil
|
||||
}
|
||||
case <-timeout.C:
|
||||
return errors.New("timeout waiting for answer file")
|
||||
case <-signal.ctx.Done():
|
||||
return errors.New("context canceled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close implements the BaseSignal interface
|
||||
func (signal *FileOfferSignal) Close() error {
|
||||
// Nothing to close for file-based signaling
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveSDPToFile saves a SessionDescription to a file
|
||||
func (signal *FileOfferSignal) saveSDPToFile(sdp *webrtc.SessionDescription, filename string) error {
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("error creating directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
// Encode SDP
|
||||
b, err := json.Marshal(sdp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(b)
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(filename, []byte(encoded), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("SDP saved to %s (%d bytes)\n", filename, len(encoded))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadSDPFromFile loads a SessionDescription from a file
|
||||
func (signal *FileOfferSignal) loadSDPFromFile(filename string) (webrtc.SessionDescription, error) {
|
||||
var sdp webrtc.SessionDescription
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return sdp, fmt.Errorf("error reading %s: %w", filename, err)
|
||||
}
|
||||
|
||||
encoded := string(data)
|
||||
b, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return sdp, fmt.Errorf("base64 decode error: %w", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(b, &sdp); err != nil {
|
||||
return sdp, fmt.Errorf("JSON unmarshal error: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("SDP loaded from %s (%d bytes)\n", filename, len(data))
|
||||
return sdp, nil
|
||||
}
|
130
firebase_answer.go
Normal file
130
firebase_answer.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/firestore"
|
||||
"firebase.google.com/go"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type AnswerSignal struct {
|
||||
peerConnection *PeerConnection
|
||||
app *firebase.App
|
||||
client *firestore.Client
|
||||
docRef *firestore.DocumentRef
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateFirebaseAnswerSignal(ctx context.Context, peerConnection *PeerConnection) *AnswerSignal {
|
||||
var (
|
||||
configuration option.ClientOption
|
||||
app *firebase.App
|
||||
client *firestore.Client
|
||||
err error
|
||||
)
|
||||
|
||||
if configuration, err = GetFirebaseConfiguration(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if app, err = firebase.NewApp(ctx, nil, configuration); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if client, err = app.Firestore(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &AnswerSignal{
|
||||
app: app,
|
||||
client: client,
|
||||
peerConnection: peerConnection,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (signal *AnswerSignal) Connect(category, connectionLabel string) error {
|
||||
signal.docRef = signal.client.Collection(category).Doc(connectionLabel)
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
var data map[string]interface{}
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
snapshot, err := signal.docRef.Get(signal.ctx)
|
||||
if err != nil || snapshot == nil {
|
||||
if status.Code(err) == codes.NotFound {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
data = snapshot.Data()
|
||||
|
||||
if currentStatus, exists := data[FieldStatus]; !exists || currentStatus != FieldStatusPending {
|
||||
continue loop
|
||||
}
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
fmt.Println("Found Offer. Creating answer...")
|
||||
|
||||
return signal.answer(data[FieldOffer].(map[string]interface{}))
|
||||
}
|
||||
|
||||
func (signal *AnswerSignal) answer(offer map[string]interface{}) error {
|
||||
sdp, ok := offer[FieldSDP].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid SDP format in offer")
|
||||
}
|
||||
|
||||
if err := signal.peerConnection.peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
SDP: sdp,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
answer, err := signal.peerConnection.peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while creating answer: %w", err)
|
||||
}
|
||||
|
||||
if err := signal.peerConnection.peerConnection.SetLocalDescription(answer); err != nil {
|
||||
return fmt.Errorf("error while setting local sdp: %w", err)
|
||||
}
|
||||
|
||||
timer := time.NewTicker(30 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-webrtc.GatheringCompletePromise(signal.peerConnection.peerConnection):
|
||||
fmt.Println("ICE Gathering complete")
|
||||
case <-timer.C:
|
||||
return errors.New("failed to gather ICE candidates within 30 seconds")
|
||||
}
|
||||
|
||||
if _, err = signal.docRef.Set(signal.ctx, map[string]interface{}{
|
||||
FieldAnswer: map[string]interface{}{
|
||||
FieldSDP: signal.peerConnection.peerConnection.LocalDescription().SDP,
|
||||
FieldUpdatedAt: firestore.ServerTimestamp,
|
||||
},
|
||||
FieldStatus: FieldStatusConnected,
|
||||
}, firestore.MergeAll); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (signal *AnswerSignal) Close() error {
|
||||
return signal.client.Close()
|
||||
}
|
49
firebase_config.go
Normal file
49
firebase_config.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type firebaseConfig struct {
|
||||
Type string `json:"type"`
|
||||
ProjectID string `json:"project_id"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientID string `json:"client_id"`
|
||||
AuthURI string `json:"auth_uri"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
|
||||
ClientX509CertURL string `json:"client_x509_cert_url"`
|
||||
UniverseDomain string `json:"universe_domain"`
|
||||
}
|
||||
|
||||
func GetFirebaseConfiguration() (option.ClientOption, error) {
|
||||
var (
|
||||
config firebaseConfig
|
||||
err error
|
||||
configBytes []byte
|
||||
)
|
||||
config = firebaseConfig{
|
||||
Type: os.Getenv("FIREBASE_TYPE"),
|
||||
ProjectID: os.Getenv("FIREBASE_PROJECT_ID"),
|
||||
PrivateKeyID: os.Getenv("FIREBASE_PRIVATE_KEY_ID"),
|
||||
PrivateKey: strings.ReplaceAll(os.Getenv("FIREBASE_PRIVATE_KEY"), "\\n", "\n"),
|
||||
ClientEmail: os.Getenv("FIREBASE_CLIENT_EMAIL"),
|
||||
ClientID: os.Getenv("FIREBASE_CLIENT_ID"),
|
||||
AuthURI: os.Getenv("FIREBASE_AUTH_URI"),
|
||||
TokenURI: os.Getenv("FIREBASE_AUTH_TOKEN_URI"),
|
||||
AuthProviderX509CertURL: os.Getenv("FIREBASE_AUTH_PROVIDER_X509_CERT_URL"),
|
||||
ClientX509CertURL: os.Getenv("FIREBASE_AUTH_CLIENT_X509_CERT_URL"),
|
||||
UniverseDomain: os.Getenv("FIREBASE_UNIVERSE_DOMAIN"),
|
||||
}
|
||||
|
||||
if configBytes, err = json.Marshal(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return option.WithCredentialsJSON(configBytes), nil
|
||||
}
|
143
firebase_offer.go
Normal file
143
firebase_offer.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/firestore"
|
||||
"firebase.google.com/go"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type OfferSignal struct {
|
||||
peerConnection *PeerConnection
|
||||
app *firebase.App
|
||||
firebaseClient *firestore.Client
|
||||
docRef *firestore.DocumentRef
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateFirebaseOfferSignal(ctx context.Context, peerConnection *PeerConnection) *OfferSignal {
|
||||
var (
|
||||
configuration option.ClientOption
|
||||
app *firebase.App
|
||||
firebaseClient *firestore.Client
|
||||
err error
|
||||
)
|
||||
|
||||
if configuration, err = GetFirebaseConfiguration(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if app, err = firebase.NewApp(ctx, nil, configuration); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if firebaseClient, err = app.Firestore(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &OfferSignal{
|
||||
app: app,
|
||||
firebaseClient: firebaseClient,
|
||||
peerConnection: peerConnection,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (signal *OfferSignal) Connect(category, connectionLabel string) error {
|
||||
signal.docRef = signal.firebaseClient.Collection(category).Doc(connectionLabel)
|
||||
_, err := signal.docRef.Get(signal.ctx)
|
||||
|
||||
if err != nil && status.Code(err) != codes.NotFound {
|
||||
fmt.Println(status.Code(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if _, err := signal.docRef.Delete(signal.ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
offer, err := signal.peerConnection.peerConnection.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while creating offer: %w", err)
|
||||
}
|
||||
|
||||
if err := signal.peerConnection.peerConnection.SetLocalDescription(offer); err != nil {
|
||||
return fmt.Errorf("error while setting local sdp: %w", err)
|
||||
}
|
||||
|
||||
timer := time.NewTicker(30 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-webrtc.GatheringCompletePromise(signal.peerConnection.peerConnection):
|
||||
fmt.Println("ICE Gathering complete")
|
||||
case <-timer.C:
|
||||
return errors.New("failed to gather ICE candidates within 30 seconds")
|
||||
}
|
||||
|
||||
if _, err = signal.docRef.Set(signal.ctx, map[string]interface{}{
|
||||
FieldOffer: map[string]interface{}{
|
||||
FieldCreatedAt: firestore.ServerTimestamp,
|
||||
FieldSDP: signal.peerConnection.peerConnection.LocalDescription().SDP,
|
||||
FieldUpdatedAt: firestore.ServerTimestamp,
|
||||
},
|
||||
FieldStatus: FieldStatusPending,
|
||||
}, firestore.MergeAll); err != nil {
|
||||
return fmt.Errorf("error while setting data to firestore: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Offer updated in firestore. Waiting for peer connection...")
|
||||
|
||||
return signal.offer()
|
||||
}
|
||||
|
||||
func (signal *OfferSignal) offer() error {
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
snapshot, err := signal.docRef.Get(signal.ctx)
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.NotFound {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
answer, exists := snapshot.Data()[FieldAnswer].(map[string]interface{})
|
||||
if !exists {
|
||||
continue loop
|
||||
}
|
||||
|
||||
sdp, ok := answer[FieldSDP].(string)
|
||||
if !ok {
|
||||
continue loop
|
||||
}
|
||||
if err = signal.peerConnection.peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeAnswer,
|
||||
SDP: sdp,
|
||||
}); err != nil {
|
||||
fmt.Printf("error while setting remote description: %s", err.Error())
|
||||
continue loop
|
||||
}
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (signal *OfferSignal) Close() error {
|
||||
return signal.firebaseClient.Close()
|
||||
}
|
76
go.mod
Normal file
76
go.mod
Normal file
@@ -0,0 +1,76 @@
|
||||
module github.com/harshabose/simple_webrtc_comm/client
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.18.0
|
||||
firebase.google.com/go v3.13.0+incompatible
|
||||
github.com/asticode/go-astiav v0.37.0
|
||||
github.com/harshabose/mediapipe v0.0.0
|
||||
github.com/harshabose/tools v0.0.0
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtp v1.8.19
|
||||
github.com/pion/sdp/v3 v3.0.13
|
||||
github.com/pion/webrtc/v4 v4.1.2
|
||||
google.golang.org/api v0.222.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.117.0 // indirect
|
||||
cloud.google.com/go/auth v0.14.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.2 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
github.com/asticode/go-astikit v0.52.0 // indirect
|
||||
github.com/bluenviron/gortsplib/v4 v4.14.1 // indirect
|
||||
github.com/bluenviron/mediacommon/v2 v2.2.0 // indirect
|
||||
github.com/coder/websocket v1.8.13 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/harshabose/mediapipe => ../mediapipe
|
||||
github.com/harshabose/tools => ../tools
|
||||
)
|
171
go.sum
Normal file
171
go.sum
Normal file
@@ -0,0 +1,171 @@
|
||||
cloud.google.com/go v0.117.0 h1:Z5TNFfQxj7WG2FgOGX1ekC5RiXrYgms6QscOm32M/4s=
|
||||
cloud.google.com/go v0.117.0/go.mod h1:ZbwhVTb1DBGt2Iwb3tNO6SEK4q+cplHZmLWH+DelYYc=
|
||||
cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
|
||||
cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
|
||||
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
|
||||
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
|
||||
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
|
||||
cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
|
||||
cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
|
||||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
|
||||
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
|
||||
github.com/asticode/go-astiav v0.37.0 h1:Ph4usW4lulotVvne8hqZ1JCOHX1f8ces6yVKdg+PnyQ=
|
||||
github.com/asticode/go-astiav v0.37.0/go.mod h1:GI0pHw6K2/pl/o8upCtT49P/q4KCwhv/8nGLlCsZLdA=
|
||||
github.com/asticode/go-astikit v0.52.0 h1:kTl2XjgiVQhUl1H7kim7NhmTtCMwVBbPrXKqhQhbk8Y=
|
||||
github.com/asticode/go-astikit v0.52.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/bluenviron/gortsplib/v4 v4.14.1 h1:v99NmXeeJFfbrO+ipPzPxYGibQaR5ZOUESOA9UQZhsI=
|
||||
github.com/bluenviron/gortsplib/v4 v4.14.1/go.mod h1:3LaEcg0d47+kfXju5KSlsSxCiZ3IKBI/sqIrBPcsS64=
|
||||
github.com/bluenviron/mediacommon/v2 v2.2.0 h1:fGXEX0OEvv5VhGHOv3Q2ABzOtSkIpl9UbwOHrnKWNTk=
|
||||
github.com/bluenviron/mediacommon/v2 v2.2.0/go.mod h1:a6MbPmXtYda9mKibKVMZlW20GYLLrX2R7ZkUE+1pwV0=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c=
|
||||
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.222.0 h1:Aiewy7BKLCuq6cUCeOUrsAlzjXPqBkEeQ/iwGHVQa/4=
|
||||
google.golang.org/api v0.222.0/go.mod h1:efZia3nXpWELrwMlN5vyQrD4GmJN1Vw0x68Et3r+a9c=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
158
peerconnection.go
Normal file
158
peerconnection.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/datachannel"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasink"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasource"
|
||||
)
|
||||
|
||||
type PeerConnection struct {
|
||||
label string
|
||||
peerConnection *webrtc.PeerConnection
|
||||
dataChannels *datachannel.DataChannels
|
||||
tracks *mediasource.Tracks
|
||||
sinks *mediasink.Sinks
|
||||
signal BaseSignal
|
||||
bwController *BWEController
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func CreatePeerConnection(ctx context.Context, cancel context.CancelFunc, label string, api *webrtc.API, config webrtc.Configuration, options ...PeerConnectionOption) (*PeerConnection, error) {
|
||||
pc := &PeerConnection{
|
||||
label: label,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc.peerConnection = peerConnection
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(pc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if pc.signal == nil {
|
||||
return nil, errors.New("signaling protocol not provided")
|
||||
}
|
||||
|
||||
return pc.onConnectionStateChangeEvent().onICEConnectionStateChange().onICEGatheringStateChange().onICECandidate(), err
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) GetLabel() string {
|
||||
return pc.label
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) GetPeerConnection() (*webrtc.PeerConnection, error) {
|
||||
if pc.peerConnection == nil {
|
||||
return nil, errors.New("raw peer connection is nil")
|
||||
}
|
||||
|
||||
return pc.peerConnection, nil
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) onConnectionStateChangeEvent() *PeerConnection {
|
||||
pc.peerConnection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||
fmt.Printf("peer connection state with label changed to %s\n", state.String())
|
||||
if state == webrtc.PeerConnectionStateDisconnected || state == webrtc.PeerConnectionStateClosed || state == webrtc.PeerConnectionStateFailed {
|
||||
fmt.Println("tying to cancel context for restart")
|
||||
pc.cancel()
|
||||
}
|
||||
})
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) onICEConnectionStateChange() *PeerConnection {
|
||||
pc.peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
fmt.Printf("ICE Connection State changed: %s\n", state.String())
|
||||
})
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) onICEGatheringStateChange() *PeerConnection {
|
||||
pc.peerConnection.OnICEGatheringStateChange(func(state webrtc.ICEGatheringState) {
|
||||
fmt.Printf("ICE Gathering State changed: %s\n", state.String())
|
||||
})
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) onICECandidate() *PeerConnection {
|
||||
pc.peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
fmt.Println("ICE gathering complete")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found candidate: %s (type: %s)\n", candidate.String(), candidate.Typ)
|
||||
})
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) CreateDataChannel(label string, options ...datachannel.Option) (*datachannel.DataChannel, error) {
|
||||
if pc.dataChannels == nil {
|
||||
return nil, errors.New("data channels are not enabled")
|
||||
}
|
||||
channel, err := pc.dataChannels.CreateDataChannel(label, pc.peerConnection, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) CreateMediaSource(label string, options ...mediasource.TrackOption) (*mediasource.Track, error) {
|
||||
if pc.tracks == nil {
|
||||
return nil, errors.New("media source are not enabled")
|
||||
}
|
||||
|
||||
track, err := pc.tracks.CreateTrack(label, pc.peerConnection, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) CreateMediaSink(label string, options ...mediasink.SinkOption) (*mediasink.Sink, error) {
|
||||
if pc.sinks == nil {
|
||||
return nil, errors.New("media sinks are not enabled")
|
||||
}
|
||||
|
||||
sink, err := pc.sinks.CreateSink(label, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) Connect(category string) error {
|
||||
if pc.signal == nil {
|
||||
return errors.New("no signaling protocol provided")
|
||||
}
|
||||
if err := pc.signal.Connect(category, pc.label); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pc *PeerConnection) Close() error {
|
||||
// TODO:
|
||||
// clear data channels if any
|
||||
// clear tracks if any
|
||||
// clear sinks if any
|
||||
// clear bwController ??
|
||||
return pc.peerConnection.Close()
|
||||
}
|
6
peerconnection_!cgo.go
Normal file
6
peerconnection_!cgo.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build !cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
// BWEController dummy when BWE is disabled
|
||||
type BWEController struct{}
|
13
peerconnection_cgo.go
Normal file
13
peerconnection_cgo.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
import "errors"
|
||||
|
||||
func (pc *PeerConnection) GetBWEstimator() (*BWEController, error) {
|
||||
if pc.bwController == nil || pc.bwController.estimator == nil {
|
||||
return nil, errors.New("bitrate control is not enabled")
|
||||
}
|
||||
|
||||
return pc.bwController, nil
|
||||
}
|
71
peerconnection_options.go
Normal file
71
peerconnection_options.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/datachannel"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasink"
|
||||
"github.com/harshabose/simple_webrtc_comm/client/pkg/mediasource"
|
||||
)
|
||||
|
||||
type PeerConnectionOption = func(*PeerConnection) error
|
||||
|
||||
func WithFirebaseOfferSignal(connection *PeerConnection) error {
|
||||
if connection.signal != nil {
|
||||
return errors.New("multiple options for signaling were provided. this is not supported")
|
||||
}
|
||||
connection.signal = CreateFirebaseOfferSignal(connection.ctx, connection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithFirebaseAnswerSignal(connection *PeerConnection) error {
|
||||
if connection.signal != nil {
|
||||
return errors.New("multiple options for signaling were provided. this is not supported")
|
||||
}
|
||||
connection.signal = CreateFirebaseAnswerSignal(connection.ctx, connection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithFileOfferSignal(offerPath, answerPath string) PeerConnectionOption {
|
||||
return func(connection *PeerConnection) error {
|
||||
if connection.signal != nil {
|
||||
return errors.New("multiple options for signaling were provided. this is not supported")
|
||||
}
|
||||
connection.signal = CreateFileOfferSignal(connection.ctx, connection, offerPath, answerPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithFileAnswerSignal(offerPath, answerPath string) PeerConnectionOption {
|
||||
return func(connection *PeerConnection) error {
|
||||
if connection.signal != nil {
|
||||
return errors.New("multiple options for signaling were provided. this is not supported")
|
||||
}
|
||||
connection.signal = CreateFileAnswerSignal(connection.ctx, connection, offerPath, answerPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMediaSources() PeerConnectionOption {
|
||||
return func(pc *PeerConnection) error {
|
||||
pc.tracks = mediasource.CreateTracks(pc.ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMediaSinks() PeerConnectionOption {
|
||||
return func(pc *PeerConnection) error {
|
||||
pc.sinks = mediasink.CreateSinks(pc.ctx, pc.peerConnection)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDataChannels() PeerConnectionOption {
|
||||
return func(pc *PeerConnection) error {
|
||||
pc.dataChannels = datachannel.CreateDataChannels(pc.ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
10
peerconnection_options_cgo.go
Normal file
10
peerconnection_options_cgo.go
Normal file
@@ -0,0 +1,10 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package client
|
||||
|
||||
func WithBandwidthControl() PeerConnectionOption {
|
||||
return func(connection *PeerConnection) error {
|
||||
connection.bwController = createBWController(connection.ctx)
|
||||
return nil
|
||||
}
|
||||
}
|
139
pkg/datachannel/datachannel.go
Normal file
139
pkg/datachannel/datachannel.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package datachannel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type DataChannel struct {
|
||||
label string
|
||||
datachannel *webrtc.DataChannel
|
||||
init *webrtc.DataChannelInit
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateDataChannel(ctx context.Context, label string, peerConnection *webrtc.PeerConnection, options ...Option) (*DataChannel, error) {
|
||||
dc := &DataChannel{
|
||||
label: label,
|
||||
datachannel: nil,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(dc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
datachannel, err := peerConnection.CreateDataChannel(label, dc.init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dc.datachannel = datachannel
|
||||
|
||||
return dc.onOpen().onClose(), nil
|
||||
}
|
||||
|
||||
func CreateRawDataChannel(ctx context.Context, channel *webrtc.DataChannel) (*DataChannel, error) {
|
||||
dataChannel := &DataChannel{
|
||||
label: channel.Label(),
|
||||
datachannel: channel,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return dataChannel.onOpen().onClose(), nil
|
||||
}
|
||||
|
||||
func (dataChannel *DataChannel) GetLabel() string {
|
||||
return dataChannel.label
|
||||
}
|
||||
|
||||
func (dataChannel *DataChannel) Close() error {
|
||||
if err := dataChannel.datachannel.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dataChannel *DataChannel) onOpen() *DataChannel {
|
||||
dataChannel.datachannel.OnOpen(func() {
|
||||
fmt.Printf("dataChannel Open with Label: %s\n", dataChannel.datachannel.Label())
|
||||
})
|
||||
return dataChannel
|
||||
}
|
||||
|
||||
func (dataChannel *DataChannel) onClose() *DataChannel {
|
||||
dataChannel.datachannel.OnClose(func() {
|
||||
fmt.Printf("dataChannel Closed with Label: %s\n", dataChannel.datachannel.Label())
|
||||
})
|
||||
return dataChannel
|
||||
}
|
||||
|
||||
func (dataChannel *DataChannel) DataChannel() *webrtc.DataChannel {
|
||||
return dataChannel.datachannel
|
||||
}
|
||||
|
||||
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
type DataChannels struct {
|
||||
datachannel map[string]*DataChannel
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateDataChannels(ctx context.Context) *DataChannels {
|
||||
return &DataChannels{
|
||||
datachannel: map[string]*DataChannel{},
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (dataChannels *DataChannels) CreateDataChannel(label string, peerConnection *webrtc.PeerConnection, options ...Option) (*DataChannel, error) {
|
||||
if _, exits := dataChannels.datachannel[label]; exits {
|
||||
return nil, fmt.Errorf("datachannel with id = '%s' already exists", label)
|
||||
}
|
||||
|
||||
channel, err := CreateDataChannel(dataChannels.ctx, label, peerConnection, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataChannels.datachannel[label] = channel
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (dataChannels *DataChannels) CreateRawDataChannel(channel *webrtc.DataChannel) (*DataChannel, error) {
|
||||
_, exists := dataChannels.datachannel[channel.Label()]
|
||||
if exists {
|
||||
return nil, fmt.Errorf("data channel already exists with label: %s", channel.Label())
|
||||
}
|
||||
|
||||
dataChannel, err := CreateRawDataChannel(dataChannels.ctx, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataChannels.datachannel[channel.Label()] = dataChannel
|
||||
|
||||
return dataChannel, nil
|
||||
}
|
||||
|
||||
func (dataChannels *DataChannels) GetDataChannel(label string) (*DataChannel, error) {
|
||||
dataChannel, exists := dataChannels.datachannel[label]
|
||||
if !exists {
|
||||
return nil, errors.New("datachannel does not exists")
|
||||
}
|
||||
return dataChannel, nil
|
||||
}
|
||||
|
||||
func (dataChannels *DataChannels) Close(label string) (err error) {
|
||||
if err = dataChannels.datachannel[label].Close(); err == nil {
|
||||
return nil
|
||||
}
|
||||
delete(dataChannels.datachannel, label)
|
||||
return err
|
||||
}
|
12
pkg/datachannel/options.go
Normal file
12
pkg/datachannel/options.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package datachannel
|
||||
|
||||
import "github.com/pion/webrtc/v4"
|
||||
|
||||
type Option = func(*DataChannel) error
|
||||
|
||||
func WithDataChannelInit(init *webrtc.DataChannelInit) Option {
|
||||
return func(channel *DataChannel) error {
|
||||
channel.init = init
|
||||
return nil
|
||||
}
|
||||
}
|
56
pkg/mediasink/options.go
Normal file
56
pkg/mediasink/options.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package mediasink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type SinkOption = func(*Sink) error
|
||||
|
||||
// TODO: CLOCKRATE, STEREO, PROFILE etc ARE IN MEDIA SOURCE. MAYBE BE INCLUDE THEM HERE?
|
||||
|
||||
func WithH264Track(clockrate uint32) SinkOption {
|
||||
return func(track *Sink) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecParameters{}
|
||||
track.codecCapability.PayloadType = webrtc.PayloadType(102)
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeH264
|
||||
track.codecCapability.ClockRate = clockrate
|
||||
track.codecCapability.Channels = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVP8Track(clockrate uint32) SinkOption {
|
||||
return func(track *Sink) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecParameters{}
|
||||
track.codecCapability.PayloadType = webrtc.PayloadType(96)
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeVP8
|
||||
track.codecCapability.ClockRate = clockrate
|
||||
track.codecCapability.Channels = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpusTrack(samplerate uint32, channelLayout uint16) SinkOption {
|
||||
return func(track *Sink) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecParameters{}
|
||||
track.codecCapability.PayloadType = webrtc.PayloadType(111)
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeOpus
|
||||
track.codecCapability.ClockRate = samplerate
|
||||
track.codecCapability.Channels = channelLayout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
187
pkg/mediasink/sinks.go
Normal file
187
pkg/mediasink/sinks.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package mediasink
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
"github.com/harshabose/mediapipe/pkg/generators"
|
||||
)
|
||||
|
||||
type Sink struct {
|
||||
generator generators.CanGeneratePionRTPPacket
|
||||
codecCapability *webrtc.RTPCodecParameters
|
||||
rtpReceiver *webrtc.RTPReceiver
|
||||
mux sync.RWMutex
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateSink(ctx context.Context, options ...SinkOption) (*Sink, error) {
|
||||
sink := &Sink{ctx: ctx}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(sink); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if sink.codecCapability == nil {
|
||||
return nil, errors.New("no sink capabilities given")
|
||||
}
|
||||
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
func (s *Sink) setGenerator(generator generators.CanGeneratePionRTPPacket) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
s.generator = generator
|
||||
}
|
||||
|
||||
func (s *Sink) setRTPReceiver(receiver *webrtc.RTPReceiver) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
s.rtpReceiver = receiver
|
||||
}
|
||||
|
||||
func (s *Sink) readRTPReceiver(rtcpBuf []byte) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
if s.rtpReceiver == nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, err := s.rtpReceiver.Read(rtcpBuf); err != nil {
|
||||
fmt.Printf("error while reading rtcp packets")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sink) rtpReceiverLoop() {
|
||||
// THIS IS NEEDED AS interceptors (pion) do not work
|
||||
for {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
s.readRTPReceiver(rtcpBuf)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sink) ReadRTP() (*rtp.Packet, interceptor.Attributes, error) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
if s.generator == nil {
|
||||
return nil, interceptor.Attributes{}, nil
|
||||
}
|
||||
|
||||
return s.generator.ReadRTP()
|
||||
}
|
||||
|
||||
type Sinks struct {
|
||||
sinks map[string]*Sink
|
||||
mux sync.RWMutex
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateSinks(ctx context.Context, pc *webrtc.PeerConnection) *Sinks {
|
||||
s := &Sinks{
|
||||
sinks: make(map[string]*Sink),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
s.onTrack(pc)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Sinks) onTrack(pc *webrtc.PeerConnection) {
|
||||
pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
sink, err := s.GetSink(remote.ID())
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
// TODO: MAYBE SET A DEFAULT SINK?
|
||||
return
|
||||
}
|
||||
|
||||
if !CompareRTPCodecParameters(remote.Codec(), *(sink.codecCapability)) {
|
||||
fmt.Println("sink registered codec did not match. skipping...")
|
||||
return
|
||||
}
|
||||
|
||||
sink.setRTPReceiver(receiver)
|
||||
sink.setGenerator(remote)
|
||||
|
||||
go sink.rtpReceiverLoop()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Sinks) CreateSink(label string, options ...SinkOption) (*Sink, error) {
|
||||
s.mux.Lock()
|
||||
defer s.mux.Unlock()
|
||||
|
||||
if _, exists := s.sinks[label]; exists {
|
||||
return nil, fmt.Errorf("sink with id='%s' already exists", label)
|
||||
}
|
||||
|
||||
sink, err := CreateSink(s.ctx, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.sinks[label] = sink
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
func (s *Sinks) GetSink(label string) (*Sink, error) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
sink, exists := s.sinks[label]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("ERROR: no sink set for track with id %s; ignoring track...\n", label)
|
||||
}
|
||||
|
||||
return sink, nil
|
||||
}
|
||||
|
||||
func CompareRTPCodecParameters(a, b webrtc.RTPCodecParameters) bool {
|
||||
identical := true
|
||||
|
||||
if a.PayloadType != b.PayloadType {
|
||||
fmt.Printf("PayloadType differs: %v != %v\n", a.PayloadType, b.PayloadType)
|
||||
identical = false
|
||||
}
|
||||
|
||||
if a.MimeType != b.MimeType {
|
||||
fmt.Printf("MimeType differs: %s != %s\n", a.MimeType, b.MimeType)
|
||||
identical = false
|
||||
}
|
||||
|
||||
if a.ClockRate != b.ClockRate {
|
||||
fmt.Printf("ClockRate differs: %d != %d\n", a.ClockRate, b.ClockRate)
|
||||
identical = false
|
||||
}
|
||||
|
||||
if a.Channels != b.Channels {
|
||||
fmt.Printf("Channels differs: %d != %d\n", a.Channels, b.Channels)
|
||||
identical = false
|
||||
}
|
||||
|
||||
if a.SDPFmtpLine != b.SDPFmtpLine {
|
||||
fmt.Printf("SDPFmtpLine differs (ignored): %s != %s\n", a.SDPFmtpLine, b.SDPFmtpLine)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(a.RTCPFeedback, b.RTCPFeedback) {
|
||||
fmt.Printf("RTCPFeedback differs (ignored): %v != %v\n", a.RTCPFeedback, b.RTCPFeedback)
|
||||
}
|
||||
|
||||
return identical
|
||||
}
|
46
pkg/mediasource/constants.go
Normal file
46
pkg/mediasource/constants.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package mediasource
|
||||
|
||||
type PacketisationMode uint8
|
||||
|
||||
const (
|
||||
PacketisationMode0 PacketisationMode = 0
|
||||
PacketisationMode1 PacketisationMode = 1
|
||||
PacketisationMode2 PacketisationMode = 2
|
||||
)
|
||||
|
||||
type ProfileLevel string
|
||||
|
||||
const (
|
||||
ProfileLevelBaseline21 ProfileLevel = "420015" // Level 2.1 (480p)
|
||||
ProfileLevelBaseline31 ProfileLevel = "42001f" // Level 3.1 (720p)
|
||||
ProfileLevelBaseline41 ProfileLevel = "420029" // Level 4.1 (1080p)
|
||||
ProfileLevelBaseline42 ProfileLevel = "42002a" // Level 4.2 (2K)
|
||||
|
||||
ProfileLevelMain21 ProfileLevel = "4D0015" // Level 2.1
|
||||
ProfileLevelMain31 ProfileLevel = "4D001f" // Level 3.1
|
||||
ProfileLevelMain41 ProfileLevel = "4D0029" // Level 4.1
|
||||
ProfileLevelMain42 ProfileLevel = "4D002a" // Level 4.2
|
||||
|
||||
ProfileLevelHigh21 ProfileLevel = "640015" // Level 2.1
|
||||
ProfileLevelHigh31 ProfileLevel = "64001f" // Level 3.1
|
||||
ProfileLevelHigh41 ProfileLevel = "640029" // Level 4.1
|
||||
ProfileLevelHigh42 ProfileLevel = "64002a" // Level 4.2
|
||||
)
|
||||
|
||||
type StereoType uint8
|
||||
|
||||
const (
|
||||
StereoMono = StereoType(0)
|
||||
StereoDual = StereoType(1)
|
||||
)
|
||||
|
||||
type Priority uint8
|
||||
|
||||
const (
|
||||
Level0 Priority = 0
|
||||
Level1 Priority = 1
|
||||
Level2 Priority = 2
|
||||
Level3 Priority = 3
|
||||
Level4 Priority = 4
|
||||
Level5 Priority = 5
|
||||
)
|
58
pkg/mediasource/options.go
Normal file
58
pkg/mediasource/options.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package mediasource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type TrackOption = func(*Track) error
|
||||
|
||||
func WithH264Track(clockrate uint32, packetisationMode PacketisationMode, profileLevel ProfileLevel) TrackOption {
|
||||
return func(track *Track) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecCapability{}
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeH264
|
||||
track.codecCapability.ClockRate = clockrate
|
||||
track.codecCapability.Channels = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVP8Track(clockrate uint32) TrackOption {
|
||||
return func(track *Track) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecCapability{}
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeVP8
|
||||
track.codecCapability.ClockRate = clockrate
|
||||
track.codecCapability.Channels = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpusTrack(samplerate uint32, channelLayout uint16, stereo StereoType) TrackOption {
|
||||
return func(track *Track) error {
|
||||
if track.codecCapability != nil {
|
||||
return errors.New("multiple tracks are not supported on single media source")
|
||||
}
|
||||
track.codecCapability = &webrtc.RTPCodecCapability{}
|
||||
track.codecCapability.MimeType = webrtc.MimeTypeOpus
|
||||
track.codecCapability.ClockRate = samplerate
|
||||
track.codecCapability.Channels = channelLayout
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPriority(level Priority) TrackOption {
|
||||
return func(track *Track) error {
|
||||
track.priority = level
|
||||
return nil
|
||||
}
|
||||
}
|
73
pkg/mediasource/track.go
Normal file
73
pkg/mediasource/track.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package mediasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
|
||||
_ "github.com/harshabose/mediapipe"
|
||||
"github.com/harshabose/mediapipe/pkg/consumers"
|
||||
)
|
||||
|
||||
// NO BUFFER IMPLEMENTATION
|
||||
|
||||
type Track struct {
|
||||
consumer consumers.CanConsumePionSamplePacket
|
||||
codecCapability *webrtc.RTPCodecCapability
|
||||
rtpSender *webrtc.RTPSender
|
||||
priority Priority
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateTrack(ctx context.Context, label string, peerConnection *webrtc.PeerConnection, options ...TrackOption) (*Track, error) {
|
||||
track := &Track{ctx: ctx}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(track); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if track.codecCapability == nil {
|
||||
return nil, errors.New("no track capabilities given")
|
||||
}
|
||||
|
||||
consumer, err := webrtc.NewTrackLocalStaticSample(*track.codecCapability, label, "webrtc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
track.consumer = consumer
|
||||
|
||||
if track.rtpSender, err = peerConnection.AddTrack(consumer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go track.rtpSenderLoop()
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (track *Track) GetPriority() Priority {
|
||||
return track.priority
|
||||
}
|
||||
|
||||
func (track *Track) rtpSenderLoop() {
|
||||
// THIS IS NEEDED AS interceptors (pion) doesnt work
|
||||
for {
|
||||
rtcpBuf := make([]byte, 1500)
|
||||
if _, _, err := track.rtpSender.Read(rtcpBuf); err != nil {
|
||||
fmt.Printf("error while reading rtcp packets")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (track *Track) WriteSample(sample media.Sample) error {
|
||||
if err := track.consumer.WriteSample(sample); err != nil {
|
||||
fmt.Printf("error while writing samples to track (id: ); err; %v. Continuing...", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
44
pkg/mediasource/tracks.go
Normal file
44
pkg/mediasource/tracks.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package mediasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type Tracks struct {
|
||||
tracks map[string]*Track
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func CreateTracks(ctx context.Context) *Tracks {
|
||||
return &Tracks{
|
||||
tracks: make(map[string]*Track),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (tracks *Tracks) CreateTrack(label string, peerConnection *webrtc.PeerConnection, options ...TrackOption) (*Track, error) {
|
||||
if _, exists := tracks.tracks[label]; exists {
|
||||
return nil, fmt.Errorf("track with id = '%s' already exists", label)
|
||||
}
|
||||
|
||||
track, err := CreateTrack(tracks.ctx, label, peerConnection, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tracks.tracks[label] = track
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (tracks *Tracks) GetTrack(id string) (*Track, error) {
|
||||
track, exists := tracks.tracks[id]
|
||||
if !exists {
|
||||
return nil, errors.New("track does not exits")
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
229
pkg/transcode/decoder.go
Normal file
229
pkg/transcode/decoder.go
Normal file
@@ -0,0 +1,229 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type GeneralDecoder struct {
|
||||
demuxer CanProduceMediaPacket
|
||||
decoderContext *astiav.CodecContext
|
||||
codec *astiav.Codec
|
||||
buffer buffer.BufferWithGenerator[*astiav.Frame]
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func CreateGeneralDecoder(ctx context.Context, canProduceMediaType CanProduceMediaPacket, options ...DecoderOption) (*GeneralDecoder, error) {
|
||||
var (
|
||||
err error
|
||||
contextOption DecoderOption
|
||||
decoder *GeneralDecoder
|
||||
)
|
||||
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
decoder = &GeneralDecoder{
|
||||
demuxer: canProduceMediaType,
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
canDescribeMediaPacket, ok := canProduceMediaType.(CanDescribeMediaPacket)
|
||||
if !ok {
|
||||
return nil, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
if canDescribeMediaPacket.MediaType() == astiav.MediaTypeVideo {
|
||||
contextOption = withVideoSetDecoderContext(canDescribeMediaPacket)
|
||||
}
|
||||
if canDescribeMediaPacket.MediaType() == astiav.MediaTypeAudio {
|
||||
contextOption = withAudioSetDecoderContext(canDescribeMediaPacket)
|
||||
}
|
||||
|
||||
options = append([]DecoderOption{contextOption}, options...)
|
||||
|
||||
for _, option := range options {
|
||||
if err = option(decoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if decoder.buffer == nil {
|
||||
decoder.buffer = buffer.CreateChannelBuffer(ctx, 256, buffer.CreateFramePool())
|
||||
}
|
||||
|
||||
if err = decoder.decoderContext.Open(decoder.codec, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decoder, nil
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) Ctx() context.Context {
|
||||
return decoder.ctx
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) Start() {
|
||||
go decoder.loop()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) Stop() {
|
||||
decoder.cancel()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) loop() {
|
||||
defer decoder.close()
|
||||
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-decoder.ctx.Done():
|
||||
return
|
||||
default:
|
||||
packet, err := decoder.getPacket()
|
||||
if err != nil {
|
||||
// fmt.Println("unable to get packet from demuxer; err:", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := decoder.decoderContext.SendPacket(packet); err != nil {
|
||||
decoder.demuxer.PutBack(packet)
|
||||
if !errors.Is(err, astiav.ErrEagain) {
|
||||
continue loop1
|
||||
}
|
||||
}
|
||||
loop2:
|
||||
for {
|
||||
frame := decoder.buffer.Generate()
|
||||
if err := decoder.decoderContext.ReceiveFrame(frame); err != nil {
|
||||
decoder.buffer.PutBack(frame)
|
||||
break loop2
|
||||
}
|
||||
|
||||
frame.SetPictureType(astiav.PictureTypeNone)
|
||||
|
||||
if err := decoder.pushFrame(frame); err != nil {
|
||||
decoder.buffer.PutBack(frame)
|
||||
continue loop2
|
||||
}
|
||||
}
|
||||
decoder.demuxer.PutBack(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) pushFrame(frame *astiav.Frame) error {
|
||||
ctx, cancel := context.WithTimeout(decoder.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return decoder.buffer.Push(ctx, frame)
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) getPacket() (*astiav.Packet, error) {
|
||||
ctx, cancel := context.WithTimeout(decoder.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return decoder.demuxer.GetPacket(ctx)
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) GetFrame(ctx context.Context) (*astiav.Frame, error) {
|
||||
return decoder.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) PutBack(frame *astiav.Frame) {
|
||||
decoder.buffer.PutBack(frame)
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) close() {
|
||||
if decoder.decoderContext != nil {
|
||||
decoder.decoderContext.Free()
|
||||
}
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SetBuffer(buffer buffer.BufferWithGenerator[*astiav.Frame]) {
|
||||
decoder.buffer = buffer
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SetCodec(producer CanDescribeMediaPacket) error {
|
||||
if decoder.codec = astiav.FindDecoder(producer.CodecID()); decoder.codec == nil {
|
||||
return ErrorNoCodecFound
|
||||
}
|
||||
decoder.decoderContext = astiav.AllocCodecContext(decoder.codec)
|
||||
if decoder.decoderContext == nil {
|
||||
return ErrorAllocateCodecContext
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) FillContextContent(producer CanDescribeMediaPacket) error {
|
||||
return producer.GetCodecParameters().ToCodecContext(decoder.decoderContext)
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SetFrameRate(producer CanDescribeFrameRate) {
|
||||
decoder.decoderContext.SetFramerate(producer.FrameRate())
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SetTimeBase(producer CanDescribeTimeBase) {
|
||||
decoder.decoderContext.SetTimeBase(producer.TimeBase())
|
||||
}
|
||||
|
||||
// ### IMPLEMENTS CanDescribeMediaVideoFrame
|
||||
|
||||
func (decoder *GeneralDecoder) FrameRate() astiav.Rational {
|
||||
return decoder.decoderContext.Framerate()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) TimeBase() astiav.Rational {
|
||||
return decoder.decoderContext.TimeBase()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) Height() int {
|
||||
return decoder.decoderContext.Height()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) Width() int {
|
||||
return decoder.decoderContext.Width()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) PixelFormat() astiav.PixelFormat {
|
||||
return decoder.decoderContext.PixelFormat()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SampleAspectRatio() astiav.Rational {
|
||||
return decoder.decoderContext.SampleAspectRatio()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) ColorSpace() astiav.ColorSpace {
|
||||
return decoder.decoderContext.ColorSpace()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) ColorRange() astiav.ColorRange {
|
||||
return decoder.decoderContext.ColorRange()
|
||||
}
|
||||
|
||||
// ## CanDescribeMediaAudioFrame
|
||||
|
||||
func (decoder *GeneralDecoder) SampleRate() int {
|
||||
return decoder.decoderContext.SampleRate()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) SampleFormat() astiav.SampleFormat {
|
||||
return decoder.decoderContext.SampleFormat()
|
||||
}
|
||||
|
||||
func (decoder *GeneralDecoder) ChannelLayout() astiav.ChannelLayout {
|
||||
return decoder.decoderContext.ChannelLayout()
|
||||
}
|
||||
|
||||
// ## CanDescribeMediaFrame
|
||||
|
||||
func (decoder *GeneralDecoder) MediaType() astiav.MediaType {
|
||||
return decoder.decoderContext.MediaType()
|
||||
}
|
63
pkg/transcode/decoder_options.go
Normal file
63
pkg/transcode/decoder_options.go
Normal file
@@ -0,0 +1,63 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type DecoderOption = func(decoder Decoder) error
|
||||
|
||||
func withVideoSetDecoderContext(demuxer CanDescribeMediaPacket) DecoderOption {
|
||||
return func(decoder Decoder) error {
|
||||
consumer, ok := decoder.(CanSetMediaPacket)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
if err := consumer.SetCodec(demuxer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := consumer.FillContextContent(demuxer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consumer.SetFrameRate(demuxer)
|
||||
consumer.SetTimeBase(demuxer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func withAudioSetDecoderContext(demuxer CanDescribeMediaPacket) DecoderOption {
|
||||
return func(decoder Decoder) error {
|
||||
consumer, ok := decoder.(CanSetMediaPacket)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
if err := consumer.SetCodec(demuxer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := consumer.FillContextContent(demuxer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consumer.SetTimeBase(demuxer)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithDecoderBuffer(size int, pool buffer.Pool[*astiav.Frame]) DecoderOption {
|
||||
return func(decoder Decoder) error {
|
||||
s, ok := decoder.(CanSetBuffer[*astiav.Frame])
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
s.SetBuffer(buffer.CreateChannelBuffer(decoder.Ctx(), size, pool))
|
||||
return nil
|
||||
}
|
||||
}
|
171
pkg/transcode/demuxer.go
Normal file
171
pkg/transcode/demuxer.go
Normal file
@@ -0,0 +1,171 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type GeneralDemuxer struct {
|
||||
formatContext *astiav.FormatContext
|
||||
inputOptions *astiav.Dictionary
|
||||
inputFormat *astiav.InputFormat
|
||||
stream *astiav.Stream
|
||||
codecParameters *astiav.CodecParameters
|
||||
buffer buffer.BufferWithGenerator[*astiav.Packet]
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func CreateGeneralDemuxer(ctx context.Context, containerAddress string, options ...DemuxerOption) (*GeneralDemuxer, error) {
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
astiav.RegisterAllDevices()
|
||||
demuxer := &GeneralDemuxer{
|
||||
formatContext: astiav.AllocFormatContext(),
|
||||
inputOptions: astiav.NewDictionary(),
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
if demuxer.formatContext == nil {
|
||||
return nil, ErrorAllocateFormatContext
|
||||
}
|
||||
|
||||
if demuxer.inputOptions == nil {
|
||||
return nil, ErrorGeneralAllocate
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(demuxer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := demuxer.formatContext.OpenInput(containerAddress, demuxer.inputFormat, demuxer.inputOptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := demuxer.formatContext.FindStreamInfo(nil); err != nil {
|
||||
return nil, ErrorNoStreamFound
|
||||
}
|
||||
|
||||
for _, stream := range demuxer.formatContext.Streams() {
|
||||
demuxer.stream = stream
|
||||
break
|
||||
}
|
||||
|
||||
if demuxer.stream == nil {
|
||||
return nil, ErrorNoVideoStreamFound
|
||||
}
|
||||
demuxer.codecParameters = demuxer.stream.CodecParameters()
|
||||
|
||||
if demuxer.buffer == nil {
|
||||
demuxer.buffer = buffer.CreateChannelBuffer(ctx, 256, buffer.CreatePacketPool())
|
||||
}
|
||||
|
||||
return demuxer, nil
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) Ctx() context.Context {
|
||||
return demuxer.ctx
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) Start() {
|
||||
go demuxer.loop()
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) Stop() {
|
||||
demuxer.cancel()
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) loop() {
|
||||
defer demuxer.close()
|
||||
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-demuxer.ctx.Done():
|
||||
return
|
||||
default:
|
||||
loop2:
|
||||
for {
|
||||
packet := demuxer.buffer.Generate()
|
||||
|
||||
if err := demuxer.formatContext.ReadFrame(packet); err != nil {
|
||||
demuxer.buffer.PutBack(packet)
|
||||
continue loop1
|
||||
}
|
||||
|
||||
if packet.StreamIndex() != demuxer.stream.Index() {
|
||||
demuxer.buffer.PutBack(packet)
|
||||
continue loop2
|
||||
}
|
||||
|
||||
if err := demuxer.pushPacket(packet); err != nil {
|
||||
demuxer.buffer.PutBack(packet)
|
||||
continue loop1
|
||||
}
|
||||
break loop2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) pushPacket(packet *astiav.Packet) error {
|
||||
ctx, cancel := context.WithTimeout(demuxer.ctx, 50*time.Millisecond) // TODO: NEEDS TO BE BASED ON FPS ON INPUT_FORMAT
|
||||
defer cancel()
|
||||
|
||||
return demuxer.buffer.Push(ctx, packet)
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) GetPacket(ctx context.Context) (*astiav.Packet, error) {
|
||||
return demuxer.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) PutBack(packet *astiav.Packet) {
|
||||
demuxer.buffer.PutBack(packet)
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) close() {
|
||||
if demuxer.formatContext != nil {
|
||||
demuxer.formatContext.CloseInput()
|
||||
demuxer.formatContext.Free()
|
||||
}
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) SetInputOption(key, value string, flags astiav.DictionaryFlags) error {
|
||||
return demuxer.inputOptions.Set(key, value, flags)
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) SetInputFormat(format *astiav.InputFormat) {
|
||||
demuxer.inputFormat = format
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) SetBuffer(buffer buffer.BufferWithGenerator[*astiav.Packet]) {
|
||||
demuxer.buffer = buffer
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) GetCodecParameters() *astiav.CodecParameters {
|
||||
return demuxer.codecParameters
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) MediaType() astiav.MediaType {
|
||||
return demuxer.codecParameters.MediaType()
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) CodecID() astiav.CodecID {
|
||||
return demuxer.codecParameters.CodecID()
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) FrameRate() astiav.Rational {
|
||||
return demuxer.formatContext.GuessFrameRate(demuxer.stream, nil)
|
||||
}
|
||||
|
||||
func (demuxer *GeneralDemuxer) TimeBase() astiav.Rational {
|
||||
return demuxer.stream.TimeBase()
|
||||
}
|
98
pkg/transcode/demuxer_options.go
Normal file
98
pkg/transcode/demuxer_options.go
Normal file
@@ -0,0 +1,98 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type DemuxerOption = func(demuxer Demuxer) error
|
||||
|
||||
func WithRTSPInputOption(demuxer Demuxer) error {
|
||||
s, ok := demuxer.(CanSetDemuxerInputOption)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
if err := s.SetInputOption("rtsp_transport", "tcp", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetInputOption("stimeout", "5000000", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetInputOption("fflags", "nobuffer", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetInputOption("flags", "low_delay", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.SetInputOption("reorder_queue_size", "0", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithFileInputOption(demuxer Demuxer) error {
|
||||
s, ok := demuxer.(CanSetDemuxerInputOption)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
if err := s.SetInputOption("re", "", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
// // Additional options for smooth playback
|
||||
// if err := demuxer.inputOptions.SetInputOption("fflags", "+genpts", 0); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithAlsaInputFormatOption(demuxer Demuxer) error {
|
||||
s, ok := demuxer.(CanSetDemuxerInputFormat)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
s.SetInputFormat(astiav.FindInputFormat("alsa"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithAvFoundationInputFormatOption(demuxer Demuxer) error {
|
||||
setInputFormat, ok := demuxer.(CanSetDemuxerInputFormat)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
setInputFormat.SetInputFormat(astiav.FindInputFormat("avfoundation"))
|
||||
|
||||
setInputOption, ok := demuxer.(CanSetDemuxerInputOption)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
if err := setInputOption.SetInputOption("video_size", "1280x720", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setInputOption.SetInputOption("framerate", "30", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setInputOption.SetInputOption("pixel_format", "uyvy422", 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithDemuxerBuffer(size int, pool buffer.Pool[*astiav.Packet]) DemuxerOption {
|
||||
return func(demuxer Demuxer) error {
|
||||
s, ok := demuxer.(CanSetBuffer[*astiav.Packet])
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
s.SetBuffer(buffer.CreateChannelBuffer(demuxer.Ctx(), size, pool))
|
||||
return nil
|
||||
}
|
||||
}
|
226
pkg/transcode/encoder.go
Normal file
226
pkg/transcode/encoder.go
Normal file
@@ -0,0 +1,226 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type GeneralEncoder struct {
|
||||
buffer buffer.BufferWithGenerator[*astiav.Packet]
|
||||
producer CanProduceMediaFrame
|
||||
codec *astiav.Codec
|
||||
encoderContext *astiav.CodecContext
|
||||
codecFlags *astiav.Dictionary
|
||||
encoderSettings codecSettings
|
||||
sps []byte
|
||||
pps []byte
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func CreateGeneralEncoder(ctx context.Context, codecID astiav.CodecID, canProduceMediaFrame CanProduceMediaFrame, options ...EncoderOption) (*GeneralEncoder, error) {
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
encoder := &GeneralEncoder{
|
||||
producer: canProduceMediaFrame,
|
||||
codecFlags: astiav.NewDictionary(),
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
encoder.codec = astiav.FindEncoder(codecID)
|
||||
if encoder.encoderContext = astiav.AllocCodecContext(encoder.codec); encoder.encoderContext == nil {
|
||||
return nil, ErrorAllocateCodecContext
|
||||
}
|
||||
|
||||
canDescribeMediaFrame, ok := canProduceMediaFrame.(CanDescribeMediaFrame)
|
||||
if !ok {
|
||||
return nil, ErrorInterfaceMismatch
|
||||
}
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeAudio {
|
||||
withAudioSetEncoderContextParameters(canDescribeMediaFrame, encoder.encoderContext)
|
||||
}
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeVideo {
|
||||
withVideoSetEncoderContextParameter(canDescribeMediaFrame, encoder.encoderContext)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if err := option(encoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if encoder.encoderSettings == nil {
|
||||
fmt.Println("warn: no encoder settings are provided")
|
||||
}
|
||||
|
||||
encoder.encoderContext.SetFlags(astiav.NewCodecContextFlags(astiav.CodecContextFlagGlobalHeader))
|
||||
|
||||
if err := encoder.encoderContext.Open(encoder.codec, encoder.codecFlags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if encoder.buffer == nil {
|
||||
encoder.buffer = buffer.CreateChannelBuffer(ctx2, 256, buffer.CreatePacketPool())
|
||||
}
|
||||
|
||||
encoder.findParameterSets(encoder.encoderContext.ExtraData())
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) Ctx() context.Context {
|
||||
return encoder.ctx
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) Start() {
|
||||
go encoder.loop()
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) GetParameterSets() ([]byte, []byte, error) {
|
||||
encoder.findParameterSets(encoder.encoderContext.ExtraData())
|
||||
return encoder.sps, encoder.pps, nil
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) TimeBase() astiav.Rational {
|
||||
return encoder.encoderContext.TimeBase()
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) loop() {
|
||||
defer encoder.close()
|
||||
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-encoder.ctx.Done():
|
||||
return
|
||||
default:
|
||||
frame, err := encoder.getFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := encoder.encoderContext.SendFrame(frame); err != nil {
|
||||
encoder.producer.PutBack(frame)
|
||||
if !errors.Is(err, astiav.ErrEagain) {
|
||||
continue loop1
|
||||
}
|
||||
}
|
||||
loop2:
|
||||
for {
|
||||
packet := encoder.buffer.Generate()
|
||||
if err = encoder.encoderContext.ReceivePacket(packet); err != nil {
|
||||
encoder.buffer.PutBack(packet)
|
||||
break loop2
|
||||
}
|
||||
|
||||
if err := encoder.pushPacket(packet); err != nil {
|
||||
encoder.buffer.PutBack(packet)
|
||||
continue loop2
|
||||
}
|
||||
}
|
||||
encoder.producer.PutBack(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) getFrame() (*astiav.Frame, error) {
|
||||
ctx, cancel := context.WithTimeout(encoder.ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return encoder.producer.GetFrame(ctx)
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) GetPacket(ctx context.Context) (*astiav.Packet, error) {
|
||||
return encoder.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) pushPacket(packet *astiav.Packet) error {
|
||||
ctx, cancel := context.WithTimeout(encoder.ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return encoder.buffer.Push(ctx, packet)
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) PutBack(packet *astiav.Packet) {
|
||||
encoder.buffer.PutBack(packet)
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) Stop() {
|
||||
encoder.cancel()
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) close() {
|
||||
if encoder.encoderContext != nil {
|
||||
encoder.encoderContext.Free()
|
||||
}
|
||||
|
||||
if encoder.codecFlags != nil {
|
||||
encoder.codecFlags.Free()
|
||||
}
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) findParameterSets(extraData []byte) {
|
||||
if len(extraData) > 0 {
|
||||
// Find the first start code (0x00000001)
|
||||
for i := 0; i < len(extraData)-4; i++ {
|
||||
if extraData[i] == 0 && extraData[i+1] == 0 && extraData[i+2] == 0 && extraData[i+3] == 1 {
|
||||
// Skip start code to get the NAL type
|
||||
nalType := extraData[i+4] & 0x1F
|
||||
|
||||
// Find the next start code or end
|
||||
nextStart := len(extraData)
|
||||
for j := i + 4; j < len(extraData)-4; j++ {
|
||||
if extraData[j] == 0 && extraData[j+1] == 0 && extraData[j+2] == 0 && extraData[j+3] == 1 {
|
||||
nextStart = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nalType == 7 { // SPS
|
||||
encoder.sps = make([]byte, nextStart-i)
|
||||
copy(encoder.sps, extraData[i:nextStart])
|
||||
} else if nalType == 8 { // PPS
|
||||
encoder.pps = make([]byte, len(extraData)-i)
|
||||
copy(encoder.pps, extraData[i:])
|
||||
}
|
||||
|
||||
i = nextStart - 1
|
||||
}
|
||||
}
|
||||
fmt.Println("SPS for current encoder: ", encoder.sps)
|
||||
fmt.Println("\tSPS for current encoder in Base64:", base64.StdEncoding.EncodeToString(encoder.sps))
|
||||
fmt.Println("PPS for current encoder: ", encoder.pps)
|
||||
fmt.Println("\tPPS for current encoder in Base64:", base64.StdEncoding.EncodeToString(encoder.pps))
|
||||
}
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) SetBuffer(buffer buffer.BufferWithGenerator[*astiav.Packet]) {
|
||||
encoder.buffer = buffer
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) SetEncoderCodecSettings(settings codecSettings) error {
|
||||
encoder.encoderSettings = settings
|
||||
return encoder.encoderSettings.ForEach(func(key string, value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return encoder.codecFlags.Set(key, value, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (encoder *GeneralEncoder) GetCurrentBitrate() (int64, error) {
|
||||
g, ok := encoder.encoderSettings.(CanGetCurrentBitrate)
|
||||
if !ok {
|
||||
return 0, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return g.GetCurrentBitrate()
|
||||
}
|
103
pkg/transcode/encoder_builder.go
Normal file
103
pkg/transcode/encoder_builder.go
Normal file
@@ -0,0 +1,103 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type GeneralEncoderBuilder struct {
|
||||
codecID astiav.CodecID
|
||||
bufferSize int
|
||||
pool buffer.Pool[*astiav.Packet]
|
||||
settings codecSettings
|
||||
producer CanProduceMediaFrame
|
||||
}
|
||||
|
||||
func NewEncoderBuilder(codecID astiav.CodecID, settings codecSettings, producer CanProduceMediaFrame, bufferSize int, pool buffer.Pool[*astiav.Packet]) *GeneralEncoderBuilder {
|
||||
return &GeneralEncoderBuilder{
|
||||
bufferSize: bufferSize,
|
||||
pool: pool,
|
||||
codecID: codecID,
|
||||
settings: settings,
|
||||
producer: producer,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *GeneralEncoderBuilder) UpdateBitrate(bps int64) error {
|
||||
s, ok := b.settings.(CanUpdateBitrate)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return s.UpdateBitrate(bps)
|
||||
}
|
||||
|
||||
func (b *GeneralEncoderBuilder) BuildWithProducer(ctx context.Context, producer CanProduceMediaFrame) (Encoder, error) {
|
||||
b.producer = producer
|
||||
return b.Build(ctx)
|
||||
}
|
||||
|
||||
func (b *GeneralEncoderBuilder) Build(ctx context.Context) (Encoder, error) {
|
||||
codec := astiav.FindEncoder(b.codecID)
|
||||
if codec == nil {
|
||||
return nil, ErrorNoCodecFound
|
||||
}
|
||||
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
encoder := &GeneralEncoder{
|
||||
producer: b.producer,
|
||||
codec: codec,
|
||||
codecFlags: astiav.NewDictionary(),
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
encoder.encoderContext = astiav.AllocCodecContext(codec)
|
||||
if encoder.encoderContext == nil {
|
||||
return nil, ErrorAllocateCodecContext
|
||||
}
|
||||
|
||||
canDescribeMediaFrame, ok := encoder.producer.(CanDescribeMediaFrame)
|
||||
if !ok {
|
||||
return nil, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeAudio {
|
||||
withAudioSetEncoderContextParameters(canDescribeMediaFrame, encoder.encoderContext)
|
||||
}
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeVideo {
|
||||
withVideoSetEncoderContextParameter(canDescribeMediaFrame, encoder.encoderContext)
|
||||
}
|
||||
|
||||
if err := encoder.SetEncoderCodecSettings(b.settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := WithEncoderBufferSize(b.bufferSize, b.pool)(encoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder.encoderContext.SetFlags(astiav.NewCodecContextFlags(astiav.CodecContextFlagGlobalHeader))
|
||||
|
||||
if err := encoder.encoderContext.Open(encoder.codec, encoder.codecFlags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder.findParameterSets(encoder.encoderContext.ExtraData())
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
func (b *GeneralEncoderBuilder) GetCurrentBitrate() (int64, error) {
|
||||
g, ok := b.settings.(CanGetCurrentBitrate)
|
||||
if !ok {
|
||||
return 0, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return g.GetCurrentBitrate()
|
||||
}
|
378
pkg/transcode/encoder_options.go
Normal file
378
pkg/transcode/encoder_options.go
Normal file
@@ -0,0 +1,378 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type (
|
||||
EncoderOption = func(encoder Encoder) error
|
||||
)
|
||||
|
||||
type codecSettings interface {
|
||||
ForEach(func(string, string) error) error
|
||||
}
|
||||
|
||||
type X264Opts struct {
|
||||
Bitrate string `x264-opts:"bitrate"`
|
||||
VBVMaxBitrate string `x264-opts:"vbv-maxrate"`
|
||||
VBVBuffer string `x264-opts:"vbv-bufsize"`
|
||||
RateTol string `x264-opts:"ratetol"`
|
||||
SyncLookAhead string `x264-opts:"sync-lookahead"`
|
||||
AnnexB string `x264-opts:"annexb"`
|
||||
}
|
||||
|
||||
func (x264 *X264Opts) ForEach(fn func(string, string) error) error {
|
||||
t := reflect.TypeOf(*x264)
|
||||
v := reflect.ValueOf(*x264)
|
||||
|
||||
// Build a single x264opts string
|
||||
var optParts []string
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get("x264-opts")
|
||||
if tag != "" && v.Field(i).String() != "" {
|
||||
optParts = append(optParts, tag+"="+v.Field(i).String())
|
||||
}
|
||||
}
|
||||
|
||||
// Join all options with colons
|
||||
if len(optParts) > 0 {
|
||||
x264optsValue := strings.Join(optParts, ":")
|
||||
if err := fn("x264opts", x264optsValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Also apply any regular x264 parameters
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get("x264")
|
||||
if tag != "" {
|
||||
if err := fn(tag, v.Field(i).String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x264 *X264Opts) UpdateBitrate(bps int64) error {
|
||||
x264.Bitrate = fmt.Sprintf("%d", bps/1000)
|
||||
x264.VBVMaxBitrate = fmt.Sprintf("%d", (bps/1000)+200)
|
||||
x264.VBVBuffer = fmt.Sprintf("%d", bps/2000)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type X264OpenSettings struct {
|
||||
*X264Opts
|
||||
// RateControl string `x264:"rc"` // not sure; fuck
|
||||
Preset string `x264:"preset"` // exists
|
||||
Tune string `x264:"tune"` // exists
|
||||
Refs string `x264:"refs"` // exists
|
||||
Profile string `x264:"profile"` // exists
|
||||
Level string `x264:"level"` // exists
|
||||
Qmin string `x264:"qmin"` // exists
|
||||
Qmax string `x264:"qmax"` // exists
|
||||
BFrames string `x264:"bf"` // exists
|
||||
BAdapt string `x264:"b_strategy"` // exists
|
||||
NGOP string `x264:"g"` // exists
|
||||
NGOPMin string `x264:"keyint_min"` // exists
|
||||
Scenecut string `x264:"sc_threshold"` // exists
|
||||
IntraRefresh string `x264:"intra-refresh"` // exists
|
||||
LookAhead string `x264:"rc-lookahead"` // exists
|
||||
SlicedThreads string `x264:"slice"` // exists
|
||||
ForceIDR string `x264:"force-idr"` // exists
|
||||
AQMode string `x264:"aq-mode"` // exists
|
||||
AQStrength string `x264:"aq-strength"` // exists
|
||||
MBTree string `x264:"mbtree"` // exists
|
||||
Threads string `x264:"threads"` // exists
|
||||
Aud string `x264:"aud"` // exists
|
||||
}
|
||||
|
||||
func (s *X264OpenSettings) ForEach(fn func(key, value string) error) error {
|
||||
t := reflect.TypeOf(*s)
|
||||
v := reflect.ValueOf(*s)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get("x264")
|
||||
if tag != "" {
|
||||
if err := fn(tag, v.Field(i).String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.X264Opts.ForEach(fn)
|
||||
}
|
||||
|
||||
func (s *X264OpenSettings) UpdateBitrate(bps int64) error {
|
||||
return s.X264Opts.UpdateBitrate(bps)
|
||||
}
|
||||
|
||||
var DefaultX264Settings = X264OpenSettings{
|
||||
X264Opts: &X264Opts{
|
||||
// RateControl: "abr",
|
||||
Bitrate: "4000",
|
||||
VBVMaxBitrate: "5000",
|
||||
VBVBuffer: "8000",
|
||||
RateTol: "1",
|
||||
SyncLookAhead: "1",
|
||||
AnnexB: "1",
|
||||
},
|
||||
Preset: "medium",
|
||||
Tune: "film",
|
||||
Refs: "6",
|
||||
Profile: "high",
|
||||
Level: "auto",
|
||||
Qmin: "18",
|
||||
Qmax: "28",
|
||||
BFrames: "3",
|
||||
BAdapt: "1",
|
||||
NGOP: "250",
|
||||
NGOPMin: "25",
|
||||
Scenecut: "40",
|
||||
IntraRefresh: "0",
|
||||
LookAhead: "40",
|
||||
SlicedThreads: "0",
|
||||
ForceIDR: "0",
|
||||
AQMode: "1",
|
||||
AQStrength: "1.0",
|
||||
MBTree: "1",
|
||||
Threads: "0",
|
||||
Aud: "0",
|
||||
}
|
||||
|
||||
var LowBandwidthX264Settings = X264OpenSettings{
|
||||
X264Opts: &X264Opts{
|
||||
// RateControl: "abr",
|
||||
Bitrate: "1500",
|
||||
VBVMaxBitrate: "1800",
|
||||
VBVBuffer: "3000",
|
||||
RateTol: "0.25",
|
||||
SyncLookAhead: "0",
|
||||
AnnexB: "1",
|
||||
},
|
||||
Preset: "veryfast",
|
||||
Tune: "fastdecode",
|
||||
Refs: "2",
|
||||
Profile: "baseline",
|
||||
Level: "4.1",
|
||||
Qmin: "23",
|
||||
Qmax: "35",
|
||||
BFrames: "0",
|
||||
BAdapt: "0",
|
||||
NGOP: "60",
|
||||
NGOPMin: "30",
|
||||
Scenecut: "30",
|
||||
IntraRefresh: "0",
|
||||
LookAhead: "20",
|
||||
SlicedThreads: "1",
|
||||
ForceIDR: "0",
|
||||
AQMode: "0",
|
||||
AQStrength: "1.2",
|
||||
MBTree: "0",
|
||||
Threads: "0",
|
||||
Aud: "0",
|
||||
}
|
||||
|
||||
var LowLatencyX264Settings = X264OpenSettings{
|
||||
X264Opts: &X264Opts{
|
||||
// RateControl: "abr",
|
||||
Bitrate: "2500",
|
||||
VBVMaxBitrate: "12000",
|
||||
VBVBuffer: "20000",
|
||||
RateTol: "0.5",
|
||||
SyncLookAhead: "0",
|
||||
AnnexB: "1",
|
||||
},
|
||||
Preset: "ultrafast",
|
||||
Tune: "zerolatency",
|
||||
Refs: "1",
|
||||
Profile: "baseline",
|
||||
Level: "4.1",
|
||||
Qmin: "20",
|
||||
Qmax: "32",
|
||||
BFrames: "0",
|
||||
BAdapt: "0",
|
||||
NGOP: "30",
|
||||
NGOPMin: "15",
|
||||
Scenecut: "0",
|
||||
IntraRefresh: "1",
|
||||
LookAhead: "10",
|
||||
SlicedThreads: "1",
|
||||
ForceIDR: "1",
|
||||
AQMode: "0",
|
||||
AQStrength: "0",
|
||||
MBTree: "0",
|
||||
|
||||
Threads: "0",
|
||||
Aud: "1",
|
||||
}
|
||||
|
||||
var HighQualityX264Settings = X264OpenSettings{
|
||||
X264Opts: &X264Opts{
|
||||
// RateControl: "abr",
|
||||
Bitrate: "15000",
|
||||
VBVMaxBitrate: "20000",
|
||||
VBVBuffer: "30000",
|
||||
RateTol: "2.0",
|
||||
SyncLookAhead: "1",
|
||||
AnnexB: "1",
|
||||
},
|
||||
Preset: "slow",
|
||||
Tune: "film",
|
||||
Refs: "8",
|
||||
Profile: "high",
|
||||
Level: "5.1",
|
||||
Qmin: "15",
|
||||
Qmax: "24",
|
||||
BFrames: "5",
|
||||
BAdapt: "2",
|
||||
NGOP: "250",
|
||||
NGOPMin: "30",
|
||||
Scenecut: "80",
|
||||
IntraRefresh: "0",
|
||||
LookAhead: "60",
|
||||
SlicedThreads: "0",
|
||||
ForceIDR: "0",
|
||||
AQMode: "0",
|
||||
AQStrength: "1.3",
|
||||
MBTree: "1",
|
||||
|
||||
Threads: "0",
|
||||
Aud: "0",
|
||||
}
|
||||
|
||||
var WebRTCOptimisedX264Settings = X264OpenSettings{
|
||||
X264Opts: &X264Opts{
|
||||
Bitrate: "800", // Keep your current target
|
||||
VBVMaxBitrate: "900", // Same as target!
|
||||
VBVBuffer: "300", // 2500/30fps ≈ 83 kbits (single frame)
|
||||
RateTol: "0.1", // More tolerance
|
||||
AnnexB: "1", // Already correct
|
||||
},
|
||||
Qmin: "26", // Wider range
|
||||
Qmax: "42", // Much wider range
|
||||
Level: "3.1", // Better compatibility
|
||||
Preset: "ultrafast",
|
||||
Tune: "zerolatency",
|
||||
Profile: "baseline",
|
||||
NGOP: "50",
|
||||
NGOPMin: "25",
|
||||
IntraRefresh: "1",
|
||||
SlicedThreads: "1", // TODO: CHECK THIS
|
||||
// ForceIDR: "1", // TODO: CHECK THIS; MIGHT BE IN CONFLICT WITH IntraRefresh
|
||||
AQMode: "1", // RE-ENABLED AS zerolatency disables this
|
||||
AQStrength: "0.5",
|
||||
|
||||
Threads: "0",
|
||||
Aud: "1",
|
||||
}
|
||||
|
||||
func WithX264DefaultOptions(encoder Encoder) error {
|
||||
return WithCodecSettings(&DefaultX264Settings)(encoder)
|
||||
}
|
||||
|
||||
func WithX264HighQualityOptions(encoder Encoder) error {
|
||||
return WithCodecSettings(&HighQualityX264Settings)(encoder)
|
||||
}
|
||||
|
||||
func WithX264LowLatencyOptions(encoder Encoder) error {
|
||||
return WithCodecSettings(&LowLatencyX264Settings)(encoder)
|
||||
}
|
||||
|
||||
func WithWebRTCOptimisedOptions(encoder Encoder) error {
|
||||
return WithCodecSettings(&WebRTCOptimisedX264Settings)(encoder)
|
||||
}
|
||||
|
||||
func WithCodecSettings(settings codecSettings) EncoderOption {
|
||||
return func(encoder Encoder) error {
|
||||
s, ok := encoder.(CanSetEncoderCodecSettings)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return s.SetEncoderCodecSettings(settings)
|
||||
}
|
||||
}
|
||||
|
||||
func WithX264LowBandwidthOptions(encoder Encoder) error {
|
||||
return WithCodecSettings(&LowBandwidthX264Settings)(encoder)
|
||||
}
|
||||
|
||||
func withAudioSetEncoderContextParameters(filter CanDescribeMediaAudioFrame, eCtx *astiav.CodecContext) {
|
||||
eCtx.SetTimeBase(filter.TimeBase())
|
||||
eCtx.SetSampleRate(filter.SampleRate())
|
||||
eCtx.SetSampleFormat(filter.SampleFormat())
|
||||
eCtx.SetChannelLayout(filter.ChannelLayout())
|
||||
eCtx.SetStrictStdCompliance(-2)
|
||||
}
|
||||
|
||||
func withVideoSetEncoderContextParameter(filter CanDescribeMediaVideoFrame, eCtx *astiav.CodecContext) {
|
||||
eCtx.SetHeight(filter.Height())
|
||||
eCtx.SetWidth(filter.Width())
|
||||
eCtx.SetTimeBase(filter.TimeBase())
|
||||
eCtx.SetPixelFormat(filter.PixelFormat())
|
||||
eCtx.SetFramerate(filter.FrameRate())
|
||||
}
|
||||
|
||||
func WithEncoderBufferSize(size int, pool buffer.Pool[*astiav.Packet]) EncoderOption {
|
||||
return func(encoder Encoder) error {
|
||||
s, ok := encoder.(CanSetBuffer[*astiav.Packet])
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
s.SetBuffer(buffer.CreateChannelBuffer(encoder.Ctx(), size, pool))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// type VP8Settings struct {
|
||||
// Deadline string `vp8:"deadline"` // Real-time encoding
|
||||
// Bitrate string `vp8:"b"` // Target bitrate
|
||||
// MinRate string `vp8:"minrate"` // Minimum bitrate
|
||||
// MaxRate string `vp8:"maxrate"` // Maximum bitrate
|
||||
// BufSize string `vp8:"bufsize"` // Buffer size
|
||||
// CRF string `vp8:"crf"` // Quality setting
|
||||
// CPUUsed string `vp8:"cpu-used"` // Speed preset
|
||||
// }
|
||||
//
|
||||
// var DefaultVP8Settings = VP8Settings{
|
||||
// Deadline: "1", // Real-time
|
||||
// Bitrate: "2500k", // 2.5 Mbps
|
||||
// MinRate: "2000k", // Min 2 Mbps
|
||||
// MaxRate: "3000k", // Max 3 Mbps
|
||||
// BufSize: "500k", // 500kb buffer
|
||||
// CRF: "10", // Good quality
|
||||
// CPUUsed: "8", // Fastest
|
||||
// }
|
||||
//
|
||||
// func (s VP8Settings) ForEach(fn func(key, value string) error) error {
|
||||
// t := reflect.TypeOf(s)
|
||||
// v := reflect.ValueOf(s)
|
||||
//
|
||||
// for i := 0; i < t.NumField(); i++ {
|
||||
// field := t.Field(i)
|
||||
// tag := field.Tag.Get("vp8")
|
||||
// if tag != "" {
|
||||
// if err := fn(tag, v.Field(i).String()); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
29
pkg/transcode/errors.go
Normal file
29
pkg/transcode/errors.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrorAllocateFormatContext = errors.New("error allocate format context")
|
||||
ErrorOpenInputContainer = errors.New("error opening container")
|
||||
ErrorNoStreamFound = errors.New("error no stream found")
|
||||
ErrorGeneralAllocate = errors.New("error allocating general object")
|
||||
ErrorNoVideoStreamFound = errors.New("no video stream found")
|
||||
ErrorInterfaceMismatch = errors.New("interface mismatch")
|
||||
|
||||
ErrorNoCodecFound = errors.New("error no codec found")
|
||||
ErrorAllocateCodecContext = errors.New("error allocating codec context")
|
||||
ErrorFillCodecContext = errors.New("error filling the codec context")
|
||||
|
||||
ErrorNoFilterName = errors.New("error filter name does not exists")
|
||||
WarnNoFilterContent = errors.New("content is empty. no filtering will be done")
|
||||
ErrorGraphParse = errors.New("error parsing the filter graph")
|
||||
ErrorGraphConfigure = errors.New("error configuring the filter graph")
|
||||
ErrorSrcContextSetParameter = errors.New("error while setting parameters to source context")
|
||||
ErrorSrcContextInitialise = errors.New("error initialising the source context")
|
||||
ErrorAllocSrcContext = errors.New("error setting source context")
|
||||
ErrorAllocSinkContext = errors.New("error setting sink context")
|
||||
|
||||
ErrorCodecNoSetting = errors.New("error no settings given")
|
||||
)
|
304
pkg/transcode/filter.go
Normal file
304
pkg/transcode/filter.go
Normal file
@@ -0,0 +1,304 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type GeneralFilter struct {
|
||||
content string
|
||||
decoder CanProduceMediaFrame
|
||||
buffer buffer.BufferWithGenerator[*astiav.Frame]
|
||||
graph *astiav.FilterGraph
|
||||
input *astiav.FilterInOut
|
||||
output *astiav.FilterInOut
|
||||
srcContext *astiav.BuffersrcFilterContext
|
||||
sinkContext *astiav.BuffersinkFilterContext
|
||||
srcContextParams *astiav.BuffersrcFilterContextParameters // NOTE: THIS BECOMES NIL AFTER INITIALISATION
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func CreateGeneralFilter(ctx context.Context, canProduceMediaFrame CanProduceMediaFrame, filterConfig FilterConfig, options ...FilterOption) (*GeneralFilter, error) {
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
filter := &GeneralFilter{
|
||||
graph: astiav.AllocFilterGraph(),
|
||||
decoder: canProduceMediaFrame,
|
||||
input: astiav.AllocFilterInOut(),
|
||||
output: astiav.AllocFilterInOut(),
|
||||
srcContextParams: astiav.AllocBuffersrcFilterContextParameters(),
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
// TODO: CHECK IF ALL ATTRIBUTES ARE ALLOCATED PROPERLY
|
||||
|
||||
filterSrc := astiav.FindFilterByName(filterConfig.Source.String())
|
||||
if filterSrc == nil {
|
||||
return nil, ErrorNoFilterName
|
||||
}
|
||||
|
||||
filterSink := astiav.FindFilterByName(filterConfig.Sink.String())
|
||||
if filterSink == nil {
|
||||
return nil, ErrorNoFilterName
|
||||
}
|
||||
|
||||
srcContext, err := filter.graph.NewBuffersrcFilterContext(filterSrc, "in")
|
||||
if err != nil {
|
||||
return nil, ErrorAllocSrcContext
|
||||
}
|
||||
filter.srcContext = srcContext
|
||||
|
||||
sinkContext, err := filter.graph.NewBuffersinkFilterContext(filterSink, "out")
|
||||
if err != nil {
|
||||
return nil, ErrorAllocSinkContext
|
||||
}
|
||||
filter.sinkContext = sinkContext
|
||||
|
||||
canDescribeMediaFrame, ok := canProduceMediaFrame.(CanDescribeMediaFrame)
|
||||
if !ok {
|
||||
return nil, ErrorInterfaceMismatch
|
||||
}
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeVideo {
|
||||
options = append([]FilterOption{withVideoSetFilterContextParameters(canDescribeMediaFrame)}, options...)
|
||||
}
|
||||
if canDescribeMediaFrame.MediaType() == astiav.MediaTypeAudio {
|
||||
options = append([]FilterOption{withAudioSetFilterContextParameters(canDescribeMediaFrame)}, options...)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if err = option(filter); err != nil {
|
||||
// TODO: SET CONTENT HERE
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if filter.buffer == nil {
|
||||
filter.buffer = buffer.CreateChannelBuffer(ctx, 256, buffer.CreateFramePool())
|
||||
}
|
||||
|
||||
if err = filter.srcContext.SetParameters(filter.srcContextParams); err != nil {
|
||||
return nil, ErrorSrcContextSetParameter
|
||||
}
|
||||
|
||||
if err = filter.srcContext.Initialize(astiav.NewDictionary()); err != nil {
|
||||
return nil, ErrorSrcContextInitialise
|
||||
}
|
||||
|
||||
filter.output.SetName("in")
|
||||
filter.output.SetFilterContext(filter.srcContext.FilterContext())
|
||||
filter.output.SetPadIdx(0)
|
||||
filter.output.SetNext(nil)
|
||||
|
||||
filter.input.SetName("out")
|
||||
filter.input.SetFilterContext(filter.sinkContext.FilterContext())
|
||||
filter.input.SetPadIdx(0)
|
||||
filter.input.SetNext(nil)
|
||||
|
||||
if filter.content == "" {
|
||||
fmt.Println(WarnNoFilterContent)
|
||||
}
|
||||
|
||||
if err = filter.graph.Parse(filter.content, filter.input, filter.output); err != nil {
|
||||
return nil, ErrorGraphParse
|
||||
}
|
||||
|
||||
if err = filter.graph.Configure(); err != nil {
|
||||
return nil, ErrorGraphConfigure
|
||||
}
|
||||
|
||||
if filter.srcContextParams != nil {
|
||||
filter.srcContextParams.Free()
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) Ctx() context.Context {
|
||||
return filter.ctx
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) Start() {
|
||||
go filter.loop()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) Stop() {
|
||||
filter.cancel()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) loop() {
|
||||
defer filter.close()
|
||||
|
||||
loop1:
|
||||
for {
|
||||
select {
|
||||
case <-filter.ctx.Done():
|
||||
return
|
||||
default:
|
||||
srcFrame, err := filter.getFrame()
|
||||
if err != nil {
|
||||
// fmt.Println("unable to get frame from decoder; err:", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := filter.srcContext.AddFrame(srcFrame, astiav.NewBuffersrcFlags(astiav.BuffersrcFlagKeepRef)); err != nil {
|
||||
filter.buffer.PutBack(srcFrame)
|
||||
continue loop1
|
||||
}
|
||||
loop2:
|
||||
for {
|
||||
sinkFrame := filter.buffer.Generate()
|
||||
if err = filter.sinkContext.GetFrame(sinkFrame, astiav.NewBuffersinkFlags()); err != nil {
|
||||
filter.buffer.PutBack(sinkFrame)
|
||||
break loop2
|
||||
}
|
||||
|
||||
if err := filter.pushFrame(sinkFrame); err != nil {
|
||||
filter.buffer.PutBack(sinkFrame)
|
||||
continue loop2
|
||||
}
|
||||
}
|
||||
filter.decoder.PutBack(srcFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) pushFrame(frame *astiav.Frame) error {
|
||||
ctx, cancel := context.WithTimeout(filter.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return filter.buffer.Push(ctx, frame)
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) getFrame() (*astiav.Frame, error) {
|
||||
ctx, cancel := context.WithTimeout(filter.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return filter.decoder.GetFrame(ctx)
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) PutBack(frame *astiav.Frame) {
|
||||
filter.buffer.PutBack(frame)
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) GetFrame(ctx context.Context) (*astiav.Frame, error) {
|
||||
return filter.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) close() {
|
||||
if filter.graph != nil {
|
||||
filter.graph.Free()
|
||||
}
|
||||
if filter.input != nil {
|
||||
filter.input.Free()
|
||||
}
|
||||
if filter.output != nil {
|
||||
filter.output.Free()
|
||||
}
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetBuffer(buffer buffer.BufferWithGenerator[*astiav.Frame]) {
|
||||
filter.buffer = buffer
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) AddToFilterContent(content string) {
|
||||
filter.content += content
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetFrameRate(describe CanDescribeFrameRate) {
|
||||
filter.srcContextParams.SetFramerate(describe.FrameRate())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetTimeBase(describe CanDescribeTimeBase) {
|
||||
filter.srcContextParams.SetTimeBase(describe.TimeBase())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetHeight(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetHeight(describe.Height())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetWidth(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetWidth(describe.Width())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetPixelFormat(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetPixelFormat(describe.PixelFormat())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetSampleAspectRatio(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetSampleAspectRatio(describe.SampleAspectRatio())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetColorSpace(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetColorSpace(describe.ColorSpace())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetColorRange(describe CanDescribeMediaVideoFrame) {
|
||||
filter.srcContextParams.SetColorRange(describe.ColorRange())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetSampleRate(describe CanDescribeMediaAudioFrame) {
|
||||
filter.srcContextParams.SetSampleRate(describe.SampleRate())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetSampleFormat(describe CanDescribeMediaAudioFrame) {
|
||||
filter.srcContextParams.SetSampleFormat(describe.SampleFormat())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SetChannelLayout(describe CanDescribeMediaAudioFrame) {
|
||||
filter.srcContextParams.SetChannelLayout(describe.ChannelLayout())
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) MediaType() astiav.MediaType {
|
||||
return filter.sinkContext.MediaType()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) FrameRate() astiav.Rational {
|
||||
return filter.sinkContext.FrameRate()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) TimeBase() astiav.Rational {
|
||||
return filter.sinkContext.TimeBase()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) Height() int {
|
||||
return filter.sinkContext.Height()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) Width() int {
|
||||
return filter.sinkContext.Width()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) PixelFormat() astiav.PixelFormat {
|
||||
return filter.sinkContext.PixelFormat()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SampleAspectRatio() astiav.Rational {
|
||||
return filter.sinkContext.SampleAspectRatio()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) ColorSpace() astiav.ColorSpace {
|
||||
return filter.sinkContext.ColorSpace()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) ColorRange() astiav.ColorRange {
|
||||
return filter.sinkContext.ColorRange()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SampleRate() int {
|
||||
return filter.sinkContext.SampleRate()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) SampleFormat() astiav.SampleFormat {
|
||||
return filter.sinkContext.SampleFormat()
|
||||
}
|
||||
|
||||
func (filter *GeneralFilter) ChannelLayout() astiav.ChannelLayout {
|
||||
return filter.sinkContext.ChannelLayout()
|
||||
}
|
304
pkg/transcode/filter_options.go
Normal file
304
pkg/transcode/filter_options.go
Normal file
@@ -0,0 +1,304 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type (
|
||||
FilterOption func(Filter) error
|
||||
Name string
|
||||
)
|
||||
|
||||
func (f Name) String() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
type FilterConfig struct {
|
||||
Source Name
|
||||
Sink Name
|
||||
}
|
||||
|
||||
const (
|
||||
videoBufferFilterName Name = "buffer"
|
||||
videoBufferSinkFilterName Name = "buffersink"
|
||||
audioBufferFilterName Name = "abuffer"
|
||||
audioBufferSinkFilterName Name = "abuffersink"
|
||||
)
|
||||
|
||||
var (
|
||||
VideoFilters = FilterConfig{
|
||||
Source: videoBufferFilterName,
|
||||
Sink: videoBufferSinkFilterName,
|
||||
}
|
||||
AudioFilters = FilterConfig{
|
||||
Source: audioBufferFilterName,
|
||||
Sink: audioBufferSinkFilterName,
|
||||
}
|
||||
)
|
||||
|
||||
func WithFilterBuffer(size int, pool buffer.Pool[*astiav.Frame]) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
s, ok := filter.(CanSetBuffer[*astiav.Frame])
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
s.SetBuffer(buffer.CreateChannelBuffer(filter.Ctx(), size, pool))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func withVideoSetFilterContextParameters(decoder CanDescribeMediaVideoFrame) func(Filter) error {
|
||||
return func(filter Filter) error {
|
||||
canSetMediaVideoFrame, ok := filter.(CanSetMediaVideoFrame)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
canSetMediaVideoFrame.SetFrameRate(decoder)
|
||||
canSetMediaVideoFrame.SetHeight(decoder)
|
||||
canSetMediaVideoFrame.SetPixelFormat(decoder)
|
||||
canSetMediaVideoFrame.SetSampleAspectRatio(decoder)
|
||||
canSetMediaVideoFrame.SetTimeBase(decoder)
|
||||
canSetMediaVideoFrame.SetWidth(decoder)
|
||||
|
||||
canSetMediaVideoFrame.SetColorSpace(decoder)
|
||||
canSetMediaVideoFrame.SetColorRange(decoder)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVideoScaleFilterContent(width, height uint16) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("scale=%d:%d,", width, height))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVideoPixelFormatFilterContent(pixelFormat astiav.PixelFormat) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
a.AddToFilterContent(fmt.Sprintf("format=pix_fmts=%s,", pixelFormat))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithVideoFPSFilterContent(fps uint8) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("fps=%d,", fps))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
func withAudioSetFilterContextParameters(decoder CanDescribeMediaAudioFrame) func(Filter) error {
|
||||
return func(filter Filter) error {
|
||||
canSetMediaAudioFrame, ok := filter.(CanSetMediaAudioFrame)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
canSetMediaAudioFrame.SetChannelLayout(decoder)
|
||||
canSetMediaAudioFrame.SetSampleFormat(decoder)
|
||||
canSetMediaAudioFrame.SetSampleRate(decoder)
|
||||
canSetMediaAudioFrame.SetTimeBase(decoder)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioSampleFormatChannelLayoutFilter(sampleFormat astiav.SampleFormat, channelLayout astiav.ChannelLayout) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("aformat=sample_fmts=%s:channel_layouts=%s", sampleFormat.String(), channelLayout.String()) + ",")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioSampleRateFilter(samplerate uint32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("aresample=%d,", samplerate))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioSamplesPerFrameContent(nsamples uint16) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("asetnsamples=%d,", nsamples))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioCompressionContent(threshold int, ratio int, attack float64, release float64) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// NOTE: DYNAMIC RANGE COMPRESSION TO HANDLE SUDDEN VOLUME CHANGES
|
||||
// Possible values 'acompressor=threshold=-12dB:ratio=2:attack=0.05:release=0.2" // MOST POPULAR VALUES
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("acompressor=threshold=%ddB:ratio=%d:attack=%.2f:release=%.2f,",
|
||||
threshold, ratio, attack, release))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioHighPassFilterContent(id string, frequency float32, order uint8) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// NOTE: HIGH-PASS FILTER TO REMOVE WIND NOISE AND TURBULENCE
|
||||
// NOTE: 120HZ CUTOFF MIGHT PRESERVE VOICE WHILE REMOVING LOW RUMBLE; BUT MORE TESTING IS NEEDED
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("highpass@%s=frequency=%.2f:poles=%d", id, frequency, order))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioLowPassFilterContent(id string, frequency float32, order uint8) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("lowpass@%s=frequency=%.2f:poles=%d", id, frequency, order))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioNotchFilterContent(id string, frequency float32, qFactor float32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("bandreject@%s=frequency=%.2f:width_type=q:width=%.2f", id, frequency, qFactor))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioNotchHarmonicsFilterContent(id string, fundamental float32, harmonics uint8, qFactor float32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
var filters = make([]string, 0)
|
||||
|
||||
for i := uint8(0); i < harmonics; i++ {
|
||||
harmonic := fundamental * float32(i+1)
|
||||
filters = append(filters, fmt.Sprintf("bandreject@%s%d=frequency=%.2f:width_type=q:width=%.2f", id, i, harmonic, qFactor))
|
||||
}
|
||||
|
||||
a.AddToFilterContent(strings.Join(filters, ",") + ",")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioEqualiserFilter(id string, frequency float32, width float32, gain float32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// NOTE: EQUALISER CAN BE USED TO ENHANCE SPEECH BANDWIDTH (300 - 3kHz). MORE RESEARCH NEEDS TO DONE
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("equalizer@%s=frequency=%.2f:width_type=h:width=%.2f:gain=%.2f,", id, frequency, width, gain))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioSilenceGateContent(threshold int, range_ int, attack float64, release float64) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// NOTE: IF EVERYTHING WORKS, WE SHOULD HAVE LIGHT NOISE WHICH CAN BE CONSIDERED AS SILENCE. THIS GATE REMOVES SILENCE
|
||||
// NOTE: POSSIBLE VALUES 'agate=threshold=-30dB:range=-30dB:attack=0.01:release=0.1" // MOST POPULAR; MORE TESTING IS NEEDED
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("agate=threshold=%ddB:range=%ddB:attack=%.2f:release=%.2f,",
|
||||
threshold, range_, attack, release))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithAudioLoudnessNormaliseContent(intensity int, truePeak float64, range_ int) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// NOTE: NORMALISES THE FINAL AUDIO. MUST BE CALLED AT THE END
|
||||
// NOTE: POSSIBLE VALUES "loudnorm=I=-16:TP=-1.5:LRA=11" // MOST POPULAR
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("loudnorm=I=%d:TP=%.1f:LRA=%d",
|
||||
intensity, truePeak, range_))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithFFTBroadBandNoiseFilter(id string, strength float32, rPatch float32, rSearch float32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
// TODO: NEEDS A UPDATOR TO CONTROL NOISE SAMPLING
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf(""))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMeanBroadBandNoiseFilter(id string, strength float32, rPatch float32, rSearch float32) FilterOption {
|
||||
return func(filter Filter) error {
|
||||
a, ok := filter.(CanAddToFilterContent)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
a.AddToFilterContent(fmt.Sprintf("anlmdn@%s=strength=%.2f:patch=%.2f:research=%.2f", id, strength, rPatch, rSearch))
|
||||
return nil
|
||||
}
|
||||
}
|
165
pkg/transcode/interfaces.go
Normal file
165
pkg/transcode/interfaces.go
Normal file
@@ -0,0 +1,165 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type CanSetDemuxerInputOption interface {
|
||||
SetInputOption(key, value string, flags astiav.DictionaryFlags) error
|
||||
}
|
||||
|
||||
type CanSetDemuxerInputFormat interface {
|
||||
SetInputFormat(*astiav.InputFormat)
|
||||
}
|
||||
|
||||
type CanSetBuffer[T any] interface {
|
||||
SetBuffer(buffer buffer.BufferWithGenerator[T])
|
||||
}
|
||||
|
||||
type CanDescribeFrameRate interface {
|
||||
FrameRate() astiav.Rational
|
||||
}
|
||||
|
||||
type CanDescribeTimeBase interface {
|
||||
TimeBase() astiav.Rational
|
||||
}
|
||||
|
||||
type CanSetFrameRate interface {
|
||||
SetFrameRate(CanDescribeFrameRate)
|
||||
}
|
||||
|
||||
type CanSetTimeBase interface {
|
||||
SetTimeBase(CanDescribeTimeBase)
|
||||
}
|
||||
|
||||
type CanDescribeMediaPacket interface {
|
||||
MediaType() astiav.MediaType
|
||||
CodecID() astiav.CodecID
|
||||
GetCodecParameters() *astiav.CodecParameters
|
||||
CanDescribeFrameRate
|
||||
CanDescribeTimeBase
|
||||
}
|
||||
|
||||
type CanProduceMediaPacket interface {
|
||||
GetPacket(ctx context.Context) (*astiav.Packet, error)
|
||||
PutBack(*astiav.Packet)
|
||||
}
|
||||
|
||||
type CanProduceMediaFrame interface {
|
||||
GetFrame(ctx context.Context) (*astiav.Frame, error)
|
||||
PutBack(*astiav.Frame)
|
||||
}
|
||||
|
||||
type CanDescribeMediaVideoFrame interface {
|
||||
CanDescribeFrameRate
|
||||
CanDescribeTimeBase
|
||||
Height() int
|
||||
Width() int
|
||||
PixelFormat() astiav.PixelFormat
|
||||
SampleAspectRatio() astiav.Rational
|
||||
ColorSpace() astiav.ColorSpace
|
||||
ColorRange() astiav.ColorRange
|
||||
}
|
||||
|
||||
type CanSetMediaVideoFrame interface {
|
||||
CanSetFrameRate
|
||||
CanSetTimeBase
|
||||
SetHeight(CanDescribeMediaVideoFrame)
|
||||
SetWidth(CanDescribeMediaVideoFrame)
|
||||
SetPixelFormat(CanDescribeMediaVideoFrame)
|
||||
SetSampleAspectRatio(CanDescribeMediaVideoFrame)
|
||||
SetColorSpace(CanDescribeMediaVideoFrame)
|
||||
SetColorRange(CanDescribeMediaVideoFrame)
|
||||
}
|
||||
|
||||
type CanDescribeMediaFrame interface {
|
||||
MediaType() astiav.MediaType
|
||||
CanDescribeMediaVideoFrame
|
||||
CanDescribeMediaAudioFrame
|
||||
}
|
||||
|
||||
type CanSetMediaAudioFrame interface {
|
||||
CanSetTimeBase
|
||||
SetSampleRate(CanDescribeMediaAudioFrame)
|
||||
SetSampleFormat(CanDescribeMediaAudioFrame)
|
||||
SetChannelLayout(CanDescribeMediaAudioFrame)
|
||||
}
|
||||
|
||||
type CanDescribeMediaAudioFrame interface {
|
||||
CanDescribeTimeBase
|
||||
SampleRate() int
|
||||
SampleFormat() astiav.SampleFormat
|
||||
ChannelLayout() astiav.ChannelLayout
|
||||
}
|
||||
|
||||
type CanSetMediaPacket interface {
|
||||
FillContextContent(CanDescribeMediaPacket) error
|
||||
SetCodec(CanDescribeMediaPacket) error
|
||||
CanSetFrameRate
|
||||
CanSetTimeBase
|
||||
}
|
||||
|
||||
type Demuxer interface {
|
||||
Ctx() context.Context
|
||||
Start()
|
||||
Stop()
|
||||
CanProduceMediaPacket
|
||||
}
|
||||
|
||||
type Decoder interface {
|
||||
Ctx() context.Context
|
||||
Start()
|
||||
Stop()
|
||||
CanProduceMediaFrame
|
||||
}
|
||||
|
||||
type CanAddToFilterContent interface {
|
||||
AddToFilterContent(string)
|
||||
}
|
||||
|
||||
type Filter interface {
|
||||
Ctx() context.Context
|
||||
Start()
|
||||
Stop()
|
||||
CanProduceMediaFrame
|
||||
}
|
||||
|
||||
type CanPauseUnPauseEncoder interface {
|
||||
PauseEncoding() error
|
||||
UnPauseEncoding() error
|
||||
}
|
||||
|
||||
type CanGetParameterSets interface {
|
||||
GetParameterSets() (sps, pps []byte, err error)
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
Ctx() context.Context
|
||||
Start()
|
||||
Stop()
|
||||
CanProduceMediaPacket
|
||||
}
|
||||
|
||||
type CanSetEncoderCodecSettings interface {
|
||||
SetEncoderCodecSettings(codecSettings) error
|
||||
}
|
||||
|
||||
type CanUpdateBitrate interface {
|
||||
UpdateBitrate(int64) error
|
||||
}
|
||||
|
||||
type CanGetCurrentBitrate interface {
|
||||
GetCurrentBitrate() (int64, error)
|
||||
}
|
||||
|
||||
type UpdateBitrateCallBack func(bps int64) error
|
||||
|
||||
type CanGetUpdateBitrateCallBack interface {
|
||||
OnUpdateBitrate() UpdateBitrateCallBack
|
||||
}
|
315
pkg/transcode/multi_encoder.go
Normal file
315
pkg/transcode/multi_encoder.go
Normal file
@@ -0,0 +1,315 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type MultiConfig struct {
|
||||
Steps uint8
|
||||
UpdateConfig
|
||||
}
|
||||
|
||||
func (c MultiConfig) validate() error {
|
||||
if c.Steps == 0 {
|
||||
return fmt.Errorf("steps need be more than 0")
|
||||
}
|
||||
return c.UpdateConfig.validate()
|
||||
}
|
||||
|
||||
func (c MultiConfig) getBitrates() []int64 {
|
||||
bitrates := make([]int64, c.Steps)
|
||||
|
||||
if c.Steps == 1 {
|
||||
bitrates[0] = c.MaxBitrate
|
||||
} else {
|
||||
step := float64(c.MaxBitrate-c.MinBitrate) / float64(c.Steps-1)
|
||||
for i := uint8(0); i < c.Steps; i++ {
|
||||
bitrates[i] = c.MinBitrate + int64(float64(i)*step)
|
||||
}
|
||||
}
|
||||
|
||||
return bitrates
|
||||
}
|
||||
|
||||
func NewMultiConfig(minBitrate, maxBitrate int64, steps uint8) MultiConfig {
|
||||
c := MultiConfig{
|
||||
UpdateConfig: UpdateConfig{
|
||||
MaxBitrate: maxBitrate,
|
||||
MinBitrate: minBitrate,
|
||||
},
|
||||
Steps: steps,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type dummyMediaFrameProducer struct {
|
||||
buffer buffer.BufferWithGenerator[*astiav.Frame]
|
||||
CanDescribeMediaFrame
|
||||
}
|
||||
|
||||
func newDummyMediaFrameProducer(buffer buffer.BufferWithGenerator[*astiav.Frame], describer CanDescribeMediaFrame) *dummyMediaFrameProducer {
|
||||
return &dummyMediaFrameProducer{
|
||||
buffer: buffer,
|
||||
CanDescribeMediaFrame: describer,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dummyMediaFrameProducer) pushFrame(ctx context.Context, frame *astiav.Frame) error {
|
||||
return p.buffer.Push(ctx, frame)
|
||||
}
|
||||
|
||||
func (p *dummyMediaFrameProducer) GetFrame(ctx context.Context) (*astiav.Frame, error) {
|
||||
return p.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (p *dummyMediaFrameProducer) Generate() *astiav.Frame {
|
||||
return p.buffer.Generate()
|
||||
}
|
||||
|
||||
func (p *dummyMediaFrameProducer) PutBack(frame *astiav.Frame) {
|
||||
p.buffer.PutBack(frame)
|
||||
}
|
||||
|
||||
type splitEncoder struct {
|
||||
encoder *GeneralEncoder
|
||||
producer *dummyMediaFrameProducer
|
||||
}
|
||||
|
||||
func newSplitEncoder(encoder *GeneralEncoder, producer *dummyMediaFrameProducer) *splitEncoder {
|
||||
return &splitEncoder{
|
||||
encoder: encoder,
|
||||
producer: producer,
|
||||
}
|
||||
}
|
||||
|
||||
type MultiUpdateEncoder struct {
|
||||
encoders []*splitEncoder
|
||||
active atomic.Pointer[splitEncoder]
|
||||
config MultiConfig
|
||||
bitrates []int64
|
||||
producer CanProduceMediaFrame
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
paused atomic.Bool
|
||||
resume chan struct{}
|
||||
pauseMux sync.Mutex
|
||||
}
|
||||
|
||||
func NewMultiUpdateEncoder(ctx context.Context, config MultiConfig, builder *GeneralEncoderBuilder) (*MultiUpdateEncoder, error) {
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
encoder := &MultiUpdateEncoder{
|
||||
encoders: make([]*splitEncoder, 0),
|
||||
config: config,
|
||||
bitrates: config.getBitrates(),
|
||||
producer: builder.producer,
|
||||
ctx: ctx2,
|
||||
cancel: cancel,
|
||||
resume: make(chan struct{}),
|
||||
}
|
||||
|
||||
describer, ok := encoder.producer.(CanDescribeMediaFrame)
|
||||
if !ok {
|
||||
return nil, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
initialBitrate, err := builder.GetCurrentBitrate()
|
||||
if err != nil {
|
||||
initialBitrate = encoder.bitrates[0]
|
||||
}
|
||||
|
||||
for _, bitrate := range encoder.bitrates {
|
||||
// TODO: WARN: 90 size might be tooo high
|
||||
// TODO: Frame pool could be abstracted away
|
||||
producer := newDummyMediaFrameProducer(buffer.CreateChannelBuffer(ctx2, 90, buffer.CreateFramePool()), describer)
|
||||
|
||||
if err := builder.UpdateBitrate(bitrate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, err := builder.BuildWithProducer(ctx2, producer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder.encoders = append(encoder.encoders, newSplitEncoder(e.(*GeneralEncoder), producer))
|
||||
}
|
||||
|
||||
encoder.switchEncoder(encoder.findBestEncoderIndex(initialBitrate))
|
||||
|
||||
return encoder, nil
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) Ctx() context.Context {
|
||||
return u.ctx
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) Start() {
|
||||
for _, encoder := range u.encoders {
|
||||
encoder.encoder.Start()
|
||||
}
|
||||
|
||||
go u.loop()
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) GetPacket(ctx context.Context) (*astiav.Packet, error) {
|
||||
return u.active.Load().encoder.GetPacket(ctx)
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) PutBack(packet *astiav.Packet) {
|
||||
u.active.Load().encoder.PutBack(packet)
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) Stop() {
|
||||
u.cancel()
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) UpdateBitrate(bps int64) error {
|
||||
if err := u.checkPause(bps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bps = u.cutoff(bps)
|
||||
|
||||
bestIndex := u.findBestEncoderIndex(bps)
|
||||
u.switchEncoder(bestIndex)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) findBestEncoderIndex(targetBps int64) int {
|
||||
bestIndex := 0
|
||||
for i, bitrate := range u.bitrates {
|
||||
if bitrate <= targetBps {
|
||||
bestIndex = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return bestIndex
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) switchEncoder(index int) {
|
||||
if index < len(u.encoders) {
|
||||
fmt.Printf("swapping to %d encoder with bitrate %d\n", index, u.bitrates[index])
|
||||
u.active.Swap(u.encoders[index])
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) cutoff(bps int64) int64 {
|
||||
if bps > u.config.MaxBitrate {
|
||||
bps = u.config.MaxBitrate
|
||||
}
|
||||
|
||||
if bps < u.config.MinBitrate {
|
||||
bps = u.config.MinBitrate
|
||||
}
|
||||
|
||||
return bps
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) shouldPause(bps int64) bool {
|
||||
return bps <= u.config.MinBitrate && u.config.CutVideoBelowMinBitrate
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) checkPause(bps int64) error {
|
||||
shouldPause := u.shouldPause(bps)
|
||||
|
||||
if shouldPause {
|
||||
fmt.Println("pausing video...")
|
||||
return u.PauseEncoding()
|
||||
}
|
||||
return u.UnPauseEncoding()
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) PauseEncoding() error {
|
||||
u.paused.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) UnPauseEncoding() error {
|
||||
u.pauseMux.Lock()
|
||||
defer u.pauseMux.Unlock()
|
||||
|
||||
if u.paused.Swap(false) {
|
||||
close(u.resume)
|
||||
u.resume = make(chan struct{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) GetParameterSets() (sps []byte, pps []byte, err error) {
|
||||
return u.active.Load().encoder.GetParameterSets()
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) loop() {
|
||||
defer u.close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
default:
|
||||
frame, err := u.getFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, encoder := range u.encoders {
|
||||
if err := u.pushFrame(encoder, frame); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
u.producer.PutBack(frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) getFrame() (*astiav.Frame, error) {
|
||||
ctx, cancel := context.WithTimeout(u.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
return u.producer.GetFrame(ctx)
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) pushFrame(encoder *splitEncoder, frame *astiav.Frame) error {
|
||||
if frame == nil {
|
||||
return fmt.Errorf("frame is nil from the producer")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(u.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
refFrame := encoder.producer.Generate()
|
||||
if refFrame == nil {
|
||||
return fmt.Errorf("failed to generate frame from encoder pool")
|
||||
}
|
||||
|
||||
if err := refFrame.Ref(frame); err != nil {
|
||||
return fmt.Errorf("erorr while adding ref to frame; err: %s", "refFrame is nil")
|
||||
}
|
||||
|
||||
// PUT IN BUFFER
|
||||
return encoder.producer.pushFrame(ctx, refFrame)
|
||||
}
|
||||
|
||||
func (u *MultiUpdateEncoder) close() {
|
||||
|
||||
}
|
247
pkg/transcode/notch_updates.go
Normal file
247
pkg/transcode/notch_updates.go
Normal file
@@ -0,0 +1,247 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// "errors"
|
||||
// "fmt"
|
||||
// "sync"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/aler9/gomavlib"
|
||||
// "github.com/aler9/gomavlib/pkg/dialects/ardupilotmega"
|
||||
// "github.com/aler9/gomavlib/pkg/dialects/common"
|
||||
// "github.com/asticode/go-astiav"
|
||||
// )
|
||||
//
|
||||
// type Propeller string
|
||||
//
|
||||
// func (prop Propeller) String() string {
|
||||
// return string(prop)
|
||||
// }
|
||||
//
|
||||
// const (
|
||||
// PropellerOne Propeller = "propeller0"
|
||||
// PropellerTwo Propeller = "propeller1"
|
||||
// PropellerThree Propeller = "propeller2"
|
||||
// PropellerFour Propeller = "propeller3"
|
||||
// PropellerFive Propeller = "propeller4"
|
||||
// PropellerSix Propeller = "propeller5"
|
||||
// PropellerSeven Propeller = "propeller6"
|
||||
// PropellerEight Propeller = "propeller7"
|
||||
// )
|
||||
//
|
||||
// type Updator interface {
|
||||
// Start(*GeneralFilter)
|
||||
// }
|
||||
//
|
||||
// func WithUpdateFilter(updator Updator) FilterOption {
|
||||
// return func(filter *GeneralFilter) error {
|
||||
// filter.updators = append(filter.updators, updator)
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// type notch struct {
|
||||
// prop Propeller
|
||||
// harmonics uint8
|
||||
// frequencies []float32
|
||||
// nBlades uint8
|
||||
// }
|
||||
//
|
||||
// func createNotch(prop Propeller, fundamental float32, harmonics, nBlades uint8) *notch {
|
||||
// n := ¬ch{
|
||||
// prop: prop,
|
||||
// harmonics: harmonics,
|
||||
// frequencies: make([]float32, harmonics),
|
||||
// nBlades: nBlades,
|
||||
// }
|
||||
//
|
||||
// for i := uint8(0); i < n.harmonics; i++ {
|
||||
// n.frequencies[i] = fundamental * float32(i+1)
|
||||
// }
|
||||
//
|
||||
// return n
|
||||
// }
|
||||
//
|
||||
// func (notch *notch) update(rpm float32) {
|
||||
// fundamental := rpm * float32(notch.nBlades) / 60.0
|
||||
// for i := uint8(0); i < notch.harmonics; i++ {
|
||||
// notch.frequencies[i] = (notch.frequencies[i] + fundamental*float32(i+1)) / 2.0
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// type PropNoiseFilterUpdator struct {
|
||||
// notches []*notch
|
||||
// node *gomavlib.Node
|
||||
// interval time.Duration
|
||||
// mux sync.RWMutex
|
||||
// flags astiav.FilterCommandFlags
|
||||
// ctx context.Context
|
||||
// }
|
||||
//
|
||||
// func CreatePropNoiseFilterUpdator(ctx context.Context, mavlinkSerial string, baudrate int, interval time.Duration) (*PropNoiseFilterUpdator, error) {
|
||||
// updater := &PropNoiseFilterUpdator{
|
||||
// notches: make([]*notch, 0),
|
||||
// flags: astiav.NewFilterCommandFlags(astiav.FilterCommandFlagFast, astiav.FilterCommandFlagOne),
|
||||
// interval: interval,
|
||||
// ctx: ctx,
|
||||
// }
|
||||
//
|
||||
// config := gomavlib.NodeConf{
|
||||
// Endpoints: []gomavlib.EndpointConf{
|
||||
// gomavlib.EndpointSerial{
|
||||
// Device: mavlinkSerial,
|
||||
// Baud: baudrate,
|
||||
// },
|
||||
// },
|
||||
// Dialect: ardupilotmega.Dialect,
|
||||
// OutVersion: gomavlib.V2,
|
||||
// OutSystemID: 10,
|
||||
// }
|
||||
//
|
||||
// node, err := gomavlib.NewNode(config)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// updater.node = node
|
||||
//
|
||||
// return updater, nil
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) AddNotchFilter(id Propeller, frequency float32, harmonics uint8, nBlades uint8) {
|
||||
// update.mux.Lock()
|
||||
//
|
||||
// // rpm nBlades will have RPM to rpm conversion with number of blades (Nb / 60)
|
||||
// update.notches = append(update.notches, createNotch(id, frequency, harmonics, nBlades))
|
||||
//
|
||||
// update.mux.Unlock()
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) loop1() {
|
||||
// ticker := time.NewTicker(update.interval)
|
||||
// defer ticker.Stop()
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case <-update.ctx.Done():
|
||||
// return
|
||||
// case <-ticker.C:
|
||||
// update.node.WriteMessageAll(&ardupilotmega.MessageCommandLong{
|
||||
// TargetSystem: 1,
|
||||
// TargetComponent: 0,
|
||||
// Command: common.MAV_CMD_REQUEST_MESSAGE,
|
||||
// Confirmation: 0,
|
||||
// Param1: float32((&ardupilotmega.MessageEscTelemetry_1To_4{}).GetID()),
|
||||
// Param2: 0,
|
||||
// Param3: 0,
|
||||
// Param4: 0,
|
||||
// Param5: 0,
|
||||
// Param6: 0,
|
||||
// Param7: 0,
|
||||
// })
|
||||
//
|
||||
// update.node.WriteMessageAll(&ardupilotmega.MessageCommandLong{
|
||||
// TargetSystem: 1,
|
||||
// TargetComponent: 0,
|
||||
// Command: common.MAV_CMD_REQUEST_MESSAGE,
|
||||
// Confirmation: 0,
|
||||
// Param1: float32((&ardupilotmega.MessageEscTelemetry_5To_8{}).GetID()),
|
||||
// Param2: 0,
|
||||
// Param3: 0,
|
||||
// Param4: 0,
|
||||
// Param5: 0,
|
||||
// Param6: 0,
|
||||
// Param7: 0,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) loop2() {
|
||||
// eventChan := update.node.Events()
|
||||
//
|
||||
// loop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-update.ctx.Done():
|
||||
// return
|
||||
// case event, ok := <-eventChan:
|
||||
// if !ok {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if frm, ok := event.(*gomavlib.EventFrame); ok {
|
||||
// switch msg := frm.Message().(type) {
|
||||
// case *ardupilotmega.MessageEscTelemetry_1To_4:
|
||||
// update.mux.Lock()
|
||||
//
|
||||
// length := min(len(update.notches), 4)
|
||||
// if length <= 0 {
|
||||
// continue loop
|
||||
// }
|
||||
// for i := 0; i < length; i++ {
|
||||
// update.notches[i].update(float32(msg.Rpm[i]))
|
||||
// }
|
||||
//
|
||||
// update.mux.Unlock()
|
||||
// case *ardupilotmega.MessageEscTelemetry_5To_8:
|
||||
// update.mux.Lock()
|
||||
//
|
||||
// length := min(len(update.notches)-4, 4)
|
||||
// if length <= 0 {
|
||||
// continue loop
|
||||
// }
|
||||
// for i := 0; i < length; i++ {
|
||||
// update.notches[i+4].update(float32(msg.Rpm[i]))
|
||||
// }
|
||||
//
|
||||
// update.mux.Unlock()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) loop3(filter *GeneralFilter) {
|
||||
// ticker := time.NewTicker(update.interval)
|
||||
// defer ticker.Stop()
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case <-update.ctx.Done():
|
||||
// return
|
||||
// case <-ticker.C:
|
||||
// if err := update.update(filter); err != nil {
|
||||
// fmt.Printf("Error updating notch filter: %v\n", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) Start(filter *GeneralFilter) {
|
||||
// go update.loop1()
|
||||
// go update.loop2()
|
||||
// go update.loop3(filter)
|
||||
// }
|
||||
//
|
||||
// func (update *PropNoiseFilterUpdator) update(filter *GeneralFilter) error {
|
||||
// if filter == nil {
|
||||
// return errors.New("filter is nil")
|
||||
// }
|
||||
// filter.mux.Lock()
|
||||
// defer filter.mux.Unlock()
|
||||
//
|
||||
// for index, notch := range update.notches {
|
||||
// target := fmt.Sprintf("%s%d", notch.prop.String(), index)
|
||||
// for _, frequency := range notch.frequencies {
|
||||
// if _, err := filter.graph.SendCommand(target, "frequency", fmt.Sprintf("%.2f", frequency), update.flags); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
109
pkg/transcode/transcoder.go
Normal file
109
pkg/transcode/transcoder.go
Normal file
@@ -0,0 +1,109 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
)
|
||||
|
||||
type Transcoder struct {
|
||||
demuxer Demuxer
|
||||
decoder Decoder
|
||||
filter Filter
|
||||
encoder Encoder
|
||||
}
|
||||
|
||||
func CreateTranscoder(options ...TranscoderOption) (*Transcoder, error) {
|
||||
t := &Transcoder{}
|
||||
for _, option := range options {
|
||||
if err := option(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func NewTranscoder(demuxer Demuxer, decoder Decoder, filter Filter, encoder Encoder) *Transcoder {
|
||||
return &Transcoder{
|
||||
demuxer: demuxer,
|
||||
decoder: decoder,
|
||||
filter: filter,
|
||||
encoder: encoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transcoder) Start() {
|
||||
fmt.Println("started encoder")
|
||||
t.demuxer.Start()
|
||||
t.decoder.Start()
|
||||
t.filter.Start()
|
||||
t.encoder.Start()
|
||||
}
|
||||
|
||||
func (t *Transcoder) Stop() {
|
||||
t.encoder.Stop()
|
||||
t.filter.Stop()
|
||||
t.decoder.Stop()
|
||||
t.demuxer.Stop()
|
||||
}
|
||||
|
||||
func (t *Transcoder) GetPacket(ctx context.Context) (*astiav.Packet, error) {
|
||||
return t.encoder.GetPacket(ctx)
|
||||
}
|
||||
|
||||
func (t *Transcoder) PutBack(packet *astiav.Packet) {
|
||||
t.encoder.PutBack(packet)
|
||||
}
|
||||
|
||||
// Generate method is to satisfy mediapipe.CanGenerate interface. TODO: but I would prefer to integrate with PutBack
|
||||
func (t *Transcoder) Generate() (*astiav.Packet, error) {
|
||||
packet, err := t.encoder.GetPacket(t.encoder.Ctx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func (t *Transcoder) PauseEncoding() error {
|
||||
p, ok := t.encoder.(CanPauseUnPauseEncoder)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return p.PauseEncoding()
|
||||
}
|
||||
|
||||
func (t *Transcoder) UnPauseEncoding() error {
|
||||
p, ok := t.encoder.(CanPauseUnPauseEncoder)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return p.UnPauseEncoding()
|
||||
}
|
||||
|
||||
func (t *Transcoder) GetParameterSets() (sps, pps []byte, err error) {
|
||||
p, ok := t.encoder.(CanGetParameterSets)
|
||||
if !ok {
|
||||
return nil, nil, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return p.GetParameterSets()
|
||||
}
|
||||
|
||||
func (t *Transcoder) UpdateBitrate(bps int64) error {
|
||||
u, ok := t.encoder.(CanUpdateBitrate)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return u.UpdateBitrate(bps)
|
||||
}
|
||||
|
||||
func (t *Transcoder) OnUpdateBitrate() UpdateBitrateCallBack {
|
||||
return t.UpdateBitrate
|
||||
}
|
87
pkg/transcode/transcoder_options.go
Normal file
87
pkg/transcode/transcoder_options.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type TranscoderOption = func(*Transcoder) error
|
||||
|
||||
func WithGeneralDemuxer(ctx context.Context, containerAddress string, options ...DemuxerOption) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
demuxer, err := CreateGeneralDemuxer(ctx, containerAddress, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.demuxer = demuxer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithGeneralDecoder(ctx context.Context, options ...DecoderOption) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
decoder, err := CreateGeneralDecoder(ctx, transcoder.demuxer, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.decoder = decoder
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithGeneralFilter(ctx context.Context, filterConfig FilterConfig, options ...FilterOption) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
filter, err := CreateGeneralFilter(ctx, transcoder.decoder, filterConfig, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.filter = filter
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithGeneralEncoder(ctx context.Context, codecID astiav.CodecID, options ...EncoderOption) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
encoder, err := CreateGeneralEncoder(ctx, codecID, transcoder.filter, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.encoder = encoder
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithBitrateControlEncoder(ctx context.Context, codecID astiav.CodecID, bitrateControlConfig UpdateConfig, settings codecSettings, bufferSize int, pool buffer.Pool[*astiav.Packet]) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
builder := NewEncoderBuilder(codecID, settings, transcoder.filter, bufferSize, pool)
|
||||
updateEncoder, err := NewUpdateEncoder(ctx, bitrateControlConfig, builder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.encoder = updateEncoder
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMultiEncoderBitrateControl(ctx context.Context, codecID astiav.CodecID, config MultiConfig, settings codecSettings, bufferSize int, pool buffer.Pool[*astiav.Packet]) TranscoderOption {
|
||||
return func(transcoder *Transcoder) error {
|
||||
builder := NewEncoderBuilder(codecID, settings, transcoder.filter, bufferSize, pool)
|
||||
multiEncoder, err := NewMultiUpdateEncoder(ctx, config, builder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transcoder.encoder = multiEncoder
|
||||
return nil
|
||||
}
|
||||
}
|
268
pkg/transcode/update_encoder_wrapper.go
Normal file
268
pkg/transcode/update_encoder_wrapper.go
Normal file
@@ -0,0 +1,268 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
|
||||
"github.com/harshabose/tools/pkg/buffer"
|
||||
)
|
||||
|
||||
type UpdateConfig struct {
|
||||
MaxBitrate, MinBitrate int64
|
||||
CutVideoBelowMinBitrate bool
|
||||
}
|
||||
|
||||
func (c UpdateConfig) validate() error {
|
||||
if c.MinBitrate > c.MaxBitrate {
|
||||
return fmt.Errorf("minimum bitrate is higher than maximum bitrate in the update encoder config")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateEncoder struct {
|
||||
encoder Encoder
|
||||
config UpdateConfig
|
||||
builder *GeneralEncoderBuilder
|
||||
buffer buffer.BufferWithGenerator[*astiav.Packet]
|
||||
mux sync.RWMutex
|
||||
ctx context.Context
|
||||
|
||||
paused atomic.Bool
|
||||
resume chan struct{}
|
||||
pauseMux sync.Mutex
|
||||
}
|
||||
|
||||
func NewUpdateEncoder(ctx context.Context, config UpdateConfig, builder *GeneralEncoderBuilder) (*UpdateEncoder, error) {
|
||||
updater := &UpdateEncoder{
|
||||
config: config,
|
||||
builder: builder,
|
||||
resume: make(chan struct{}),
|
||||
buffer: buffer.CreateChannelBuffer(ctx, 30, buffer.CreatePacketPool()),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder, err := builder.Build(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updater.encoder = encoder
|
||||
|
||||
go updater.loop()
|
||||
|
||||
return updater, nil
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) Ctx() context.Context {
|
||||
u.mux.Lock()
|
||||
defer u.mux.Unlock()
|
||||
|
||||
return u.encoder.Ctx()
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) Start() {
|
||||
u.mux.Lock()
|
||||
defer u.mux.Unlock()
|
||||
|
||||
u.encoder.Start()
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) GetPacket(ctx context.Context) (*astiav.Packet, error) {
|
||||
return u.buffer.Pop(ctx)
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) PutBack(packet *astiav.Packet) {
|
||||
u.mux.RLock()
|
||||
defer u.mux.RUnlock()
|
||||
|
||||
u.encoder.PutBack(packet)
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) Stop() {
|
||||
u.mux.Lock()
|
||||
defer u.mux.Unlock()
|
||||
|
||||
u.encoder.Stop()
|
||||
}
|
||||
|
||||
// UpdateBitrate modifies the encoder's target bitrate to the specified value in bits per second.
|
||||
// Returns an error if the update fails.
|
||||
func (u *UpdateEncoder) UpdateBitrate(bps int64) error {
|
||||
// return nil
|
||||
if err := u.checkPause(bps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bps = u.cutoff(bps)
|
||||
|
||||
g, ok := u.encoder.(CanGetCurrentBitrate)
|
||||
if !ok {
|
||||
return ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
current, err := g.GetCurrentBitrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, change := calculateBitrateChange(current, bps)
|
||||
if change < 5 {
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("got bitrate update request (%d -> %d)\n", current, bps)
|
||||
|
||||
start := time.Now()
|
||||
if err := u.builder.UpdateBitrate(bps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newEncoder, err := u.builder.Build(u.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build new encoder: %w", err)
|
||||
}
|
||||
|
||||
newEncoder.Start()
|
||||
|
||||
// Wait for the first packet from the new encoder
|
||||
// firstPacket := <-newEncoder.WaitForPacket()
|
||||
// newEncoder.PutBack(firstPacket)
|
||||
|
||||
u.mux.Lock()
|
||||
oldEncoder := u.encoder
|
||||
u.encoder = newEncoder
|
||||
u.mux.Unlock()
|
||||
|
||||
// Print encoder update notification
|
||||
fmt.Println()
|
||||
fmt.Println("╔═══════════════════════════════════════╗")
|
||||
fmt.Println("║ 🎥 ENCODER UPDATED 🎥 ║")
|
||||
fmt.Printf("║ New Bitrate: %6d kbps ║\n", bps/1000)
|
||||
fmt.Printf("║ Change: %6.2f ║\n", change)
|
||||
fmt.Printf("║ Update time: %6d ms ║\n", time.Since(start).Milliseconds())
|
||||
fmt.Println("╚═══════════════════════════════════════╝")
|
||||
fmt.Println()
|
||||
|
||||
if oldEncoder != nil {
|
||||
oldEncoder.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) cutoff(bps int64) int64 {
|
||||
if bps > u.config.MaxBitrate {
|
||||
bps = u.config.MaxBitrate
|
||||
}
|
||||
|
||||
if bps < u.config.MinBitrate {
|
||||
bps = u.config.MinBitrate
|
||||
}
|
||||
|
||||
return bps
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) shouldPause(bps int64) bool {
|
||||
return bps <= u.config.MinBitrate && u.config.CutVideoBelowMinBitrate
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) checkPause(bps int64) error {
|
||||
shouldPause := u.shouldPause(bps)
|
||||
|
||||
if shouldPause {
|
||||
fmt.Println("pausing video...")
|
||||
return u.PauseEncoding()
|
||||
}
|
||||
return u.UnPauseEncoding()
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) PauseEncoding() error {
|
||||
u.paused.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) UnPauseEncoding() error {
|
||||
u.pauseMux.Lock()
|
||||
defer u.pauseMux.Unlock()
|
||||
|
||||
if u.paused.Swap(false) {
|
||||
close(u.resume)
|
||||
u.resume = make(chan struct{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) GetParameterSets() (sps []byte, pps []byte, err error) {
|
||||
p, ok := u.encoder.(CanGetParameterSets)
|
||||
if !ok {
|
||||
return nil, nil, ErrorInterfaceMismatch
|
||||
}
|
||||
|
||||
return p.GetParameterSets()
|
||||
}
|
||||
|
||||
func calculateBitrateChange(currentBps, newBps int64) (absoluteChange int64, percentageChange float64) {
|
||||
absoluteChange = newBps - currentBps
|
||||
if absoluteChange < 0 {
|
||||
absoluteChange = -absoluteChange
|
||||
}
|
||||
|
||||
if currentBps > 0 {
|
||||
percentageChange = (float64(absoluteChange) / float64(currentBps)) * 100
|
||||
}
|
||||
|
||||
return absoluteChange, percentageChange
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) getPacket() (*astiav.Packet, error) {
|
||||
u.mux.RLock()
|
||||
defer u.mux.RUnlock()
|
||||
|
||||
if u.encoder != nil {
|
||||
ctx, cancel := context.WithTimeout(u.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
return u.encoder.GetPacket(ctx) // Don't hold lock during blocking call
|
||||
}
|
||||
|
||||
return nil, errors.New("encoder is nil")
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) pushPacket(p *astiav.Packet) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(u.ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
return u.buffer.Push(ctx, p)
|
||||
}
|
||||
|
||||
func (u *UpdateEncoder) loop() {
|
||||
for {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
default:
|
||||
p, err := u.getPacket()
|
||||
if err != nil {
|
||||
// fmt.Println("error getting packet from encoder; err:", err.Error())
|
||||
}
|
||||
|
||||
if err := u.pushPacket(p); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
143
pkg/transcode/x264options.go
Normal file
143
pkg/transcode/x264options.go
Normal file
@@ -0,0 +1,143 @@
|
||||
//go:build cgo_enabled
|
||||
|
||||
package transcode
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type X264AdvancedOptions struct {
|
||||
// PRIMARY OPTIONS
|
||||
Bitrate string `x264-opts:"bitrate"`
|
||||
VBVMaxBitrate string `x264-opts:"vbv-maxrate"`
|
||||
VBVBuffer string `x264-opts:"vbv-bufsize"`
|
||||
RateTolerance string `x264-opts:"ratetol"`
|
||||
MaxGOP string `x264-opts:"keyint"`
|
||||
MinGOP string `x264-opts:"min-keyint"`
|
||||
MaxQP string `x264-opts:"qpmax"`
|
||||
MinQP string `x264-opts:"qpmin"`
|
||||
MaxQPStep string `x264-opts:"qpstep"`
|
||||
IntraRefresh string `x264-opts:"intra-refresh"`
|
||||
ConstrainedIntra string `x264-opts:"constrained-intra"`
|
||||
|
||||
// SECONDARY OPTIONS; SOME OF THEM ARE ALREADY SET BY PRESET, PROFILE AND TUNE
|
||||
SceneCut string `x264-opts:"scenecut"`
|
||||
BFrames string `x264-opts:"bframes"`
|
||||
BAdapt string `x264-opts:"b-adapt"`
|
||||
Refs string `x264-opts:"ref"`
|
||||
RCLookAhead string `x264-opts:"rc-lookahead"`
|
||||
AQMode string `x264-opts:"aq-mode"`
|
||||
NalHrd string `x264-opts:"nal-hrd"`
|
||||
}
|
||||
|
||||
func (o *X264AdvancedOptions) ForEach(f func(key, value string) error) error {
|
||||
t := reflect.TypeOf(*o)
|
||||
v := reflect.ValueOf(*o)
|
||||
|
||||
// Build a single x264opts string
|
||||
var optParts []string
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get("x264-opts")
|
||||
if tag != "" && v.Field(i).String() != "" {
|
||||
optParts = append(optParts, tag+"="+v.Field(i).String())
|
||||
}
|
||||
}
|
||||
|
||||
// Join all options with colons
|
||||
if len(optParts) > 0 {
|
||||
x264optsValue := strings.Join(optParts, ":")
|
||||
if err := f("x264opts", x264optsValue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *X264AdvancedOptions) UpdateBitrate(bps int64) error {
|
||||
kbps := bps / 1000
|
||||
|
||||
// Core bitrate settings (strict CBR)
|
||||
o.Bitrate = strconv.FormatInt(kbps, 10)
|
||||
o.VBVMaxBitrate = strconv.FormatInt(kbps, 10) // Same as bitrate for CBR
|
||||
|
||||
// VBV buffer: 0.5 seconds for low latency
|
||||
// Formula: buffer_kb = (bitrate_kbps * buffer_duration_seconds)
|
||||
// Minimum of 100 kb might be needed. TODO: do more research
|
||||
bufferKb := max(kbps/2, 100) // 0.5 seconds = 1/2 second
|
||||
o.VBVBuffer = strconv.FormatInt(bufferKb, 10)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *X264AdvancedOptions) GetCurrentBitrate() (int64, error) {
|
||||
kbps, err := strconv.ParseInt(o.Bitrate, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return kbps * 1000, nil // Convert kbps to bps
|
||||
}
|
||||
|
||||
type X264Options struct {
|
||||
*X264AdvancedOptions
|
||||
// PRECOMPILED OPTIONS
|
||||
Profile string `x264:"profile"`
|
||||
Level string `x264:"level"`
|
||||
Preset string `x264:"preset"`
|
||||
Tune string `x264:"tune"`
|
||||
}
|
||||
|
||||
func (o *X264Options) ForEach(f func(key, value string) error) error {
|
||||
t := reflect.TypeOf(*o)
|
||||
v := reflect.ValueOf(*o)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tag := field.Tag.Get("x264")
|
||||
if tag != "" {
|
||||
if err := f(tag, v.Field(i).String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return o.X264AdvancedOptions.ForEach(f)
|
||||
}
|
||||
|
||||
func (o *X264Options) UpdateBitrate(bps int64) error {
|
||||
return o.X264AdvancedOptions.UpdateBitrate(bps)
|
||||
}
|
||||
|
||||
// TODO: WARN: MAKING THIS A POINTER VARIABLE WILL MAKE ALL TRACKS WHICH USE THIS SETTINGS TO SHARE BITRATE
|
||||
|
||||
var LowLatencyBitrateControlled = &X264Options{
|
||||
Profile: "baseline",
|
||||
Level: "3.1",
|
||||
Preset: "ultrafast",
|
||||
Tune: "zerolatency",
|
||||
|
||||
X264AdvancedOptions: &X264AdvancedOptions{
|
||||
Bitrate: "800", // 800kbps
|
||||
VBVMaxBitrate: "800", // Equal to Bitrate
|
||||
VBVBuffer: "400", // Half of Bitrate
|
||||
RateTolerance: "1", // 1% rate tolerance
|
||||
MaxGOP: "25",
|
||||
MinGOP: "13",
|
||||
// MaxQP: "80",
|
||||
// MinQP: "24",
|
||||
// MaxQPStep: "80",
|
||||
IntraRefresh: "0",
|
||||
ConstrainedIntra: "0",
|
||||
SceneCut: "0",
|
||||
BFrames: "0",
|
||||
BAdapt: "0",
|
||||
Refs: "1",
|
||||
RCLookAhead: "0",
|
||||
AQMode: "1", // Not sure; do more research
|
||||
NalHrd: "cbr",
|
||||
},
|
||||
}
|
39
rtc_config.go
Normal file
39
rtc_config.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
func GetRTCConfiguration() webrtc.Configuration {
|
||||
return webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{os.Getenv("STUN_SERVER_URL")},
|
||||
},
|
||||
// {
|
||||
// URLs: []string{os.Getenv("TURN_UDP_SERVER_URL")},
|
||||
// Username: os.Getenv("TURN_SERVER_USERNAME"),
|
||||
// Credential: os.Getenv("TURN_SERVER_PASSWORD"),
|
||||
// CredentialType: webrtc.ICECredentialTypePassword,
|
||||
// },
|
||||
// {
|
||||
// URLs: []string{os.Getenv("TURN_TCP_SERVER_URL")},
|
||||
// Username: os.Getenv("TURN_SERVER_USERNAME"),
|
||||
// Credential: os.Getenv("TURN_SERVER_PASSWORD"),
|
||||
// CredentialType: webrtc.ICECredentialTypePassword,
|
||||
// },
|
||||
// {
|
||||
// URLs: []string{os.Getenv("TURN_TLS_SERVER_URL")},
|
||||
// Username: os.Getenv("TURN_SERVER_USERNAME"),
|
||||
// Credential: os.Getenv("TURN_SERVER_PASSWORD"),
|
||||
// CredentialType: webrtc.ICECredentialTypePassword,
|
||||
// },
|
||||
},
|
||||
ICETransportPolicy: webrtc.ICETransportPolicyAll,
|
||||
BundlePolicy: webrtc.BundlePolicyMaxCompat,
|
||||
RTCPMuxPolicy: webrtc.RTCPMuxPolicyRequire,
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlan,
|
||||
}
|
||||
}
|
17
signal.go
Normal file
17
signal.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package client
|
||||
|
||||
const (
|
||||
FieldOffer = "offer"
|
||||
FieldAnswer = "answer"
|
||||
FieldSDP = "sdp"
|
||||
FieldUpdatedAt = "updated-at"
|
||||
FieldStatus = "status"
|
||||
FieldStatusPending = "pending"
|
||||
FieldStatusConnected = "connected"
|
||||
FieldCreatedAt = "created-at"
|
||||
)
|
||||
|
||||
type BaseSignal interface {
|
||||
Connect(string, string) error
|
||||
Close() error
|
||||
}
|
10
stream.sdp
Normal file
10
stream.sdp
Normal file
@@ -0,0 +1,10 @@
|
||||
v=0
|
||||
o=- 0 0 IN IP4 127.0.0.1
|
||||
s=test-video
|
||||
c=IN IP4 127.0.0.1
|
||||
t=0 0
|
||||
m=video 4002 RTP/AVP 96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=x-dimensions:1920,1080
|
||||
a=x-framerate:25
|
||||
a=maxptime:40
|
Reference in New Issue
Block a user