Git Product home page Git Product logo

roc-go's Introduction

Go bindings for Roc Toolkit

GoDev Build Coverage Status GitHub release Matrix chat

This library provides Go (golang) bindings for Roc Toolkit, a toolkit for real-time audio streaming over the network.

About Roc

Key features of Roc Toolkit:

  • real-time streaming with guaranteed latency;
  • robust work on unreliable networks like Wi-Fi, due to use of Forward Erasure Correction codes;
  • CD-quality audio;
  • multiple profiles for different CPU and latency requirements;
  • relying on open, standard protocols, like RTP and FECFRAME;
  • interoperability with both Roc and third-party software.

Compatible Roc Toolkit senders and receivers include:

Documentation

Documentation for the bindings is available on pkg.go.dev.

Documentation for the underlying C API can be found here.

Quick start

Sender

import (
	"github.com/roc-streaming/roc-go/roc"
)

context, err := roc.OpenContext(roc.ContextConfig{})
if err != nil {
	panic(err)
}
defer context.Close()

sender, err := roc.OpenSender(roc.SenderConfig{
	FrameEncoding: roc.MediaEncoding{
		Rate:     44100,
		Format:   roc.FormatPcmFloat32,
		Channels: roc.ChannelLayoutStereo,
	},
	PacketEncoding: roc.PacketEncodingAvpL16Stereo,
	FecEncoding:    roc.FecEncodingRs8m,
	ClockSource:    roc.ClockSourceInternal,
})
if err != nil {
	panic(err)
}
defer sender.Close()

sourceEndpoint, err := roc.ParseEndpoint("rtp+rs8m://192.168.0.1:10001")
if err != nil {
	panic(err)
}

repairEndpoint, err := roc.ParseEndpoint("rs8m://192.168.0.1:10002")
if err != nil {
	panic(err)
}

controlEndpoint, err := roc.ParseEndpoint("rtcp://192.168.0.1:10003")
if err != nil {
	panic(err)
}

err = sender.Connect(roc.SlotDefault, roc.InterfaceAudioSource, sourceEndpoint)
if err != nil {
	panic(err)
}

err = sender.Connect(roc.SlotDefault, roc.InterfaceAudioRepair, repairEndpoint)
if err != nil {
	panic(err)
}

err = sender.Connect(roc.SlotDefault, roc.InterfaceAudioControl, controlEndpoint)
if err != nil {
	panic(err)
}

for {
	samples := make([]float32, 320)

	/* fill samples */

	err = sender.WriteFloats(samples)
	if err != nil {
		panic(err)
	}
}

Receiver

import (
	"github.com/roc-streaming/roc-go/roc"
)

context, err := roc.OpenContext(roc.ContextConfig{})
if err != nil {
	panic(err)
}
defer context.Close()

receiver, err := roc.OpenReceiver(roc.ReceiverConfig{
	FrameEncoding: roc.MediaEncoding{
		Rate:     44100,
		Format:   roc.FormatPcmFloat32,
		Channels: roc.ChannelLayoutStereo,
	},
	ClockSource: roc.ClockSourceInternal,
})
if err != nil {
	panic(err)
}
defer receiver.Close()

sourceEndpoint, err := roc.ParseEndpoint("rtp+rs8m://0.0.0.0:10001")
if err != nil {
	panic(err)
}

repairEndpoint, err := roc.ParseEndpoint("rs8m://0.0.0.0:10002")
if err != nil {
	panic(err)
}

controlEndpoint, err := roc.ParseEndpoint("rtcp://0.0.0.0:10003")
if err != nil {
	panic(err)
}

err = receiver.Bind(roc.SlotDefault, roc.InterfaceAudioSource, sourceEndpoint)
if err != nil {
	panic(err)
}

err = receiver.Bind(roc.SlotDefault, roc.InterfaceAudioRepair, repairEndpoint)
if err != nil {
	panic(err)
}

err = receiver.Bind(roc.SlotDefault, roc.InterfaceAudioControl, controlEndpoint)
if err != nil {
	panic(err)
}

for {
	samples := make([]float32, 320)

	err = receiver.ReadFloats(samples)
	if err != nil {
		panic(err)
	}

	/* process samples */
}

Versioning

Go bindings and the C library both use semantic versioning.

Bindings are compatible with the C library when:

  • major version of bindings is same as major version of C library
  • minor version of bindings is same or higher as minor version of C library

Patch versions of bindings and C library are independent.

For example, version 1.2.3 of the bindings would be compatible with 1.2.x and 1.3.x, but not with 1.1.x (minor version is lower) or 2.x.x (major version is different).

