mirror of
https://github.com/harshabose/client.git
synced 2025-09-27 03:35:55 +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