Installation

You will need to have Roc Toolkit library and headers installed system-wide. Refer to official build instructions on how to install it.

After installing libroc, you can install bindings using regular go get:

go get github.com/roc-streaming/roc-go/roc

Development

Install development dependencies:

Run all checks:

make

Run only specific checks:

make gen|build|lint|test|testall

Update modules:

make tidy

Format code:

make fmt

Release

To release a new version:

  • Create git tag

    ./tag.py --push <remote> <version>
    

    e.g.

    ./tag.py --push origin 1.2.3
    

    Or use tag.py without --push to only create a tag locally, and then push it manually.

  • Wait until "Release" CI job completes and creates GitHub release draft.

  • Edit GitHub release created by CI and publish it.

Authors

See here.

License

Bindings are licensed under MIT.

For details on Roc Toolkit licensing, see here.

roc-go's People

Contributors

asalle avatar darrellbor avatar dhanusaputra avatar fastbyt3 avatar g41797 avatar gavv avatar ortex avatar parasraba155 avatar piotrlewandowski323 avatar pypaut avatar wataru-nakanishi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

roc-go's Issues

Race detector crash in shared library destructor

From discussion with @ortex in chat.

Let's add a test that opens Context and does not close it:

func TestContext_OpenNoClose(t *testing.T) {
	ctx, err := OpenContext(ContextConfig{MaxPacketSize: 50, MaxFrameSize: 70})
	require.NoError(t, err)
	require.NotNil(t, ctx)
}

The test works:

$ go test -v -run TestContext_OpenNoClose            
=== RUN   TestContext_OpenNoClose
--- PASS: TestContext_OpenNoClose (0.00s)
PASS
ok  	github.com/roc-streaming/roc-go/roc	0.006s

However if we run it under race detector, we'll get a weird crash:

$ go test -v -race -run TestContext_OpenNoClose |& pp
=== RUN   TestContext_OpenNoClose
--- PASS: TestContext_OpenNoClose (0.00s)
PASS
fatal error: exitsyscall: syscall frame is no longer valid


To see all goroutines, visit https://github.com/maruel/panicparse#gotraceback

1: running [locked]
    runtime panic.go:1047   throw(string(0x6f7b77, len=824635848344))
    runtime proc.go:3925    exitsyscall()
    runtime cgocall.go:229  cgocallbackg(Pointer(0xc0002077c8), Pointer(0xc0000061a0))
    runtime :1              cgocallbackg(0x6574c0, 0x7fffa13f2f68, 0)
    runtime asm_amd64.s:998 cgocallback(0x4831f7, 0x52a831, 0x483225)
runtime: g 1: unexpected return pc for syscall.Syscall called from 0x80000000000
stack: frame={sp:0xc0002077d8, fp:0xc000207848} stack=[0xc000204000,0xc000208000)
0x000000c0002076d8:  0x0000000000524f9c <testing.tRunner.func1+0x00000000000005dc>  0x0000000000000000 
0x000000c0002076e8:  0x000000c000207778  0x0000000000416925 <runtime.cgocallbackg+0x00000000000000e5> 
0x000000c0002076f8:  0x0000000000000000  0x0000000000000000 
0x000000c000207708:  0x0000000000000000  0x0000000000492991 <syscall.Syscall+0x0000000000000031> 
0x000000c000207718:  0x0000000000000000  0x0000000000000000 
0x000000c000207728:  0x0000000000000000  0x0000000000000000 
0x000000c000207738:  0x0000000000000000  0x0000000000000000 
0x000000c000207748:  0x0000000000000000  0x0000000000000000 
0x000000c000207758:  0x000000c0000061a0  0x00000000008b1aa0 
0x000000c000207768:  0x00000000006574c0 <_cgoexp_3d9ef9e457df_rocGoLogHandler+0x0000000000000000>  0x00007fffa13f2f68 
0x000000c000207778:  0x000000c0002077a0  0x0000000000482e6f <runtime.cgocallbackg+0x000000000000002f> 
0x000000c000207788:  0x000000c0002077c8  0x000000000047b8db <runtime.exitsyscall+0x000000000000013b> 
0x000000c000207798:  0x000000c0000061a0  0x00007fffa13f2f00 
0x000000c0002077a8:  0x0000000000480014 <runtime.cgocallback+0x00000000000000b4>  0x00000000006574c0 <_cgoexp_3d9ef9e457df_rocGoLogHandler+0x0000000000000000> 
0x000000c0002077b8:  0x00007fffa13f2f68  0x0000000000000000 
0x000000c0002077c8:  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005>  0x0000000000492991 <syscall.Syscall+0x0000000000000031> 
0x000000c0002077d8: <0x00000000004831f7 <runtime.racefuncenter+0x0000000000000017>  0x000000000052a831 <testing.(*M).after.func1+0x0000000000000031> 
0x000000c0002077e8:  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005>  0x000000000052b4da <testing.(*M).writeProfiles+0x0000000000000c7a> 
0x000000c0002077f8:  0x0000000000000001  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005> 
0x000000c000207808:  0x00000000004af532 <internal/poll.(*fdMutex).rwunlock+0x0000000000000132>  0x0000000000000000 
0x000000c000207818:  0x0000000000000001  0x0000000000000001 
0x000000c000207828:  0x0000000000000005  0x000000000000000c 
0x000000c000207838:  0x0000000000000000 !0x0000080000000000 
0x000000c000207848: >0x7ffff80000000000  0x0000000000000004 
0x000000c000207858:  0x000000c00012c06c  0x000000000045c335 <runtime.racereleaseg+0x0000000000000075> 
0x000000c000207868:  0x00000000006792b0  0x00007f03e9603900 
0x000000c000207878:  0x000000c00013f0bc  0x0000000000000000 
0x000000c000207888:  0x0000000000000000  0x0000000000481e0c <sync/atomic.AddInt32+0x000000000000000c> 
0x000000c000207898:  0x000000000048355b <sync/atomic.AddInt32+0x000000000000001b>  0x000000c00013f0bc 
0x000000c0002078a8:  0x00000000ffffffff  0x0000000000000000 
0x000000c0002078b8:  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005>  0x0000000000489a9a <sync.(*Mutex).Unlock+0x000000000000005a> 
0x000000c0002078c8:  0x000000000045c335 <runtime.racereleaseg+0x0000000000000075>  0x00000000006792b0 
0x000000c0002078d8:  0x00007f03e9603900  0x00000000018164b0 
0x000000c0002078e8:  0x0000000000000000  0x0000000000000000 
0x000000c0002078f8:  0x0000000000481e0c <sync/atomic.AddInt32+0x000000000000000c>  0x000000000048355b <sync/atomic.AddInt32+0x000000000000001b> 
0x000000c000207908:  0x00000000018164b0  0x00000000ffffffff 
0x000000c000207918:  0x0000000000000000  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005> 
0x000000c000207928:  0x0000000000489a9a <sync.(*Mutex).Unlock+0x000000000000005a>  0x00000000004831f7 <runtime.racefuncenter+0x0000000000000017> 
0x000000c000207938:  0x00000000004487f3 <runtime.deferreturn+0x0000000000000033>  0x0000000000483225 <runtime.racefuncexit+0x0000000000000005> 
syscall.Syscall(0x7ffff80000000000?, 0x4?, 0xc00012c06c?, 0x45c335?)
	/home/linuxbrew/.linuxbrew/Cellar/go/1.20.2/libexec/src/syscall/syscall_linux.go:69 +0x31 fp=0xc000207848 sp=0xc0002077d8 pc=0x492991

1: force gc (idle) [Created by runtime.init.6 @ proc.go:293]
    runtime proc.go:381        gopark(func(0x8b1aa0), Pointer(0x0), waitReason(0x0))
    runtime proc.go:387        goparkunlock(...)
    runtime proc.go:305        forcegchelper()
    runtime asm_amd64.s:1598   goexit()
1: GC scavenge wait [Created by runtime.gcenable @ mgc.go:179]
    runtime proc.go:381        gopark(func(#3), Pointer(0x1), waitReason(0x0))
    runtime proc.go:387        goparkunlock(...)
    runtime mgcscavenge.go:400 (*scavengerState).park(*scavengerState(#2))
    runtime mgcscavenge.go:628 bgscavenge(chan int(0x0))
    runtime mgc.go:179         gcenable.func2()
    runtime asm_amd64.s:1598   goexit()
1: GC sweep wait [Created by runtime.gcenable @ mgc.go:178]
    runtime proc.go:381        gopark(func(0x0), Pointer(0x0), waitReason(0x0))
    runtime proc.go:387        goparkunlock(...)
    runtime mgcsweep.go:278    bgsweep(chan int(0x0))
    runtime mgc.go:178         gcenable.func1()
    runtime asm_amd64.s:1598   goexit()
1: finalizer wait [Created by runtime.createfing @ mfinal.go:163]
    runtime proc.go:381        gopark(func(0x0), Pointer(0x0), waitReason(0x0))
    runtime mfinal.go:193      runfinq()
    runtime asm_amd64.s:1598   goexit()
exit status 2
FAIL	github.com/roc-streaming/roc-go/roc	0.025s

We should understand the reason of the crash, and whether it indicates a real bug or not.

Note: there is rocGoLogHandler in the stacktrace.

Add command-line flag to enable logs in tests

Currently, logging to console is disabled in tests. This is achieved by to measures:

  • log_test.go defines defaultLogLevel LogLevel = LogError; all tests that temporary enable logs, restore log level to this value; this level prevents all log messages except errors

  • main_test.go invokes log.SetOutput(ioutil.Discard); all tests that temporary redirect logs, restore log output to ioutil.Discard; this prevents logs to be written to console

However, for debugging, it may be useful to enable console logging.

We can do the following:

  • make defaultLogLevel a variable (default is still LogError)
  • add new global variable defaultLogOutput (default is ioutil.Discard)
  • add command-line flag to tests that enables logs; when the flag is set, defaultLogLevel is set to LogDebug, and defaultLogOutput to console
  • at start, tests should set log level and output to defaultLogLevel and defaultLogOutput
  • find all tests that call log.SetOutput(ioutil.Discard), and instead use log.SetOutput(defaultLogOutput)

Also, I think it makes sense to rename defaultLogLevel and defaultLogOutput to defaultTestLogLevel and defaultTestLogOutput.

Add bindings for reuseaddr option

roc-toolkit 0.2.3 added two new functions:

  • roc_sender_set_reuseaddr
  • roc_receiver_set_reuseaddr

See API reference for description.

We should add two corresponding methods to bindings:

  • Sender.SetReuseaddr(bool)
  • Receiver.SetReuseaddr(bool)

Comments should be copied and adopted from C header.

We also need to add tests for both successful and unsuccessful invocations of the two newly added methods. No need to test how the option itself work, just a simple "smoke" test.

Implement Logger

Hm, it seems that LogSetHandlerImpl generated by c-for-go already stores the function into a global variable:

roc-go/roc/cgo_helpers.go
Lines 682 to 690 in 251b447

 func (x LogHandler) PassValue() (ref C.roc_log_handler, allocs *cgoAllocMap) { 
 	if x == nil { 
 		return nil, nil 
 	} 
 	if logHandler9E4BBC5EFunc == nil { 
 		logHandler9E4BBC5EFunc = x 
 	} 
 	return (C.roc_log_handler)(C.roc_log_handler_9e4bbc5e), nil 
 } 

So we don't need to duplicate this code.

However, it also seems that the generated code is not thread-safe. We should learn whether c-for-go is capable of generating thread-safe code. If it's not, we should implement thread-safety in our wrappers. The goal is to make the entire API thread-safe, just like original C API.

Check if return is nil and return an error along in wrappers

Exactly. I'm suggesting to add the error artificially, like:

func OpenContext(config *ContextConfig) (*Context, error) {
    ctx := ContextOpen(config)
    if ctx == nil {
        return nil, errors.New(...)
    }
    return ctx. nil
}

This may look a bit stupid from the implementation point of view, but this will give the API user a hint that the function can fail and errors should be checked. Otherwise it may seem like it can't fail since there is no error in the signature, which so typical in the Go world.

Add integration test for SetMulticastGroup

Implement integration test that covers Receiver.SetMulticastGroup. See documentation for details.

Things to cover:

  • SetMulticastGroup: succeeds
  • SetMulticastGroup: invalid ip string
  • SetMulticastGroup: roc_receiver_set_multicast_group returns error (see docs for when it happens)

Add end-to-end test with resampler

TestEnd2End_Default is a simple integration test that writes samples to sender, reads them from receiver, and verifies the received stream.

TestEnd2End_Default disables resampler, so that the received stream is only minimally modified, as described in #101.

We should add a new test TestEnd2End_Resampler, that enables resampling on receiver by setting ReceiverConfig.ResamplerBackend and ResamplerProfile.

Since resampler modifies stream in a very complicated way, the acceptance criteria for this test should much more relaxed compared to #101. We can just wait until we accumulate 10'000 non-zero samples, ignoring zeros, and without checking actual sample values. However we still need to use tickers similar to described in #101.

See also: https://roc-streaming.org/toolkit/docs/internals/fe_resampler.html

Use time.Time for LogMessage.Time

LogMessage.Time encodes absolute time, in form of nanoseconds since epoch, as uint64. It would be more convenient to encode it as time.Time instead. Comments and tests should be adjusted accordingly.

Add API documentation

We should add documentation for the package and for all public functions. Most docs can be copied from libroc headers.

  • Endpoint
  • Config
  • Config
  • Context
  • Sender
  • Receiver
  • Log

Add error handling to go2cStr

Currently, if go2cStr meets zero byte in the middle or in the end of the string, it stops early (and also oddly produces two zero bytes instead of one).

Instead, it should report error that the input string is incorrect. Everywhere where we call go2cStr, we should check error and report it to user.

Implement "-f" option in tag.py script

tag.py is a script for creating and pushing git tag (#106).

If the tag already exists locally or remotely, it will fail, which is usually OK, but sometimes we need to override recently created malformed tag.

It would be nice to add an option called -f / --force, which allows to override existing tag, both local and remote (in case when --push is specified).

Optimize how blocking I/O interacts with Go scheduler

Sender and Receiver both have ClockSource field in their config structs, with two possible values:

  • ClockExternal - Receiver.ReadFloats and Sender.WriteFloats are non-blocking; the user is resposible for invoking these methods at appropriate time (in other words, the stream clock is managed by user, thus "external clock")

  • ClockInternal - Receiver.ReadFloats and Sender.WriteFloats are blocking; they will automatically block to align read and write operations with the stream clock (in other words, the stream clock is managed by library, thus "internal clock")

Current implementation of Receiver.ReadFloats and Sender.WriteFloats just invokes corresponding C function; if blocking is needed (for ClockInternal), it is done inside the C code.

However, this does not play perfectly well with the Go scheduler. Scheduler threads will idle until sysmon detects that we've blocked, instead of performing useful work, and then will have to do some extra houskeeping before proceeding.


We could avoid this the following way, for Sender:

  • If ClockSource is ClockInternal, OpenSender creates a channel and background goroutine. Sender.Close asks background goroutine to interrupt and waits until it exits.

  • Background goroutine calls runtime.LockOSThread to detach from Go scheduler and starts reading from channel.

  • Sender.WriteFloats writes frame to channel.

  • Background goroutine reads frames from channel and passes them to the C function.

  • Sender.WriteFloats waits until background goroutine finishes handling the frame.

This way, Sender.WriteFloats will block on a Go channel instead of blocking in C code, which will integrate well with the Go scheduler (it will efficently switch to other goroutines when the channel is blocked). Blocking inside C code will happen on a dedicated goroutine detached from scheduler, which won't harm it.


Receiver should be addressed the same way, but the flow of samples will be the opposite (from C to Go, not from Go to C).


A unit test should be added both to receiver_test.go and sender_test.go for I/O in internal clock mode.

Add end-to-end test without FEC

TestEnd2End_Default is a simple integration test that writes samples to sender, reads them from receiver, and verifies the received stream.

Currently TestEnd2End_Default enables Reed-Solomon forward error correction (FEC) by setting SenderConfig.FecEncoding and binding two endpoints both on sender and receiver: one endpoint for "source" packets (i.e. media) and one for "repair" packets (for error correction).

We need to split it into two tests:

  • TestEnd2End_Default - should not use FEC
  • TestEnd2End_FEC - should use FEC

The new TestEnd2End_Default should disable FEC in sender config and use only a single endpoint rtp://127.0.0.1:0 for source packets, and don't use repair endpoint.

TestEnd2End_Default and TestEnd2End_FEC should share most of the code, so that the algorithm described in #101 would be used for both of them.

Related:

Add end-to-end test with internal clock

TestEnd2End_Default is a simple integration test that writes samples to sender, reads them from receiver, and verifies the received stream.

TestEnd2End_Default sets ClockSource to ClockExternal both in sender and receiver config.

We should add a new test TestEnd2End_Blocking that sets ClockSource to ClockInternal both for sender and receiver.

TestEnd2End_Default and TestEnd2End_Blocking should share most of the code, so that the algorithm described in #101 would be used for both of them. However, in TestEnd2End_Blocking, we should disable tickers described in #101.

For further details about ClockExternal vs ClockInternal, see #100.

Add version checking

When any non-method public function is called first time (except Version func), we need to call Version() to retrieve versions of native lib and go bindings, and check the following invariant:

  • major versions are equal
  • minor version of bindings is greater than or equal to minor version of native lib

(See "Versioning" section in Readme for explanation)

Non-method public functions include: OpenContext, OpenSender, OpenReceicer, SetLogger, SetLoggerFunc, SetLogLevel.

If the invariant is not satisfied, we should panic. Panic message should include both versions and reason.

Add debug logging

Add debug logs for all public methods of Context, Sender, and Receiver, except real-time operations: ReadFloats and WriteFloats.

We should log beginning and end of each operation, so that by looking at logs we can easily say when we entered and exited native code. We can do it by adding something like this to each method (pseudocode):

log(...)
defer log(...)

We should include arguments passed to methods to logs. For enums, let's use stringer tool to generate String() method. https://pkg.go.dev/golang.org/x/tools/cmd/stringer

During Context opening, we should also log versions of bindings and native lib, reported by Version() call.

Logs should be passed to the handler registered via SetLoggerFunc() with log level set to debug. They should take into account log level set by SetLogLevel().

Improve README

  • Overview (briefly about project)
  • Features
  • Documentation (libroc, bindings)
  • Installation (libroc, bindings)
  • Versioning (mapping bindings versions to libroc versions)
  • Development
  • Authors
  • License (note on bindings and library)

Add integration test for SetOutgoingAddress

Implement integration test that covers Sender.SetOutgoingAddress. See documentation for details.

Things to cover:

  • SetOutgoingAddress: succeeds
  • SetOutgoingAddress: invalid ip string
  • SetOutgoingAddress: roc_sender_set_outgoing_address returns error (see docs for when it happens)

Don't modify generated code

We should never modify generated code by hand. Otherwise our edits would be lost after regeneration. But we should be able to regenerate code when needed, e.g. when a new libroc release is out.

See #1 (comment)

Create python script for making release

Currently, release creation involves these steps:

  • open version.go and update bindingsVersion to new version X.Y.Z
  • commit this change
  • create git tag vX.Y.Z
  • push branch and tag
  • wait until CI for git tag passes and creates github release
  • edit github release changelog and publish release

It would be convenient to automate the first half of this procedure. We could create a new script tag.py used as follows:

./tag.py 0.2.1

or:

./tag.py --push origin 0.2.1

This script will:

  • verify that its command-line argument have correct form (int.int.int)
  • verify that the specified tag does not exist yet
  • update bindingsVersion in version.go to the specified tag
  • commit this change
  • create git tag with specified name
  • if --push is given, push the change and the tag to specified remote (e.g. origin)

The script should be cross-platform (thus python).


After doing this, release procedure will look as follows:

  • run ./tag.py --push origin VERSION
  • wait CI
  • edit github release changelog and publish release

We'll need to document (briefly) the new procedure in README.

Reduce allocations on hot path

We should try to avoid allocations when writing and reading frames.

If the user gives us a []float slice, we can get the pointer to the raw memory of the slice data (using unsafe I guess) and pass this pointer to roc_sender_write().

Theoretically this should work because roc_sender_write() does not store this pointer anywhere. It is guaranteed that this memory will not be accessed after the function returns, so this approach should not cause problems with GC (these problems are the reason why cgo uses C.calloc which I'd like to avoid).

See #1 (comment)

Rework error handling

it seems that every function in roc has it own intricate error return. Just one-for-all func convertToErr does not seem to suffice. We need to thoroughly go through every function and emulated the interface.

Add standalone examples

We have "Quick start" section in README which shows how to use sender and receiver.

In addition, we also should add a few complete stand-alone examples, which can be compiled to executables and run from command-line.

We can start with implementing two examples:

  • Sender. A tool that generates a sine wave and sends it to remote Roc receiver.
  • Receiver. A tool that receives sound from remote sender and plays it to speakers using some sound library.

We already have a similar example for the C API: https://roc-streaming.org/toolkit/docs/api/examples.html

The C API is documented here: https://roc-streaming.org/toolkit/docs/api/reference.html
The Go bindings to it are documented here: https://pkg.go.dev/github.com/roc-streaming/roc-go/roc

The file e2e_test.go and "Quick start" section in README provide almost all the code needed for these examples, except generating sine wave and playing sound. For playing sound, we should use some cross-platform go library with simple API that would not complicate example too much.

The code of examples should be well commented and explain each step. Comments can be mostly adopted from the C examples linked above. We should also add a section to README describing what examples we have and how to run them.

Add test for log message struct

Add new test TestLog_Message, similar to TestLog_Func, which does the following:

  • sets log level to LogTrace
  • installs log handler using SetLoggerFunc
  • waits until log handler is called with a message of level LogTrace (trace messages have most fields)
  • and inspects received message

It should check that message's field Level is trace, and fields Module, File, Line, Time, Pid, Tid, Text are non-empty / non-zero.

Hide Frame type from user

Hiding Frame type, i.e. replacing it with frame, and replacing Sender.Write(*Frame) with type-safe wrappers like Sender.WriteAudioBytes([]byte) and Sender.WriteAudioFloats([]float), as described in roc-streaming/roc-toolkit#248.

Exporting something like this to the user:

type Frame struct {
	Samples       unsafe.Pointer
	SamplesSize   uint
}

would be too low-level and unfamiliar for a Go library (because of the unsafe.Pointer).

Add end-to-end tests with multiple senders and receivers

TestEnd2End_Default is a simple integration test that writes samples to sender, reads them from receiver, and verifies the received stream.

This test uses a single sender and a single receiver, however other combinations are supported too:

  • many senders, one receiver
  • one sender, many receivers
  • many senders, many receivers

These combinations are supported using slots. You may create multiple slots on a sender, and connect each slot to different receiver. You can create multiple slots on receiver, and connect senders to different receiver slots. Finally, you can connect multiple senders to the same slot of receiver, but in this case all senders should use the same protocol.

We should cover all these combinations with tests (feel free to suggest better names):

  • TestEnd2End_Connections/one_sender_two_receivers

    Two receivers (each with one slot), and one sender with two slots, connected to two receivers.

  • TestEnd2End_Connections/one_receiver_one_slot_two_senders

    One receiver with one slot, and two senders connected to the same receiver slot.

  • TestEnd2End_Connections/one_receiver_two_slots_two_senders

    One receiver with two slot (ideally with two different protocols), and two senders, each connected to own slot.

  • TestEnd2End_Connections/two_receivers_two_senders

    Two receivers, each with one slot, and two senders, each with two slots, one slot connected to first receiver, and another slot connected to second receiver.

TestEnd2End_Default and TestEnd2End_Connections should share the code for sending and receiving samples, so that the algorithm described in #101 would be used for both of them.

Update to 0.3

Update existing APIs:

  • enums and configs
  • slot configuration
    • roc_interface_config
    • roc_sender_set_outgoing_address() + roc_sender_set_reuseaddr() => roc_sender_configure()
    • roc_receiver_set_multicast_group() + roc_receiver_set_reuseaddr() => roc_receiver_configure()

Add bindings to new APIs:

  • roc_sender_unlink() / roc_receiver_unlink()
  • roc_context_register_encoding()

Improve acceptance criteria of end-to-end test

TestEnd2End_Default is a simple test that works as follows:

  • creates sender and receiver, and connects sender to receiver
  • in the first goroutine, repeatedly writes samples to sender
  • in the second goroutine, repeatedly reads samples from receiver
  • waits until it receives two consecutive non-zero samples and exits

This algorithm is the most basic check that verifies that if we send "something", then we eventually receiver "something", with a very relaxed definition of "something" (two non-zero samples).


It was implemented like this because we can't implement very strict check: we can't rely that we receive the exact same stream as we send.

When resampler and other processing is disabled, a few differences are possible between sent and received streams:

  • received stream can produce arbitrary amount of zero samples before it starts producing the original stream
  • arbitrary chunks of original stream may be replaced with zeros in received stream
  • frame boundaries are not preserved; the first frame written to the sender may start in the middle of the first non-zero frame read from receiver

Keeping this in mind, we can improve TestEnd2End: make the check more strict and meaningful than it is currently, though relaxed enough to take into account possible stream modifications described above.

Here is an algorithm that we could implement in the test:

  • on sender, produce a repeating sequence of incrementing numbers 0 1 2 ... 99 100 0 1 2 ...; we can use this sequency for L channel and the same but negated sequence for R channel

  • on receiver:

    • wait until we receive first non-zero sample

    • check that since then, each sample either have expected value (so that the next sample is previous sample plus one), or is zero (if corresponding packet was lost)

    • also check that either L = -R or L = R = 0

    • wait until we accumulate 10'000 non-zero samples in total (not taking into account possible zero samples)

  • both on sender and receiver, we should limit the speed of invocation of WriteFloats and ReadFloats methods

    For example, if sample rate in sender/receiver config is 44100, we should try to write and read only 44100 samples per seconds; e.g., if our frame size is 100, then we should write frame each 1/441 = 0.002267 seconds.

    We can create tickers for both sender and receiver with corresponding frequency and wait a message from ticker before invoking write or read next time.

Add simple stress test

We should think about adding a stress test which will:

  • intensively use multiple senders, receivers, and contexts from parallel goroutines;
  • constantly check some invariants, e.g. that no operation hangs and each stream continues to receive more non-zero samples.

It would be also good to run in under race detector.

e2e_test.go from #24 can be used as a base for such a test. See also #101 and https://github.com/roc-streaming/roc-toolkit/issues/298 for some ideas on integration and stress tests.

Add go.mod

Add go.mode and use semantic versioning.

Implement finalizers for Context, Sender, Receiver

Context, Sender, and Receiver are long-living objects that own rather heavy native resources (C library handles). User is responsible to call Close() to free those resources.

If user forgets to call Close(), GC will collect go structs, but corresponding native resources will leak. It may be relatively easy to forget calling Close() because this objects typically are not used as local variables for which we can use defer, but instead are leaving in global variables or being part of other long-living objects.

We could reduce damage by forgetting calling Close() by setting finalizers for those objects. Finalizers have performance hit, but it's not critical here because the number of these kind of objects is not high.

Steps:

  • to Sender and Receiver, add unexported field referencing their Context; it will prevent GC to collect Context before Sender and Receiver; it's not allowed to close native context before closing all native senders and receivers attached to it

  • in OpenContext, OpenSender, OpenReceiver, attach finalizer to the created object; finalizer should just invoke Close method

  • in Close method, detach finalizer from the object; so that if the user explicitly closes the object, finalizers are not needed anymore

  • add tests for context, sender, and receiver; check that after creating those objects and forgetting to close them, and after repeatedly calling GC() in a loop, all those objects are closed automatically

Add test for context closing

Add test which checks that if there are receiver or sender attached to context, Context.Close returns error; and when there are no more receivers and senders, context can be successfully closed.

Improve integration tests for sender and receiver

Currently we have one simple test (sender_receiver_test.go added in #24), which runs sender in receiver in two goroutines and waits until receiver will get a few non-zero samples.

It would be nice to add a few more sophisticated tests. We don't need to test all possible combinations in bindings, since we assume that the underlying library is already tested; however covering a few important features and checking that everything works as expected would be helpful.

Things to cover:

  • Tests with and without FEC. The checks can be same, we just need to ensure that both configuration work.

  • Tests with and without resampler. When resampler is enabled, it's hard to compare streams, and we should just check that we receive some amount of non-zero samples. When resampler is disabled, on the other hand, the receiver wont modify the stream, and the only difference between input and output streams are possibles losses (when FEC can't recover packets) and shifts. In this case we can perform a more strict check and ensure that we receive the expected stream, except possible losses and shifts.

  • Test for external and internal clock sources. See documentation for details; internal clock source means clocking by CPU timer inside sender or receiver, and external clock source means clocking by user, i.e. requires calling read and write at appropriate time.

  • Tests for multiple contexts, senders, and receivers. Possible combination are: one receiver or sender per context, or multiple receivers and sender sharing same context; one sender connected to receiver, or multiple senders connected to a single receiver.

For background, see the following links:

Add bindings for roc_sender_encoder and roc_sender_decoder

Follow-up for #118.

We already have bindings for roc_sender and roc_receiver (see sender.go and receiver.go).

Roc Toolkit 0.3 introduced a networkless version of this API: roc_sender_encoder and roc_sender_decoder. We need to add bindings and unit tests for this new API.

Documentation for C API can be found here: https://roc-streaming.org/toolkit/docs/api/reference.html

C headers for new API are here: sender_encoder.h, receiver_decoder.h.

Use time.Duration for time intervals

Find fields in SenderConfig and ReceiverConfig that hold time intervals, and switch their type from plane integer to time.Duration. Adjust comments and implementation accordingly.

Fields of interest:

  • SenderConfig:

    • PacketLength
  • ReceiverConfig

    • TargetLatency
    • MaxLatencyOverrun
    • MaxLatencyUnderrun
    • NoPlaybackTimeout
    • BrokenPlaybackTimeout
    • BreakageDetectionWindow

To avoid copy-paste in implementation, we'll need to add new conversion functions (between time.Duration and C.longlong / C.ulonglong) to convert.go.

We'll need separate functions for conversion to signed and unsigned longs. (unsigned version should return error when it encounters negative duration). New functions should be covered with tests in convert_test.go.

Use stringer for all enums

We have a lot of enums in config.go and log.go, and it would be convenient if they all had String() method that returned enum's symbolic representation. This can be easily achieved using stringer tool.

Steps:

  • add stringer magic comment to all enums: Interface, Protocol, FecEncoding, PacketEncoding, FrameEncoding, ChannelSet, ResamplerBackend, ResamplerProfile, ClockSource, LogLevel (note: no need to add it to Slot, it's not a enum)

  • configure stringer to trim enum's prefix

  • add Makefile target that runs go generate and document it in README

  • add CI step (in build.yml) checking that we did not forget to run go generate and commit results (it may run go generate on CI and then check git status)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.