1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

For regression test, add srs-bench to 3rdparty

This commit is contained in:
winlin 2021-03-04 13:23:01 +08:00
parent de87dd427d
commit 876210f6c9
1158 changed files with 256967 additions and 3 deletions

View file

@ -0,0 +1,3 @@
---
exclude_paths:
- examples/examples.json

View file

@ -0,0 +1,3 @@
{
"extends": ["standard"]
}

View file

@ -0,0 +1,24 @@
### JetBrains IDE ###
#####################
.idea/
### Emacs Temporary Files ###
#############################
*~
### Folders ###
###############
bin/
vendor/
node_modules/
### Files ###
#############
*.ivf
*.ogg
tags
cover.out
*.sw[poe]
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem

View file

@ -0,0 +1,89 @@
linters-settings:
govet:
check-shadowing: true
misspell:
locale: US
exhaustive:
default-signifies-exhaustive: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bodyclose # checks whether HTTP response body is closed successfully
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- gci # Gci control golang package import order and make it always deterministic.
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goerr113 # Golang linter to check the errors handling expressions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goheader # Checks is file header matches to pattern
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- noctx # noctx finds sending http request without context.Context
- scopelint # Scopelint checks for unpinned variables in go programs
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- structcheck # Finds unused struct fields
- stylecheck # Stylecheck is a replacement for golint
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- funlen # Tool for detection of long functions
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- gomnd # An analyzer to detect magic numbers.
- lll # Reports long lines
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted
- nestif # Reports deeply nested if statements
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- wsl # Whitespace Linter - Forces you to use empty lines!
issues:
exclude-use-default: false
exclude-rules:
# Allow complex tests, better to be self contained
- path: _test\.go
linters:
- gocognit
# Allow complex main function in examples
- path: examples
text: "of func `main` is high"
linters:
- gocognit
run:
skip-dirs-use-default: false

View file

@ -0,0 +1,43 @@
<h1 align="center">
Design
</h1>
WebRTC is a powerful, but complicated technology you can build amazing things with, it comes with a steep learning curve though.
Using WebRTC in the browser is easy, but outside the browser is more of a challenge. There are multiple libraries, and they all have
varying levels of quality. Most are also difficult to build, and depend on libraries that aren't available in repos or portable.
Pion WebRTC aims to solve all that! Built in native Go you should be able to send and receive media and text from anywhere with minimal headache.
These are the design principals that drive Pion WebRTC and hopefully convince you it is worth a try.
### Portable
Pion WebRTC is written in Go and extremely portable. Anywhere Golang runs, Pion WebRTC should work as well! Instead of dealing with complicated
cross-compiling of multiple libraries, you now can run anywhere with one `go build`
### Flexible
When possible we leave all decisions to the user. When choice is possible (like what logging library is used) we defer to the developer.
### Simple API
If you know how to use WebRTC in your browser, you know how to use Pion WebRTC.
We try our best just to duplicate the Javascript API, so your code can look the same everywhere.
If this is your first time using WebRTC, don't worry! We have multiple [examples](https://github.com/pion/webrtc/tree/master/examples) and [GoDoc](https://pkg.go.dev/github.com/pion/webrtc/v3)
### Bring your own media
Pion WebRTC doesn't make any assumptions about where your audio, video or text come from. You can use FFmpeg, GStreamer, MLT or just serve a video file.
This library only serves to transport, not create media.
### Safe
Golang provides a great foundation to build safe network services.
Especially when running a networked service that is highly concurrent bugs can be devastating.
### Readable
If code comes from an RFC we try to make sure everything is commented with a link to the spec.
This makes learning and debugging easier, this WebRTC library was written to also serve as a guide for others.
### Tested
Every commit is tested via travis-ci Go provides fantastic facilities for testing, and more will be added as time goes on.
### Shared libraries
Every Pion project is built using shared libraries, allowing others to review and reuse our libraries.
### Community
The most important part of Pion is the community. This projects only exist because of individual contributions. We aim to be radically open and do everything we can to support those that make Pion possible.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,264 @@
<h1 align="center">
<a href="https://pion.ly"><img src="./.github/pion-gopher-webrtc.png" alt="Pion WebRTC" height="250px"></a>
<br>
Pion WebRTC
<br>
</h1>
<h4 align="center">A pure Go implementation of the WebRTC API</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-webrtc-gray.svg?longCache=true&colorB=brightgreen" alt="Pion webrtc"></a>
<a href="https://sourcegraph.com/github.com/pion/webrtc?badge"><img src="https://sourcegraph.com/github.com/pion/webrtc/-/badge.svg" alt="Sourcegraph Widget"></a>
<a href="https://pion.ly/slack"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<a href="https://twitter.com/_pion?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40_pion&style=social&url=https%3A%2F%2Ftwitter.com%2F_pion" alt="Twitter Widget"></a>
<a href="https://github.com/pion/awesome-pion" alt="Awesome Pion"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
<br>
<a href="https://travis-ci.org/pion/webrtc"><img src="https://travis-ci.org/pion/webrtc.svg?branch=master" alt="Build Status"></a>
<a href="https://pkg.go.dev/github.com/pion/webrtc/v3"><img src="https://pkg.go.dev/badge/github.com/pion/webrtc/v3" alt="PkgGoDev"></a>
<a href="https://codecov.io/gh/pion/webrtc"><img src="https://codecov.io/gh/pion/webrtc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/webrtc"><img src="https://goreportcard.com/badge/github.com/pion/webrtc" alt="Go Report Card"></a>
<a href="https://www.codacy.com/app/Sean-Der/webrtc"><img src="https://api.codacy.com/project/badge/Grade/18f4aec384894e6aac0b94effe51961d" alt="Codacy Badge"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
### New Release
Pion WebRTC v3.0.0 has been released! See the [release notes](https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0) to learn about new features and breaking changes.
If you aren't able to upgrade yet check the [tags](https://github.com/pion/webrtc/tags) for the latest `v2` release.
We would love your feedback! Please create GitHub issues or join [the Slack channel](https://pion.ly/slack) to follow development and speak with the maintainers.
----
### Usage
[Go Modules](https://blog.golang.org/using-go-modules) are mandatory for using Pion WebRTC. So make sure you set `export GO111MODULE=on`, and explicitly specify `/v2` or `/v3` when importing.
**[example applications](examples/README.md)** contains code samples of common things people build with Pion WebRTC.
**[example-webrtc-applications](https://github.com/pion/example-webrtc-applications)** contains more full featured examples that use 3rd party libraries.
**[awesome-pion](https://github.com/pion/awesome-pion)** contains projects that have used Pion, and serve as real world examples of usage.
**[GoDoc](https://pkg.go.dev/github.com/pion/webrtc/v3)** is an auto generated API reference. All our Public APIs are commented.
**[FAQ](https://github.com/pion/webrtc/wiki/FAQ)** has answers to common questions. If you have a question not covered please ask in [Slack](https://pion.ly/slack) we are always looking to expand it.
Now go build something awesome! Here are some **ideas** to get your creative juices flowing:
* Send a video file to multiple browser in real time for perfectly synchronized movie watching.
* Send a webcam on an embedded device to your browser with no additional server required!
* Securely send data between two servers, without using pub/sub.
* Record your webcam and do special effects server side.
* Build a conferencing application that processes audio/video and make decisions off of it.
* Remotely control a robots and stream its cameras in realtime.
### Want to learn more about WebRTC?
Check out [WebRTC for the Curious](https://webrtcforthecurious.com). A book about WebRTC in depth, not just about the APIs.
Learn the full details of ICE, SCTP, DTLS, SRTP, and how they work together to make up the WebRTC stack.
This is also a great resource if you are trying to debug. Learn the tools of the trade and how to approach WebRTC issues.
This book is vendor agnostic and will not have any Pion specific information.
### Features
#### PeerConnection API
* Go implementation of [webrtc-pc](https://w3c.github.io/webrtc-pc/) and [webrtc-stats](https://www.w3.org/TR/webrtc-stats/)
* DataChannels
* Send/Receive audio and video
* Renegotiation
* Plan-B and Unified Plan
* [SettingEngine](https://pkg.go.dev/github.com/pion/webrtc/v3#SettingEngine) for Pion specific extensions
#### Connectivity
* Full ICE Agent
* ICE Restart
* Trickle ICE
* STUN
* TURN (UDP, TCP, DTLS and TLS)
* mDNS candidates
#### DataChannels
* Ordered/Unordered
* Lossy/Lossless
#### Media
* API with direct RTP/RTCP access
* Opus, PCM, H264, VP8 and VP9 packetizer
* API also allows developer to pass their own packetizer
* IVF, Ogg, H264 and Matroska provided for easy sending and saving
* [getUserMedia](https://github.com/pion/mediadevices) implementation (Requires Cgo)
* Easy integration with x264, libvpx, GStreamer and ffmpeg.
* [Simulcast](https://github.com/pion/webrtc/tree/master/examples/simulcast)
* [SVC](https://github.com/pion/rtp/blob/master/codecs/vp9_packet.go#L138)
* [NACK](https://github.com/pion/interceptor/pull/4)
* Full loss recovery and congestion control is not complete, see [pion/interceptor](https://github.com/pion/interceptor) for progress
* See [ion](https://github.com/pion/ion-sfu/tree/master/pkg/buffer) for how an implementor can do it today
#### Security
* TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 and TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA for DTLS v1.2
* SRTP_AEAD_AES_256_GCM and SRTP_AES128_CM_HMAC_SHA1_80 for SRTP
* Hardware acceleration available for GCM suites
#### Pure Go
* No Cgo usage
* Wide platform support
* Windows, macOS, Linux, FreeBSD
* iOS, Android
* [WASM](https://github.com/pion/webrtc/wiki/WebAssembly-Development-and-Testing) see [examples](examples/README.md#webassembly)
* 386, amd64, arm, mips, ppc64
* Easy to build *Numbers generated on Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz*
* **Time to build examples/play-from-disk** - 0.66s user 0.20s system 306% cpu 0.279 total
* **Time to run entire test suite** - 25.60s user 9.40s system 45% cpu 1:16.69 total
* Tools to measure performance [provided](https://github.com/pion/rtsp-bench)
### Roadmap
The library is in active development, please refer to the [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
We also maintain a list of [Big Ideas](https://github.com/pion/webrtc/wiki/Big-Ideas) these are things we want to build but don't have a clear plan or the resources yet.
If you are looking to get involved this is a great place to get started! We would also love to hear your ideas! Even if you can't implement it yourself, it could inspire others.
### Community
Pion has an active community on the [Slack](https://pion.ly/slack).
Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news.
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
* [John Bradley](https://github.com/kc5nra) - *Original Author*
* [Michael Melvin Santry](https://github.com/santrym) - *Mascot*
* [Raphael Randschau](https://github.com/nicolai86) - *STUN*
* [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
* [Michiel De Backker](https://github.com/backkem) - *SDP, Public API, Project Management*
* [Brendan Rius](https://github.com/brendanrius) - *Cleanup*
* [Konstantin Itskov](https://github.com/trivigy) - *SDP Parsing*
* [chenkaiC4](https://github.com/chenkaiC4) - *Fix GolangCI Linter*
* [Ronan J](https://github.com/ronanj) - *Fix STCP PPID*
* [wattanakorn495](https://github.com/wattanakorn495)
* [Max Hawkins](https://github.com/maxhawkins) - *RTCP*
* [Justin Okamoto](https://github.com/justinokamoto) - *Fix Docs*
* [leeoxiang](https://github.com/notedit) - *Implement Janus examples*
* [Denis](https://github.com/Hixon10) - *Adding docker-compose to pion-to-pion example*
* [earle](https://github.com/aguilEA) - *Generate DTLS fingerprint in Go*
* [Jake B](https://github.com/silbinarywolf) - *Fix Windows installation instructions*
* [Michael MacDonald](https://github.com/mjmac) - *Plan B compatibility, Remote TURN/Trickle-ICE, Logging framework*
* [Oleg Kovalov](https://github.com/cristaloleg) *Use wildcards instead of hardcoding travis-ci config*
* [Woodrow Douglass](https://github.com/wdouglass) *RTCP, RTP improvements, G.722 support, Bugfixes*
* [Tobias Fridén](https://github.com/tobiasfriden) *SRTP authentication verification*
* [Yutaka Takeda](https://github.com/enobufs) *Fix ICE connection timeout*
* [Hugo Arregui](https://github.com/hugoArregui) *Fix connection timeout*
* [Rob Deutsch](https://github.com/rob-deutsch) *RTPReceiver graceful shutdown*
* [Jin Lei](https://github.com/jinleileiking) - *SFU example use http*
* [Will Watson](https://github.com/wwatson) - *Enable gocritic*
* [Luke Curley](https://github.com/kixelated)
* [Antoine Baché](https://github.com/Antonito) - *OGG Opus export*
* [frank](https://github.com/feixiao) - *Building examples on OSX*
* [mxmCherry](https://github.com/mxmCherry)
* [Alex Browne](https://github.com/albrow) - *JavaScript/Wasm bindings*
* [adwpc](https://github.com/adwpc) - *SFU example with websocket*
* [imalic3](https://github.com/imalic3) - *SFU websocket example with datachannel broadcast*
* [Žiga Željko](https://github.com/zigazeljko)
* [Simonacca Fotokite](https://github.com/simonacca-fotokite)
* [Marouane](https://github.com/nindolabs) *Fix Offer bundle generation*
* [Christopher Fry](https://github.com/christopherfry)
* [Adam Kiss](https://github.com/masterada)
* [xsbchen](https://github.com/xsbchen)
* [Alex Harford](https://github.com/alexjh)
* [Aleksandr Razumov](https://github.com/ernado)
* [mchlrhw](https://github.com/mchlrhw)
* [AlexWoo(武杰)](https://github.com/AlexWoo) *Fix RemoteDescription parsing for certificate fingerprint*
* [Cecylia Bocovich](https://github.com/cohosh)
* [Slugalisk](https://github.com/slugalisk)
* [Agugua Kenechukwu](https://github.com/spaceCh1mp)
* [Ato Araki](https://github.com/atotto)
* [Rafael Viscarra](https://github.com/rviscarra)
* [Mike Coleman](https://github.com/fivebats)
* [Suhas Gaddam](https://github.com/suhasgaddam)
* [Atsushi Watanabe](https://github.com/at-wat)
* [Robert Eperjesi](https://github.com/epes)
* [Aaron France](https://github.com/AeroNotix)
* [Gareth Hayes](https://github.com/gazhayes)
* [Sebastian Waisbrot](https://github.com/seppo0010)
* [Masataka Hisasue](https://github.com/sylba2050) - *Fix Docs*
* [Hongchao Ma(马洪超)](https://github.com/hcm007)
* [Aaron France](https://github.com/AeroNotix)
* [Chris Hiszpanski](https://github.com/thinkski) - *Fix Answer bundle generation*
* [Vicken Simonian](https://github.com/vsimon)
* [Guilherme Souza](https://github.com/gqgs)
* [Andrew N. Shalaev](https://github.com/isqad)
* [David Hamilton](https://github.com/dihamilton)
* [Ilya Mayorov](https://github.com/faroyam)
* [Patrick Lange](https://github.com/langep)
* [cyannuk](https://github.com/cyannuk)
* [Lukas Herman](https://github.com/lherman-cs)
* [Konstantin Chugalinskiy](https://github.com/kchugalinskiy)
* [Bao Nguyen](https://github.com/sysbot)
* [Luke S](https://github.com/encounter)
* [Hendrik Hofstadt](https://github.com/hendrikhofstadt)
* [Clayton McCray](https://github.com/ClaytonMcCray)
* [lawl](https://github.com/lawl)
* [Jorropo](https://github.com/Jorropo)
* [Akil](https://github.com/akilude)
* [Quentin Renard](https://github.com/asticode)
* [opennota](https://github.com/opennota)
* [Simon Eisenmann](https://github.com/longsleep)
* [Ben Weitzman](https://github.com/benweitzman)
* [Masahiro Nakamura](https://github.com/tsuu32)
* [Tarrence van As](https://github.com/tarrencev)
* [Yuki Igarashi](https://github.com/bonprosoft)
* [Egon Elbre](https://github.com/egonelbre)
* [Jerko Steiner](https://github.com/jeremija)
* [Roman Romanenko](https://github.com/r-novel)
* [YongXin SHI](https://github.com/a-wing)
* [Magnus Wahlstrand](https://github.com/kyeett)
* [Chad Retz](https://github.com/cretz)
* [Simone Gotti](https://github.com/sgotti)
* [Cedric Fung](https://github.com/cedricfung)
* [Norman Rasmussen](https://github.com/normanr) - *Fix Empty DataChannel messages*
* [salmān aljammāz](https://github.com/saljam)
* [cnderrauber](https://github.com/cnderrauber)
* [Juliusz Chroboczek](https://github.com/jech)
* [John Berthels](https://github.com/jbert)
* [Somers Matthews](https://github.com/somersbmatthews)
* [Vitaliy F](https://github.com/funvit)
* [Ivan Egorov](https://github.com/vany-egorov)
* [Nick Mykins](https://github.com/nmyk)
* [Jason Brady](https://github.com/jbrady42)
* [krishna chiatanya](https://github.com/kittuov)
* [JacobZwang](https://github.com/JacobZwang)
* [박종훈](https://github.com/JonghunBok)
* [Sam Lancia](https://github.com/nerd2)
* [Henry](https://github.com/cryptix)
* [Jeff Tchang](https://github.com/tachang)
* [JooYoung Lim](https://github.com/DevRockstarZ)
* [Sidney San Martín](https://github.com/s4y)
* [soolaugust](https://github.com/soolaugust)
* [Kuzmin Vladimir](https://github.com/tekig)
* [Alessandro Ros](https://github.com/aler9)
* [Thomas Miller](https://github.com/tmiv)
* [yoko(q191201771)](https://github.com/q191201771)
* [Joshua Obasaju](https://github.com/obasajujoshua31)
* [Mission Liao](https://github.com/mission-liao)
* [Hanjun Kim](https://github.com/hallazzang)
* [ZHENK](https://github.com/scorpionknifes)
* [Rahul Nakre](https://github.com/rahulnakre)
* [OrlandoCo](https://github.com/OrlandoCo)
* [Assad Obaid](https://github.com/assadobaid)
* [Jamie Good](https://github.com/jamiegood) - *Bug fix in jsfiddle example*
* [Artur Shellunts](https://github.com/ashellunts)
* [Sean Knight](https://github.com/SeanKnight)
* [o0olele](https://github.com/o0olele)
* [Bo Shi](https://github.com/bshimc)
* [Suzuki Takeo](https://github.com/BambooTuna)
* [baiyufei](https://github.com/baiyufei)
* [pascal-ace](https://github.com/pascal-ace)
* [Threadnaught](https://github.com/Threadnaught)
* [Dean Eigenmann](https://github.com/decanus)
### License
MIT License - see [LICENSE](LICENSE) for full text

View file

@ -0,0 +1,73 @@
// +build !js
package webrtc
import (
"github.com/pion/interceptor"
"github.com/pion/logging"
)
// API bundles the global functions of the WebRTC and ORTC API.
// Some of these functions are also exported globally using the
// defaultAPI object. Note that the global version of the API
// may be phased out in the future.
type API struct {
settingEngine *SettingEngine
mediaEngine *MediaEngine
interceptor interceptor.Interceptor
}
// NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects
func NewAPI(options ...func(*API)) *API {
a := &API{}
for _, o := range options {
o(a)
}
if a.settingEngine == nil {
a.settingEngine = &SettingEngine{}
}
if a.settingEngine.LoggerFactory == nil {
a.settingEngine.LoggerFactory = logging.NewDefaultLoggerFactory()
}
if a.mediaEngine == nil {
a.mediaEngine = &MediaEngine{}
}
if a.interceptor == nil {
a.interceptor = &interceptor.NoOp{}
}
return a
}
// WithMediaEngine allows providing a MediaEngine to the API.
// Settings can be changed after passing the engine to an API.
func WithMediaEngine(m *MediaEngine) func(a *API) {
return func(a *API) {
if m != nil {
a.mediaEngine = m
} else {
a.mediaEngine = &MediaEngine{}
}
}
}
// WithSettingEngine allows providing a SettingEngine to the API.
// Settings should not be changed after passing the engine to an API.
func WithSettingEngine(s SettingEngine) func(a *API) {
return func(a *API) {
a.settingEngine = &s
}
}
// WithInterceptorRegistry allows providing Interceptors to the API.
// Settings should not be changed after passing the registry to an API.
func WithInterceptorRegistry(interceptorRegistry *interceptor.Registry) func(a *API) {
return func(a *API) {
a.interceptor = interceptorRegistry.Build()
}
}

View file

@ -0,0 +1,31 @@
// +build js,wasm
package webrtc
// API bundles the global funcions of the WebRTC and ORTC API.
type API struct {
settingEngine *SettingEngine
}
// NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects
func NewAPI(options ...func(*API)) *API {
a := &API{}
for _, o := range options {
o(a)
}
if a.settingEngine == nil {
a.settingEngine = &SettingEngine{}
}
return a
}
// WithSettingEngine allows providing a SettingEngine to the API.
// Settings should not be changed after passing the engine to an API.
func WithSettingEngine(s SettingEngine) func(a *API) {
return func(a *API) {
a.settingEngine = &s
}
}

View file

@ -0,0 +1,20 @@
package webrtc
import "sync/atomic"
type atomicBool struct {
val int32
}
func (b *atomicBool) set(value bool) { // nolint: unparam
var i int32
if value {
i = 1
}
atomic.StoreInt32(&(b.val), i)
}
func (b *atomicBool) get() bool {
return atomic.LoadInt32(&(b.val)) != 0
}

View file

@ -0,0 +1,78 @@
package webrtc
import (
"encoding/json"
)
// BundlePolicy affects which media tracks are negotiated if the remote
// endpoint is not bundle-aware, and what ICE candidates are gathered. If the
// remote endpoint is bundle-aware, all media tracks and data channels are
// bundled onto the same transport.
type BundlePolicy int
const (
// BundlePolicyBalanced indicates to gather ICE candidates for each
// media type in use (audio, video, and data). If the remote endpoint is
// not bundle-aware, negotiate only one audio and video track on separate
// transports.
BundlePolicyBalanced BundlePolicy = iota + 1
// BundlePolicyMaxCompat indicates to gather ICE candidates for each
// track. If the remote endpoint is not bundle-aware, negotiate all media
// tracks on separate transports.
BundlePolicyMaxCompat
// BundlePolicyMaxBundle indicates to gather ICE candidates for only
// one track. If the remote endpoint is not bundle-aware, negotiate only
// one media track.
BundlePolicyMaxBundle
)
// This is done this way because of a linter.
const (
bundlePolicyBalancedStr = "balanced"
bundlePolicyMaxCompatStr = "max-compat"
bundlePolicyMaxBundleStr = "max-bundle"
)
func newBundlePolicy(raw string) BundlePolicy {
switch raw {
case bundlePolicyBalancedStr:
return BundlePolicyBalanced
case bundlePolicyMaxCompatStr:
return BundlePolicyMaxCompat
case bundlePolicyMaxBundleStr:
return BundlePolicyMaxBundle
default:
return BundlePolicy(Unknown)
}
}
func (t BundlePolicy) String() string {
switch t {
case BundlePolicyBalanced:
return bundlePolicyBalancedStr
case BundlePolicyMaxCompat:
return bundlePolicyMaxCompatStr
case BundlePolicyMaxBundle:
return bundlePolicyMaxBundleStr
default:
return ErrUnknownType.Error()
}
}
// UnmarshalJSON parses the JSON-encoded data and stores the result
func (t *BundlePolicy) UnmarshalJSON(b []byte) error {
var val string
if err := json.Unmarshal(b, &val); err != nil {
return err
}
*t = newBundlePolicy(val)
return nil
}
// MarshalJSON returns the JSON encoding
func (t BundlePolicy) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}

View file

@ -0,0 +1,185 @@
// +build !js
package webrtc
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"time"
"github.com/pion/dtls/v2/pkg/crypto/fingerprint"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
// Certificate represents a x509Cert used to authenticate WebRTC communications.
type Certificate struct {
privateKey crypto.PrivateKey
x509Cert *x509.Certificate
statsID string
}
// NewCertificate generates a new x509 compliant Certificate to be used
// by DTLS for encrypting data sent over the wire. This method differs from
// GenerateCertificate by allowing to specify a template x509.Certificate to
// be used in order to define certificate parameters.
func NewCertificate(key crypto.PrivateKey, tpl x509.Certificate) (*Certificate, error) {
var err error
var certDER []byte
switch sk := key.(type) {
case *rsa.PrivateKey:
pk := sk.Public()
tpl.SignatureAlgorithm = x509.SHA256WithRSA
certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
case *ecdsa.PrivateKey:
pk := sk.Public()
tpl.SignatureAlgorithm = x509.ECDSAWithSHA256
certDER, err = x509.CreateCertificate(rand.Reader, &tpl, &tpl, pk, sk)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
default:
return nil, &rtcerr.NotSupportedError{Err: ErrPrivateKeyType}
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
return &Certificate{privateKey: key, x509Cert: cert, statsID: fmt.Sprintf("certificate-%d", time.Now().UnixNano())}, nil
}
// Equals determines if two certificates are identical by comparing both the
// secretKeys and x509Certificates.
func (c Certificate) Equals(o Certificate) bool {
switch cSK := c.privateKey.(type) {
case *rsa.PrivateKey:
if oSK, ok := o.privateKey.(*rsa.PrivateKey); ok {
if cSK.N.Cmp(oSK.N) != 0 {
return false
}
return c.x509Cert.Equal(o.x509Cert)
}
return false
case *ecdsa.PrivateKey:
if oSK, ok := o.privateKey.(*ecdsa.PrivateKey); ok {
if cSK.X.Cmp(oSK.X) != 0 || cSK.Y.Cmp(oSK.Y) != 0 {
return false
}
return c.x509Cert.Equal(o.x509Cert)
}
return false
default:
return false
}
}
// Expires returns the timestamp after which this certificate is no longer valid.
func (c Certificate) Expires() time.Time {
if c.x509Cert == nil {
return time.Time{}
}
return c.x509Cert.NotAfter
}
// GetFingerprints returns the list of certificate fingerprints, one of which
// is computed with the digest algorithm used in the certificate signature.
func (c Certificate) GetFingerprints() ([]DTLSFingerprint, error) {
fingerprintAlgorithms := []crypto.Hash{crypto.SHA256}
res := make([]DTLSFingerprint, len(fingerprintAlgorithms))
i := 0
for _, algo := range fingerprintAlgorithms {
name, err := fingerprint.StringFromHash(algo)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err)
}
value, err := fingerprint.Fingerprint(c.x509Cert, algo)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrFailedToGenerateCertificateFingerprint, err)
}
res[i] = DTLSFingerprint{
Algorithm: name,
Value: value,
}
}
return res[:i+1], nil
}
// GenerateCertificate causes the creation of an X.509 certificate and
// corresponding private key.
func GenerateCertificate(secretKey crypto.PrivateKey) (*Certificate, error) {
origin := make([]byte, 16)
/* #nosec */
if _, err := rand.Read(origin); err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
// Max random value, a 130-bits integer, i.e 2^130 - 1
maxBigInt := new(big.Int)
/* #nosec */
maxBigInt.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(maxBigInt, big.NewInt(1))
/* #nosec */
serialNumber, err := rand.Int(rand.Reader, maxBigInt)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
return NewCertificate(secretKey, x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
BasicConstraintsValid: true,
NotBefore: time.Now(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
NotAfter: time.Now().AddDate(0, 1, 0),
SerialNumber: serialNumber,
Version: 2,
Subject: pkix.Name{CommonName: hex.EncodeToString(origin)},
IsCA: true,
})
}
// CertificateFromX509 creates a new WebRTC Certificate from a given PrivateKey and Certificate
//
// This can be used if you want to share a certificate across multiple PeerConnections
func CertificateFromX509(privateKey crypto.PrivateKey, certificate *x509.Certificate) Certificate {
return Certificate{privateKey, certificate, fmt.Sprintf("certificate-%d", time.Now().UnixNano())}
}
func (c Certificate) collectStats(report *statsReportCollector) error {
report.Collecting()
fingerPrintAlgo, err := c.GetFingerprints()
if err != nil {
return err
}
base64Certificate := base64.RawURLEncoding.EncodeToString(c.x509Cert.Raw)
stats := CertificateStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeCertificate,
ID: c.statsID,
Fingerprint: fingerPrintAlgo[0].Value,
FingerprintAlgorithm: fingerPrintAlgo[0].Algorithm,
Base64Certificate: base64Certificate,
IssuerCertificateID: c.x509Cert.Issuer.String(),
}
report.Collect(stats.ID, stats)
return nil
}

View file

@ -0,0 +1,20 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

View file

@ -0,0 +1,51 @@
// +build !js
package webrtc
// A Configuration defines how peer-to-peer communication via PeerConnection
// is established or re-established.
// Configurations may be set up once and reused across multiple connections.
// Configurations are treated as readonly. As long as they are unmodified,
// they are safe for concurrent use.
type Configuration struct {
// ICEServers defines a slice describing servers available to be used by
// ICE, such as STUN and TURN servers.
ICEServers []ICEServer `json:"iceServers,omitempty"`
// ICETransportPolicy indicates which candidates the ICEAgent is allowed
// to use.
ICETransportPolicy ICETransportPolicy `json:"iceTransportPolicy,omitempty"`
// BundlePolicy indicates which media-bundling policy to use when gathering
// ICE candidates.
BundlePolicy BundlePolicy `json:"bundlePolicy,omitempty"`
// RTCPMuxPolicy indicates which rtcp-mux policy to use when gathering ICE
// candidates.
RTCPMuxPolicy RTCPMuxPolicy `json:"rtcpMuxPolicy,omitempty"`
// PeerIdentity sets the target peer identity for the PeerConnection.
// The PeerConnection will not establish a connection to a remote peer
// unless it can be successfully authenticated with the provided name.
PeerIdentity string `json:"peerIdentity"`
// Certificates describes a set of certificates that the PeerConnection
// uses to authenticate. Valid values for this parameter are created
// through calls to the GenerateCertificate function. Although any given
// DTLS connection will use only one certificate, this attribute allows the
// caller to provide multiple certificates that support different
// algorithms. The final certificate will be selected based on the DTLS
// handshake, which establishes which certificates are allowed. The
// PeerConnection implementation selects which of the certificates is
// used for a given connection; how certificates are selected is outside
// the scope of this specification. If this value is absent, then a default
// set of certificates is generated for each PeerConnection instance.
Certificates []Certificate `json:"certificates,omitempty"`
// ICECandidatePoolSize describes the size of the prefetched ICE pool.
ICECandidatePoolSize uint8 `json:"iceCandidatePoolSize,omitempty"`
// SDPSemantics controls the type of SDP offers accepted by and
// SDP answers generated by the PeerConnection.
SDPSemantics SDPSemantics `json:"sdpSemantics,omitempty"`
}

View file

@ -0,0 +1,24 @@
package webrtc
import "strings"
// getICEServers side-steps the strict parsing mode of the ice package
// (as defined in https://tools.ietf.org/html/rfc7064) by copying and then
// stripping any erroneous queries from "stun(s):" URLs before parsing.
func (c Configuration) getICEServers() []ICEServer {
iceServers := append([]ICEServer{}, c.ICEServers...)
for iceServersIndex := range iceServers {
iceServers[iceServersIndex].URLs = append([]string{}, iceServers[iceServersIndex].URLs...)
for urlsIndex, rawURL := range iceServers[iceServersIndex].URLs {
if strings.HasPrefix(rawURL, "stun") {
// strip the query from "stun(s):" if present
parts := strings.Split(rawURL, "?")
rawURL = parts[0]
}
iceServers[iceServersIndex].URLs[urlsIndex] = rawURL
}
}
return iceServers
}

View file

@ -0,0 +1,35 @@
// +build js,wasm
package webrtc
// Configuration defines a set of parameters to configure how the
// peer-to-peer communication via PeerConnection is established or
// re-established.
type Configuration struct {
// ICEServers defines a slice describing servers available to be used by
// ICE, such as STUN and TURN servers.
ICEServers []ICEServer
// ICETransportPolicy indicates which candidates the ICEAgent is allowed
// to use.
ICETransportPolicy ICETransportPolicy
// BundlePolicy indicates which media-bundling policy to use when gathering
// ICE candidates.
BundlePolicy BundlePolicy
// RTCPMuxPolicy indicates which rtcp-mux policy to use when gathering ICE
// candidates.
RTCPMuxPolicy RTCPMuxPolicy
// PeerIdentity sets the target peer identity for the PeerConnection.
// The PeerConnection will not establish a connection to a remote peer
// unless it can be successfully authenticated with the provided name.
PeerIdentity string
// Certificates are not supported in the JavaScript/Wasm bindings.
// Certificates []Certificate
// ICECandidatePoolSize describes the size of the prefetched ICE pool.
ICECandidatePoolSize uint8
}

View file

@ -0,0 +1,25 @@
package webrtc
const (
// Unknown defines default public constant to use for "enum" like struct
// comparisons when no value was defined.
Unknown = iota
unknownStr = "unknown"
ssrcStr = "ssrc"
// Equal to UDP MTU
receiveMTU = 1460
// simulcastProbeCount is the amount of RTP Packets
// that handleUndeclaredSSRC will read and try to dispatch from
// mid and rid values
simulcastProbeCount = 10
// simulcastMaxProbeRoutines is how many active routines can be used to probe
// If the total amount of incoming SSRCes exceeds this new requests will be ignored
simulcastMaxProbeRoutines = 25
mediaSectionApplication = "application"
rtpOutboundMTU = 1200
)

View file

@ -0,0 +1,597 @@
// +build !js
package webrtc
import (
"errors"
"fmt"
"io"
"math"
"sync"
"sync/atomic"
"time"
"github.com/pion/datachannel"
"github.com/pion/logging"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
const dataChannelBufferSize = math.MaxUint16 // message size limit for Chromium
var errSCTPNotEstablished = errors.New("SCTP not established")
// DataChannel represents a WebRTC DataChannel
// The DataChannel interface represents a network channel
// which can be used for bidirectional peer-to-peer transfers of arbitrary data
type DataChannel struct {
mu sync.RWMutex
statsID string
label string
ordered bool
maxPacketLifeTime *uint16
maxRetransmits *uint16
protocol string
negotiated bool
id *uint16
readyState atomic.Value // DataChannelState
bufferedAmountLowThreshold uint64
detachCalled bool
// The binaryType represents attribute MUST, on getting, return the value to
// which it was last set. On setting, if the new value is either the string
// "blob" or the string "arraybuffer", then set the IDL attribute to this
// new value. Otherwise, throw a SyntaxError. When an DataChannel object
// is created, the binaryType attribute MUST be initialized to the string
// "blob". This attribute controls how binary data is exposed to scripts.
// binaryType string
onMessageHandler func(DataChannelMessage)
openHandlerOnce sync.Once
onOpenHandler func()
onCloseHandler func()
onBufferedAmountLow func()
onErrorHandler func(error)
sctpTransport *SCTPTransport
dataChannel *datachannel.DataChannel
// A reference to the associated api object used by this datachannel
api *API
log logging.LeveledLogger
}
// NewDataChannel creates a new DataChannel.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewDataChannel(transport *SCTPTransport, params *DataChannelParameters) (*DataChannel, error) {
d, err := api.newDataChannel(params, api.settingEngine.LoggerFactory.NewLogger("ortc"))
if err != nil {
return nil, err
}
err = d.open(transport)
if err != nil {
return nil, err
}
return d, nil
}
// newDataChannel is an internal constructor for the data channel used to
// create the DataChannel object before the networking is set up.
func (api *API) newDataChannel(params *DataChannelParameters, log logging.LeveledLogger) (*DataChannel, error) {
// https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #5)
if len(params.Label) > 65535 {
return nil, &rtcerr.TypeError{Err: ErrStringSizeLimit}
}
d := &DataChannel{
statsID: fmt.Sprintf("DataChannel-%d", time.Now().UnixNano()),
label: params.Label,
protocol: params.Protocol,
negotiated: params.Negotiated,
id: params.ID,
ordered: params.Ordered,
maxPacketLifeTime: params.MaxPacketLifeTime,
maxRetransmits: params.MaxRetransmits,
api: api,
log: log,
}
d.setReadyState(DataChannelStateConnecting)
return d, nil
}
// open opens the datachannel over the sctp transport
func (d *DataChannel) open(sctpTransport *SCTPTransport) error {
d.mu.Lock()
if d.sctpTransport != nil {
// already open
d.mu.Unlock()
return nil
}
d.sctpTransport = sctpTransport
if err := d.ensureSCTP(); err != nil {
d.mu.Unlock()
return err
}
var channelType datachannel.ChannelType
var reliabilityParameter uint32
switch {
case d.maxPacketLifeTime == nil && d.maxRetransmits == nil:
if d.ordered {
channelType = datachannel.ChannelTypeReliable
} else {
channelType = datachannel.ChannelTypeReliableUnordered
}
case d.maxRetransmits != nil:
reliabilityParameter = uint32(*d.maxRetransmits)
if d.ordered {
channelType = datachannel.ChannelTypePartialReliableRexmit
} else {
channelType = datachannel.ChannelTypePartialReliableRexmitUnordered
}
default:
reliabilityParameter = uint32(*d.maxPacketLifeTime)
if d.ordered {
channelType = datachannel.ChannelTypePartialReliableTimed
} else {
channelType = datachannel.ChannelTypePartialReliableTimedUnordered
}
}
cfg := &datachannel.Config{
ChannelType: channelType,
Priority: datachannel.ChannelPriorityNormal,
ReliabilityParameter: reliabilityParameter,
Label: d.label,
Protocol: d.protocol,
Negotiated: d.negotiated,
LoggerFactory: d.api.settingEngine.LoggerFactory,
}
if d.id == nil {
err := d.sctpTransport.generateAndSetDataChannelID(d.sctpTransport.dtlsTransport.role(), &d.id)
if err != nil {
return err
}
}
dc, err := datachannel.Dial(d.sctpTransport.association, *d.id, cfg)
if err != nil {
d.mu.Unlock()
return err
}
// bufferedAmountLowThreshold and onBufferedAmountLow might be set earlier
dc.SetBufferedAmountLowThreshold(d.bufferedAmountLowThreshold)
dc.OnBufferedAmountLow(d.onBufferedAmountLow)
d.mu.Unlock()
d.handleOpen(dc)
return nil
}
func (d *DataChannel) ensureSCTP() error {
if d.sctpTransport == nil {
return errSCTPNotEstablished
}
d.sctpTransport.lock.RLock()
defer d.sctpTransport.lock.RUnlock()
if d.sctpTransport.association == nil {
return errSCTPNotEstablished
}
return nil
}
// Transport returns the SCTPTransport instance the DataChannel is sending over.
func (d *DataChannel) Transport() *SCTPTransport {
d.mu.RLock()
defer d.mu.RUnlock()
return d.sctpTransport
}
// After onOpen is complete check that the user called detach
// and provide an error message if the call was missed
func (d *DataChannel) checkDetachAfterOpen() {
d.mu.RLock()
defer d.mu.RUnlock()
if d.api.settingEngine.detach.DataChannels && !d.detachCalled {
d.log.Warn("webrtc.DetachDataChannels() enabled but didn't Detach, call Detach from OnOpen")
}
}
// OnOpen sets an event handler which is invoked when
// the underlying data transport has been established (or re-established).
func (d *DataChannel) OnOpen(f func()) {
d.mu.Lock()
d.openHandlerOnce = sync.Once{}
d.onOpenHandler = f
d.mu.Unlock()
if d.ReadyState() == DataChannelStateOpen {
// If the data channel is already open, call the handler immediately.
go d.openHandlerOnce.Do(func() {
f()
d.checkDetachAfterOpen()
})
}
}
func (d *DataChannel) onOpen() {
d.mu.RLock()
handler := d.onOpenHandler
d.mu.RUnlock()
if handler != nil {
go d.openHandlerOnce.Do(func() {
handler()
d.checkDetachAfterOpen()
})
}
}
// OnClose sets an event handler which is invoked when
// the underlying data transport has been closed.
func (d *DataChannel) OnClose(f func()) {
d.mu.Lock()
defer d.mu.Unlock()
d.onCloseHandler = f
}
func (d *DataChannel) onClose() {
d.mu.RLock()
handler := d.onCloseHandler
d.mu.RUnlock()
if handler != nil {
go handler()
}
}
// OnMessage sets an event handler which is invoked on a binary
// message arrival over the sctp transport from a remote peer.
// OnMessage can currently receive messages up to 16384 bytes
// in size. Check out the detach API if you want to use larger
// message sizes. Note that browser support for larger messages
// is also limited.
func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) {
d.mu.Lock()
defer d.mu.Unlock()
d.onMessageHandler = f
}
func (d *DataChannel) onMessage(msg DataChannelMessage) {
d.mu.RLock()
handler := d.onMessageHandler
d.mu.RUnlock()
if handler == nil {
return
}
handler(msg)
}
func (d *DataChannel) handleOpen(dc *datachannel.DataChannel) {
d.mu.Lock()
d.dataChannel = dc
d.mu.Unlock()
d.setReadyState(DataChannelStateOpen)
d.onOpen()
d.mu.Lock()
defer d.mu.Unlock()
if !d.api.settingEngine.detach.DataChannels {
go d.readLoop()
}
}
// OnError sets an event handler which is invoked when
// the underlying data transport cannot be read.
func (d *DataChannel) OnError(f func(err error)) {
d.mu.Lock()
defer d.mu.Unlock()
d.onErrorHandler = f
}
func (d *DataChannel) onError(err error) {
d.mu.RLock()
handler := d.onErrorHandler
d.mu.RUnlock()
if handler != nil {
go handler(err)
}
}
// See https://github.com/pion/webrtc/issues/1516
// nolint:gochecknoglobals
var rlBufPool = sync.Pool{New: func() interface{} {
return make([]byte, dataChannelBufferSize)
}}
func (d *DataChannel) readLoop() {
for {
buffer := rlBufPool.Get().([]byte)
n, isString, err := d.dataChannel.ReadDataChannel(buffer)
if err != nil {
rlBufPool.Put(buffer) // nolint:staticcheck
d.setReadyState(DataChannelStateClosed)
if err != io.EOF {
d.onError(err)
}
d.onClose()
return
}
m := DataChannelMessage{Data: make([]byte, n), IsString: isString}
copy(m.Data, buffer[:n])
// The 'staticcheck' pragma is a false positive on the part of the CI linter.
rlBufPool.Put(buffer) // nolint:staticcheck
// NB: Why was DataChannelMessage not passed as a pointer value?
d.onMessage(m) // nolint:staticcheck
}
}
// Send sends the binary message to the DataChannel peer
func (d *DataChannel) Send(data []byte) error {
err := d.ensureOpen()
if err != nil {
return err
}
_, err = d.dataChannel.WriteDataChannel(data, false)
return err
}
// SendText sends the text message to the DataChannel peer
func (d *DataChannel) SendText(s string) error {
err := d.ensureOpen()
if err != nil {
return err
}
_, err = d.dataChannel.WriteDataChannel([]byte(s), true)
return err
}
func (d *DataChannel) ensureOpen() error {
d.mu.RLock()
defer d.mu.RUnlock()
if d.ReadyState() != DataChannelStateOpen {
return io.ErrClosedPipe
}
return nil
}
// Detach allows you to detach the underlying datachannel. This provides
// an idiomatic API to work with, however it disables the OnMessage callback.
// Before calling Detach you have to enable this behavior by calling
// webrtc.DetachDataChannels(). Combining detached and normal data channels
// is not supported.
// Please refer to the data-channels-detach example and the
// pion/datachannel documentation for the correct way to handle the
// resulting DataChannel object.
func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) {
d.mu.Lock()
defer d.mu.Unlock()
if !d.api.settingEngine.detach.DataChannels {
return nil, errDetachNotEnabled
}
if d.dataChannel == nil {
return nil, errDetachBeforeOpened
}
d.detachCalled = true
return d.dataChannel, nil
}
// Close Closes the DataChannel. It may be called regardless of whether
// the DataChannel object was created by this peer or the remote peer.
func (d *DataChannel) Close() error {
d.mu.Lock()
haveSctpTransport := d.dataChannel != nil
d.mu.Unlock()
if d.ReadyState() == DataChannelStateClosed {
return nil
}
d.setReadyState(DataChannelStateClosing)
if !haveSctpTransport {
return nil
}
return d.dataChannel.Close()
}
// Label represents a label that can be used to distinguish this
// DataChannel object from other DataChannel objects. Scripts are
// allowed to create multiple DataChannel objects with the same label.
func (d *DataChannel) Label() string {
d.mu.RLock()
defer d.mu.RUnlock()
return d.label
}
// Ordered represents if the DataChannel is ordered, and false if
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
d.mu.RLock()
defer d.mu.RUnlock()
return d.ordered
}
// MaxPacketLifeTime represents the length of the time window (msec) during
// which transmissions and retransmissions may occur in unreliable mode.
func (d *DataChannel) MaxPacketLifeTime() *uint16 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.maxPacketLifeTime
}
// MaxRetransmits represents the maximum number of retransmissions that are
// attempted in unreliable mode.
func (d *DataChannel) MaxRetransmits() *uint16 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.maxRetransmits
}
// Protocol represents the name of the sub-protocol used with this
// DataChannel.
func (d *DataChannel) Protocol() string {
d.mu.RLock()
defer d.mu.RUnlock()
return d.protocol
}
// Negotiated represents whether this DataChannel was negotiated by the
// application (true), or not (false).
func (d *DataChannel) Negotiated() bool {
d.mu.RLock()
defer d.mu.RUnlock()
return d.negotiated
}
// ID represents the ID for this DataChannel. The value is initially
// null, which is what will be returned if the ID was not provided at
// channel creation time, and the DTLS role of the SCTP transport has not
// yet been negotiated. Otherwise, it will return the ID that was either
// selected by the script or generated. After the ID is set to a non-null
// value, it will not change.
func (d *DataChannel) ID() *uint16 {
d.mu.RLock()
defer d.mu.RUnlock()
return d.id
}
// ReadyState represents the state of the DataChannel object.
func (d *DataChannel) ReadyState() DataChannelState {
if v := d.readyState.Load(); v != nil {
return v.(DataChannelState)
}
return DataChannelState(0)
}
// BufferedAmount represents the number of bytes of application data
// (UTF-8 text and binary data) that have been queued using send(). Even
// though the data transmission can occur in parallel, the returned value
// MUST NOT be decreased before the current task yielded back to the event
// loop to prevent race conditions. The value does not include framing
// overhead incurred by the protocol, or buffering done by the operating
// system or network hardware. The value of BufferedAmount slot will only
// increase with each call to the send() method as long as the ReadyState is
// open; however, BufferedAmount does not reset to zero once the channel
// closes.
func (d *DataChannel) BufferedAmount() uint64 {
d.mu.RLock()
defer d.mu.RUnlock()
if d.dataChannel == nil {
return 0
}
return d.dataChannel.BufferedAmount()
}
// BufferedAmountLowThreshold represents the threshold at which the
// bufferedAmount is considered to be low. When the bufferedAmount decreases
// from above this threshold to equal or below it, the bufferedamountlow
// event fires. BufferedAmountLowThreshold is initially zero on each new
// DataChannel, but the application may change its value at any time.
// The threshold is set to 0 by default.
func (d *DataChannel) BufferedAmountLowThreshold() uint64 {
d.mu.RLock()
defer d.mu.RUnlock()
if d.dataChannel == nil {
return d.bufferedAmountLowThreshold
}
return d.dataChannel.BufferedAmountLowThreshold()
}
// SetBufferedAmountLowThreshold is used to update the threshold.
// See BufferedAmountLowThreshold().
func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) {
d.mu.Lock()
defer d.mu.Unlock()
d.bufferedAmountLowThreshold = th
if d.dataChannel != nil {
d.dataChannel.SetBufferedAmountLowThreshold(th)
}
}
// OnBufferedAmountLow sets an event handler which is invoked when
// the number of bytes of outgoing data becomes lower than the
// BufferedAmountLowThreshold.
func (d *DataChannel) OnBufferedAmountLow(f func()) {
d.mu.Lock()
defer d.mu.Unlock()
d.onBufferedAmountLow = f
if d.dataChannel != nil {
d.dataChannel.OnBufferedAmountLow(f)
}
}
func (d *DataChannel) getStatsID() string {
d.mu.Lock()
defer d.mu.Unlock()
return d.statsID
}
func (d *DataChannel) collectStats(collector *statsReportCollector) {
collector.Collecting()
d.mu.Lock()
defer d.mu.Unlock()
stats := DataChannelStats{
Timestamp: statsTimestampNow(),
Type: StatsTypeDataChannel,
ID: d.statsID,
Label: d.label,
Protocol: d.protocol,
// TransportID string `json:"transportId"`
State: d.ReadyState(),
}
if d.id != nil {
stats.DataChannelIdentifier = int32(*d.id)
}
if d.dataChannel != nil {
stats.MessagesSent = d.dataChannel.MessagesSent()
stats.BytesSent = d.dataChannel.BytesSent()
stats.MessagesReceived = d.dataChannel.MessagesReceived()
stats.BytesReceived = d.dataChannel.BytesReceived()
}
collector.Collect(stats.ID, stats)
}
func (d *DataChannel) setReadyState(r DataChannelState) {
d.readyState.Store(r)
}

View file

@ -0,0 +1,319 @@
// +build js,wasm
package webrtc
import (
"fmt"
"syscall/js"
"github.com/pion/datachannel"
)
const dataChannelBufferSize = 16384 // Lowest common denominator among browsers
// DataChannel represents a WebRTC DataChannel
// The DataChannel interface represents a network channel
// which can be used for bidirectional peer-to-peer transfers of arbitrary data
type DataChannel struct {
// Pointer to the underlying JavaScript RTCPeerConnection object.
underlying js.Value
// Keep track of handlers/callbacks so we can call Release as required by the
// syscall/js API. Initially nil.
onOpenHandler *js.Func
onCloseHandler *js.Func
onMessageHandler *js.Func
onBufferedAmountLow *js.Func
// A reference to the associated api object used by this datachannel
api *API
}
// OnOpen sets an event handler which is invoked when
// the underlying data transport has been established (or re-established).
func (d *DataChannel) OnOpen(f func()) {
if d.onOpenHandler != nil {
oldHandler := d.onOpenHandler
defer oldHandler.Release()
}
onOpenHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
d.onOpenHandler = &onOpenHandler
d.underlying.Set("onopen", onOpenHandler)
}
// OnClose sets an event handler which is invoked when
// the underlying data transport has been closed.
func (d *DataChannel) OnClose(f func()) {
if d.onCloseHandler != nil {
oldHandler := d.onCloseHandler
defer oldHandler.Release()
}
onCloseHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
d.onCloseHandler = &onCloseHandler
d.underlying.Set("onclose", onCloseHandler)
}
// OnMessage sets an event handler which is invoked on a binary message arrival
// from a remote peer. Note that browsers may place limitations on message size.
func (d *DataChannel) OnMessage(f func(msg DataChannelMessage)) {
if d.onMessageHandler != nil {
oldHandler := d.onMessageHandler
defer oldHandler.Release()
}
onMessageHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// pion/webrtc/projects/15
data := args[0].Get("data")
go func() {
// valueToDataChannelMessage may block when handling 'Blob' data
// so we need to call it from a new routine. See:
// https://pkg.go.dev/syscall/js#FuncOf
msg := valueToDataChannelMessage(data)
f(msg)
}()
return js.Undefined()
})
d.onMessageHandler = &onMessageHandler
d.underlying.Set("onmessage", onMessageHandler)
}
// Send sends the binary message to the DataChannel peer
func (d *DataChannel) Send(data []byte) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
array := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(array, data)
d.underlying.Call("send", array)
return nil
}
// SendText sends the text message to the DataChannel peer
func (d *DataChannel) SendText(s string) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
d.underlying.Call("send", s)
return nil
}
// Detach allows you to detach the underlying datachannel. This provides
// an idiomatic API to work with, however it disables the OnMessage callback.
// Before calling Detach you have to enable this behavior by calling
// webrtc.DetachDataChannels(). Combining detached and normal data channels
// is not supported.
// Please reffer to the data-channels-detach example and the
// pion/datachannel documentation for the correct way to handle the
// resulting DataChannel object.
func (d *DataChannel) Detach() (datachannel.ReadWriteCloser, error) {
if !d.api.settingEngine.detach.DataChannels {
return nil, fmt.Errorf("enable detaching by calling webrtc.DetachDataChannels()")
}
detached := newDetachedDataChannel(d)
return detached, nil
}
// Close Closes the DataChannel. It may be called regardless of whether
// the DataChannel object was created by this peer or the remote peer.
func (d *DataChannel) Close() (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
d.underlying.Call("close")
// Release any handlers as required by the syscall/js API.
if d.onOpenHandler != nil {
d.onOpenHandler.Release()
}
if d.onCloseHandler != nil {
d.onCloseHandler.Release()
}
if d.onMessageHandler != nil {
d.onMessageHandler.Release()
}
if d.onBufferedAmountLow != nil {
d.onBufferedAmountLow.Release()
}
return nil
}
// Label represents a label that can be used to distinguish this
// DataChannel object from other DataChannel objects. Scripts are
// allowed to create multiple DataChannel objects with the same label.
func (d *DataChannel) Label() string {
return d.underlying.Get("label").String()
}
// Ordered represents if the DataChannel is ordered, and false if
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
ordered := d.underlying.Get("ordered")
if jsValueIsUndefined(ordered) {
return true // default is true
}
return ordered.Bool()
}
// MaxPacketLifeTime represents the length of the time window (msec) during
// which transmissions and retransmissions may occur in unreliable mode.
func (d *DataChannel) MaxPacketLifeTime() *uint16 {
if !jsValueIsUndefined(d.underlying.Get("maxPacketLifeTime")) {
return valueToUint16Pointer(d.underlying.Get("maxPacketLifeTime"))
} else {
// See https://bugs.chromium.org/p/chromium/issues/detail?id=696681
// Chrome calls this "maxRetransmitTime"
return valueToUint16Pointer(d.underlying.Get("maxRetransmitTime"))
}
}
// MaxRetransmits represents the maximum number of retransmissions that are
// attempted in unreliable mode.
func (d *DataChannel) MaxRetransmits() *uint16 {
return valueToUint16Pointer(d.underlying.Get("maxRetransmits"))
}
// Protocol represents the name of the sub-protocol used with this
// DataChannel.
func (d *DataChannel) Protocol() string {
return d.underlying.Get("protocol").String()
}
// Negotiated represents whether this DataChannel was negotiated by the
// application (true), or not (false).
func (d *DataChannel) Negotiated() bool {
return d.underlying.Get("negotiated").Bool()
}
// ID represents the ID for this DataChannel. The value is initially
// null, which is what will be returned if the ID was not provided at
// channel creation time. Otherwise, it will return the ID that was either
// selected by the script or generated. After the ID is set to a non-null
// value, it will not change.
func (d *DataChannel) ID() *uint16 {
return valueToUint16Pointer(d.underlying.Get("id"))
}
// ReadyState represents the state of the DataChannel object.
func (d *DataChannel) ReadyState() DataChannelState {
return newDataChannelState(d.underlying.Get("readyState").String())
}
// BufferedAmount represents the number of bytes of application data
// (UTF-8 text and binary data) that have been queued using send(). Even
// though the data transmission can occur in parallel, the returned value
// MUST NOT be decreased before the current task yielded back to the event
// loop to prevent race conditions. The value does not include framing
// overhead incurred by the protocol, or buffering done by the operating
// system or network hardware. The value of BufferedAmount slot will only
// increase with each call to the send() method as long as the ReadyState is
// open; however, BufferedAmount does not reset to zero once the channel
// closes.
func (d *DataChannel) BufferedAmount() uint64 {
return uint64(d.underlying.Get("bufferedAmount").Int())
}
// BufferedAmountLowThreshold represents the threshold at which the
// bufferedAmount is considered to be low. When the bufferedAmount decreases
// from above this threshold to equal or below it, the bufferedamountlow
// event fires. BufferedAmountLowThreshold is initially zero on each new
// DataChannel, but the application may change its value at any time.
func (d *DataChannel) BufferedAmountLowThreshold() uint64 {
return uint64(d.underlying.Get("bufferedAmountLowThreshold").Int())
}
// SetBufferedAmountLowThreshold is used to update the threshold.
// See BufferedAmountLowThreshold().
func (d *DataChannel) SetBufferedAmountLowThreshold(th uint64) {
d.underlying.Set("bufferedAmountLowThreshold", th)
}
// OnBufferedAmountLow sets an event handler which is invoked when
// the number of bytes of outgoing data becomes lower than the
// BufferedAmountLowThreshold.
func (d *DataChannel) OnBufferedAmountLow(f func()) {
if d.onBufferedAmountLow != nil {
oldHandler := d.onBufferedAmountLow
defer oldHandler.Release()
}
onBufferedAmountLow := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
d.onBufferedAmountLow = &onBufferedAmountLow
d.underlying.Set("onbufferedamountlow", onBufferedAmountLow)
}
// valueToDataChannelMessage converts the given value to a DataChannelMessage.
// val should be obtained from MessageEvent.data where MessageEvent is received
// via the RTCDataChannel.onmessage callback.
func valueToDataChannelMessage(val js.Value) DataChannelMessage {
// If val is of type string, the conversion is straightforward.
if val.Type() == js.TypeString {
return DataChannelMessage{
IsString: true,
Data: []byte(val.String()),
}
}
// For other types, we need to first determine val.constructor.name.
constructorName := val.Get("constructor").Get("name").String()
var data []byte
switch constructorName {
case "Uint8Array":
// We can easily convert Uint8Array to []byte
data = uint8ArrayValueToBytes(val)
case "Blob":
// Convert the Blob to an ArrayBuffer and then convert the ArrayBuffer
// to a Uint8Array.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Blob
// The JavaScript API for reading from the Blob is asynchronous. We use a
// channel to signal when reading is done.
reader := js.Global().Get("FileReader").New()
doneChan := make(chan struct{})
reader.Call("addEventListener", "loadend", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
// Signal that the FileReader is done reading/loading by sending through
// the doneChan.
doneChan <- struct{}{}
}()
return js.Undefined()
}))
reader.Call("readAsArrayBuffer", val)
// Wait for the FileReader to finish reading/loading.
<-doneChan
// At this point buffer.result is a typed array, which we know how to
// handle.
buffer := reader.Get("result")
uint8Array := js.Global().Get("Uint8Array").New(buffer)
data = uint8ArrayValueToBytes(uint8Array)
default:
// Assume we have an ArrayBufferView type which we can convert to a
// Uint8Array in JavaScript.
// See: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView
uint8Array := js.Global().Get("Uint8Array").New(val)
data = uint8ArrayValueToBytes(uint8Array)
}
return DataChannelMessage{
IsString: false,
Data: data,
}
}

View file

@ -0,0 +1,71 @@
// +build js,wasm
package webrtc
import (
"errors"
)
type detachedDataChannel struct {
dc *DataChannel
read chan DataChannelMessage
done chan struct{}
}
func newDetachedDataChannel(dc *DataChannel) *detachedDataChannel {
read := make(chan DataChannelMessage)
done := make(chan struct{})
// Wire up callbacks
dc.OnMessage(func(msg DataChannelMessage) {
read <- msg // pion/webrtc/projects/15
})
// pion/webrtc/projects/15
return &detachedDataChannel{
dc: dc,
read: read,
done: done,
}
}
func (c *detachedDataChannel) Read(p []byte) (int, error) {
n, _, err := c.ReadDataChannel(p)
return n, err
}
func (c *detachedDataChannel) ReadDataChannel(p []byte) (int, bool, error) {
select {
case <-c.done:
return 0, false, errors.New("Reader closed")
case msg := <-c.read:
n := copy(p, msg.Data)
if n < len(msg.Data) {
return n, msg.IsString, errors.New("Read buffer to small")
}
return n, msg.IsString, nil
}
}
func (c *detachedDataChannel) Write(p []byte) (n int, err error) {
return c.WriteDataChannel(p, false)
}
func (c *detachedDataChannel) WriteDataChannel(p []byte, isString bool) (n int, err error) {
if isString {
err = c.dc.SendText(string(p))
return len(p), err
}
err = c.dc.Send(p)
return len(p), err
}
func (c *detachedDataChannel) Close() error {
close(c.done)
return c.dc.Close()
}

View file

@ -0,0 +1,33 @@
package webrtc
// DataChannelInit can be used to configure properties of the underlying
// channel such as data reliability.
type DataChannelInit struct {
// Ordered indicates if data is allowed to be delivered out of order. The
// default value of true, guarantees that data will be delivered in order.
Ordered *bool
// MaxPacketLifeTime limits the time (in milliseconds) during which the
// channel will transmit or retransmit data if not acknowledged. This value
// may be clamped if it exceeds the maximum value supported.
MaxPacketLifeTime *uint16
// MaxRetransmits limits the number of times a channel will retransmit data
// if not successfully delivered. This value may be clamped if it exceeds
// the maximum value supported.
MaxRetransmits *uint16
// Protocol describes the subprotocol name used for this channel.
Protocol *string
// Negotiated describes if the data channel is created by the local peer or
// the remote peer. The default value of false tells the user agent to
// announce the channel in-band and instruct the other peer to dispatch a
// corresponding DataChannel. If set to true, it is up to the application
// to negotiate the channel and create an DataChannel with the same id
// at the other peer.
Negotiated *bool
// ID overrides the default selection of ID for this channel.
ID *uint16
}

View file

@ -0,0 +1,10 @@
package webrtc
// DataChannelMessage represents a message received from the
// data channel. IsString will be set to true if the incoming
// message is of the string type. Otherwise the message is of
// a binary type.
type DataChannelMessage struct {
IsString bool
Data []byte
}

View file

@ -0,0 +1,12 @@
package webrtc
// DataChannelParameters describes the configuration of the DataChannel.
type DataChannelParameters struct {
Label string `json:"label"`
Protocol string `json:"protocol"`
ID *uint16 `json:"id"`
Ordered bool `json:"ordered"`
MaxPacketLifeTime *uint16 `json:"maxPacketLifeTime"`
MaxRetransmits *uint16 `json:"maxRetransmits"`
Negotiated bool `json:"negotiated"`
}

View file

@ -0,0 +1,61 @@
package webrtc
// DataChannelState indicates the state of a data channel.
type DataChannelState int
const (
// DataChannelStateConnecting indicates that the data channel is being
// established. This is the initial state of DataChannel, whether created
// with CreateDataChannel, or dispatched as a part of an DataChannelEvent.
DataChannelStateConnecting DataChannelState = iota + 1
// DataChannelStateOpen indicates that the underlying data transport is
// established and communication is possible.
DataChannelStateOpen
// DataChannelStateClosing indicates that the procedure to close down the
// underlying data transport has started.
DataChannelStateClosing
// DataChannelStateClosed indicates that the underlying data transport
// has been closed or could not be established.
DataChannelStateClosed
)
// This is done this way because of a linter.
const (
dataChannelStateConnectingStr = "connecting"
dataChannelStateOpenStr = "open"
dataChannelStateClosingStr = "closing"
dataChannelStateClosedStr = "closed"
)
func newDataChannelState(raw string) DataChannelState {
switch raw {
case dataChannelStateConnectingStr:
return DataChannelStateConnecting
case dataChannelStateOpenStr:
return DataChannelStateOpen
case dataChannelStateClosingStr:
return DataChannelStateClosing
case dataChannelStateClosedStr:
return DataChannelStateClosed
default:
return DataChannelState(Unknown)
}
}
func (t DataChannelState) String() string {
switch t {
case DataChannelStateConnecting:
return dataChannelStateConnectingStr
case DataChannelStateOpen:
return dataChannelStateOpenStr
case DataChannelStateClosing:
return dataChannelStateClosingStr
case DataChannelStateClosed:
return dataChannelStateClosedStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,14 @@
package webrtc
// DTLSFingerprint specifies the hash function algorithm and certificate
// fingerprint as described in https://tools.ietf.org/html/rfc4572.
type DTLSFingerprint struct {
// Algorithm specifies one of the the hash function algorithms defined in
// the 'Hash function Textual Names' registry.
Algorithm string `json:"algorithm"`
// Value specifies the value of the certificate fingerprint in lowercase
// hex string as expressed utilizing the syntax of 'fingerprint' in
// https://tools.ietf.org/html/rfc4572#section-5.
Value string `json:"value"`
}

View file

@ -0,0 +1,7 @@
package webrtc
// DTLSParameters holds information relating to DTLS configuration.
type DTLSParameters struct {
Role DTLSRole `json:"role"`
Fingerprints []DTLSFingerprint `json:"fingerprints"`
}

View file

@ -0,0 +1,92 @@
package webrtc
import (
"github.com/pion/sdp/v3"
)
// DTLSRole indicates the role of the DTLS transport.
type DTLSRole byte
const (
// DTLSRoleAuto defines the DTLS role is determined based on
// the resolved ICE role: the ICE controlled role acts as the DTLS
// client and the ICE controlling role acts as the DTLS server.
DTLSRoleAuto DTLSRole = iota + 1
// DTLSRoleClient defines the DTLS client role.
DTLSRoleClient
// DTLSRoleServer defines the DTLS server role.
DTLSRoleServer
)
const (
// https://tools.ietf.org/html/rfc5763
/*
The answerer MUST use either a
setup attribute value of setup:active or setup:passive. Note that
if the answerer uses setup:passive, then the DTLS handshake will
not begin until the answerer is received, which adds additional
latency. setup:active allows the answer and the DTLS handshake to
occur in parallel. Thus, setup:active is RECOMMENDED.
*/
defaultDtlsRoleAnswer = DTLSRoleClient
/*
The endpoint that is the offerer MUST use the setup attribute
value of setup:actpass and be prepared to receive a client_hello
before it receives the answer.
*/
defaultDtlsRoleOffer = DTLSRoleAuto
)
func (r DTLSRole) String() string {
switch r {
case DTLSRoleAuto:
return "auto"
case DTLSRoleClient:
return "client"
case DTLSRoleServer:
return "server"
default:
return unknownStr
}
}
// Iterate a SessionDescription from a remote to determine if an explicit
// role can been determined from it. The decision is made from the first role we we parse.
// If no role can be found we return DTLSRoleAuto
func dtlsRoleFromRemoteSDP(sessionDescription *sdp.SessionDescription) DTLSRole {
if sessionDescription == nil {
return DTLSRoleAuto
}
for _, mediaSection := range sessionDescription.MediaDescriptions {
for _, attribute := range mediaSection.Attributes {
if attribute.Key == "setup" {
switch attribute.Value {
case sdp.ConnectionRoleActive.String():
return DTLSRoleClient
case sdp.ConnectionRolePassive.String():
return DTLSRoleServer
default:
return DTLSRoleAuto
}
}
}
}
return DTLSRoleAuto
}
func connectionRoleFromDtlsRole(d DTLSRole) sdp.ConnectionRole {
switch d {
case DTLSRoleClient:
return sdp.ConnectionRoleActive
case DTLSRoleServer:
return sdp.ConnectionRolePassive
case DTLSRoleAuto:
return sdp.ConnectionRoleActpass
default:
return sdp.ConnectionRole(0)
}
}

View file

@ -0,0 +1,419 @@
// +build !js
package webrtc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/pion/dtls/v2"
"github.com/pion/dtls/v2/pkg/crypto/fingerprint"
"github.com/pion/srtp/v2"
"github.com/pion/webrtc/v3/internal/mux"
"github.com/pion/webrtc/v3/internal/util"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
// DTLSTransport allows an application access to information about the DTLS
// transport over which RTP and RTCP packets are sent and received by
// RTPSender and RTPReceiver, as well other data such as SCTP packets sent
// and received by data channels.
type DTLSTransport struct {
lock sync.RWMutex
iceTransport *ICETransport
certificates []Certificate
remoteParameters DTLSParameters
remoteCertificate []byte
state DTLSTransportState
srtpProtectionProfile srtp.ProtectionProfile
onStateChangeHandler func(DTLSTransportState)
conn *dtls.Conn
srtpSession, srtcpSession atomic.Value
srtpEndpoint, srtcpEndpoint *mux.Endpoint
simulcastStreams []*srtp.ReadStreamSRTP
srtpReady chan struct{}
dtlsMatcher mux.MatchFunc
api *API
}
// NewDTLSTransport creates a new DTLSTransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewDTLSTransport(transport *ICETransport, certificates []Certificate) (*DTLSTransport, error) {
t := &DTLSTransport{
iceTransport: transport,
api: api,
state: DTLSTransportStateNew,
dtlsMatcher: mux.MatchDTLS,
srtpReady: make(chan struct{}),
}
if len(certificates) > 0 {
now := time.Now()
for _, x509Cert := range certificates {
if !x509Cert.Expires().IsZero() && now.After(x509Cert.Expires()) {
return nil, &rtcerr.InvalidAccessError{Err: ErrCertificateExpired}
}
t.certificates = append(t.certificates, x509Cert)
}
} else {
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, &rtcerr.UnknownError{Err: err}
}
certificate, err := GenerateCertificate(sk)
if err != nil {
return nil, err
}
t.certificates = []Certificate{*certificate}
}
return t, nil
}
// ICETransport returns the currently-configured *ICETransport or nil
// if one has not been configured
func (t *DTLSTransport) ICETransport() *ICETransport {
t.lock.RLock()
defer t.lock.RUnlock()
return t.iceTransport
}
// onStateChange requires the caller holds the lock
func (t *DTLSTransport) onStateChange(state DTLSTransportState) {
t.state = state
handler := t.onStateChangeHandler
if handler != nil {
handler(state)
}
}
// OnStateChange sets a handler that is fired when the DTLS
// connection state changes.
func (t *DTLSTransport) OnStateChange(f func(DTLSTransportState)) {
t.lock.Lock()
defer t.lock.Unlock()
t.onStateChangeHandler = f
}
// State returns the current dtls transport state.
func (t *DTLSTransport) State() DTLSTransportState {
t.lock.RLock()
defer t.lock.RUnlock()
return t.state
}
// GetLocalParameters returns the DTLS parameters of the local DTLSTransport upon construction.
func (t *DTLSTransport) GetLocalParameters() (DTLSParameters, error) {
fingerprints := []DTLSFingerprint{}
for _, c := range t.certificates {
prints, err := c.GetFingerprints()
if err != nil {
return DTLSParameters{}, err
}
fingerprints = append(fingerprints, prints...)
}
return DTLSParameters{
Role: DTLSRoleAuto, // always returns the default role
Fingerprints: fingerprints,
}, nil
}
// GetRemoteCertificate returns the certificate chain in use by the remote side
// returns an empty list prior to selection of the remote certificate
func (t *DTLSTransport) GetRemoteCertificate() []byte {
t.lock.RLock()
defer t.lock.RUnlock()
return t.remoteCertificate
}
func (t *DTLSTransport) startSRTP() error {
srtpConfig := &srtp.Config{
Profile: t.srtpProtectionProfile,
BufferFactory: t.api.settingEngine.BufferFactory,
LoggerFactory: t.api.settingEngine.LoggerFactory,
}
if t.api.settingEngine.replayProtection.SRTP != nil {
srtpConfig.RemoteOptions = append(
srtpConfig.RemoteOptions,
srtp.SRTPReplayProtection(*t.api.settingEngine.replayProtection.SRTP),
)
}
if t.api.settingEngine.disableSRTPReplayProtection {
srtpConfig.RemoteOptions = append(
srtpConfig.RemoteOptions,
srtp.SRTPNoReplayProtection(),
)
}
if t.api.settingEngine.replayProtection.SRTCP != nil {
srtpConfig.RemoteOptions = append(
srtpConfig.RemoteOptions,
srtp.SRTCPReplayProtection(*t.api.settingEngine.replayProtection.SRTCP),
)
}
if t.api.settingEngine.disableSRTCPReplayProtection {
srtpConfig.RemoteOptions = append(
srtpConfig.RemoteOptions,
srtp.SRTCPNoReplayProtection(),
)
}
connState := t.conn.ConnectionState()
err := srtpConfig.ExtractSessionKeysFromDTLS(&connState, t.role() == DTLSRoleClient)
if err != nil {
return fmt.Errorf("%w: %v", errDtlsKeyExtractionFailed, err)
}
srtpSession, err := srtp.NewSessionSRTP(t.srtpEndpoint, srtpConfig)
if err != nil {
return fmt.Errorf("%w: %v", errFailedToStartSRTP, err)
}
srtcpSession, err := srtp.NewSessionSRTCP(t.srtcpEndpoint, srtpConfig)
if err != nil {
return fmt.Errorf("%w: %v", errFailedToStartSRTCP, err)
}
t.srtpSession.Store(srtpSession)
t.srtcpSession.Store(srtcpSession)
close(t.srtpReady)
return nil
}
func (t *DTLSTransport) getSRTPSession() (*srtp.SessionSRTP, error) {
if value := t.srtpSession.Load(); value != nil {
return value.(*srtp.SessionSRTP), nil
}
return nil, errDtlsTransportNotStarted
}
func (t *DTLSTransport) getSRTCPSession() (*srtp.SessionSRTCP, error) {
if value := t.srtcpSession.Load(); value != nil {
return value.(*srtp.SessionSRTCP), nil
}
return nil, errDtlsTransportNotStarted
}
func (t *DTLSTransport) role() DTLSRole {
// If remote has an explicit role use the inverse
switch t.remoteParameters.Role {
case DTLSRoleClient:
return DTLSRoleServer
case DTLSRoleServer:
return DTLSRoleClient
default:
}
// If SettingEngine has an explicit role
switch t.api.settingEngine.answeringDTLSRole {
case DTLSRoleServer:
return DTLSRoleServer
case DTLSRoleClient:
return DTLSRoleClient
default:
}
// Remote was auto and no explicit role was configured via SettingEngine
if t.iceTransport.Role() == ICERoleControlling {
return DTLSRoleServer
}
return defaultDtlsRoleAnswer
}
// Start DTLS transport negotiation with the parameters of the remote DTLS transport
func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error {
// Take lock and prepare connection, we must not hold the lock
// when connecting
prepareTransport := func() (DTLSRole, *dtls.Config, error) {
t.lock.Lock()
defer t.lock.Unlock()
if err := t.ensureICEConn(); err != nil {
return DTLSRole(0), nil, err
}
if t.state != DTLSTransportStateNew {
return DTLSRole(0), nil, &rtcerr.InvalidStateError{Err: fmt.Errorf("%w: %s", errInvalidDTLSStart, t.state)}
}
t.srtpEndpoint = t.iceTransport.NewEndpoint(mux.MatchSRTP)
t.srtcpEndpoint = t.iceTransport.NewEndpoint(mux.MatchSRTCP)
t.remoteParameters = remoteParameters
cert := t.certificates[0]
t.onStateChange(DTLSTransportStateConnecting)
return t.role(), &dtls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{cert.x509Cert.Raw},
PrivateKey: cert.privateKey,
},
},
SRTPProtectionProfiles: []dtls.SRTPProtectionProfile{dtls.SRTP_AEAD_AES_128_GCM, dtls.SRTP_AES128_CM_HMAC_SHA1_80},
ClientAuth: dtls.RequireAnyClientCert,
LoggerFactory: t.api.settingEngine.LoggerFactory,
InsecureSkipVerify: true,
}, nil
}
var dtlsConn *dtls.Conn
dtlsEndpoint := t.iceTransport.NewEndpoint(mux.MatchDTLS)
role, dtlsConfig, err := prepareTransport()
if err != nil {
return err
}
if t.api.settingEngine.replayProtection.DTLS != nil {
dtlsConfig.ReplayProtectionWindow = int(*t.api.settingEngine.replayProtection.DTLS)
}
// Connect as DTLS Client/Server, function is blocking and we
// must not hold the DTLSTransport lock
if role == DTLSRoleClient {
dtlsConn, err = dtls.Client(dtlsEndpoint, dtlsConfig)
} else {
dtlsConn, err = dtls.Server(dtlsEndpoint, dtlsConfig)
}
// Re-take the lock, nothing beyond here is blocking
t.lock.Lock()
defer t.lock.Unlock()
if err != nil {
t.onStateChange(DTLSTransportStateFailed)
return err
}
srtpProfile, ok := dtlsConn.SelectedSRTPProtectionProfile()
if !ok {
t.onStateChange(DTLSTransportStateFailed)
return ErrNoSRTPProtectionProfile
}
switch srtpProfile {
case dtls.SRTP_AEAD_AES_128_GCM:
t.srtpProtectionProfile = srtp.ProtectionProfileAeadAes128Gcm
case dtls.SRTP_AES128_CM_HMAC_SHA1_80:
t.srtpProtectionProfile = srtp.ProtectionProfileAes128CmHmacSha1_80
default:
t.onStateChange(DTLSTransportStateFailed)
return ErrNoSRTPProtectionProfile
}
t.conn = dtlsConn
t.onStateChange(DTLSTransportStateConnected)
if t.api.settingEngine.disableCertificateFingerprintVerification {
return nil
}
// Check the fingerprint if a certificate was exchanged
remoteCerts := t.conn.ConnectionState().PeerCertificates
if len(remoteCerts) == 0 {
t.onStateChange(DTLSTransportStateFailed)
return errNoRemoteCertificate
}
t.remoteCertificate = remoteCerts[0]
parsedRemoteCert, err := x509.ParseCertificate(t.remoteCertificate)
if err != nil {
t.onStateChange(DTLSTransportStateFailed)
return err
}
if err = t.validateFingerPrint(parsedRemoteCert); err != nil {
t.onStateChange(DTLSTransportStateFailed)
return err
}
return t.startSRTP()
}
// Stop stops and closes the DTLSTransport object.
func (t *DTLSTransport) Stop() error {
t.lock.Lock()
defer t.lock.Unlock()
// Try closing everything and collect the errors
var closeErrs []error
if srtpSessionValue := t.srtpSession.Load(); srtpSessionValue != nil {
closeErrs = append(closeErrs, srtpSessionValue.(*srtp.SessionSRTP).Close())
}
if srtcpSessionValue := t.srtcpSession.Load(); srtcpSessionValue != nil {
closeErrs = append(closeErrs, srtcpSessionValue.(*srtp.SessionSRTCP).Close())
}
for i := range t.simulcastStreams {
closeErrs = append(closeErrs, t.simulcastStreams[i].Close())
}
if t.conn != nil {
// dtls connection may be closed on sctp close.
if err := t.conn.Close(); err != nil && !errors.Is(err, dtls.ErrConnClosed) {
closeErrs = append(closeErrs, err)
}
}
t.onStateChange(DTLSTransportStateClosed)
return util.FlattenErrs(closeErrs)
}
func (t *DTLSTransport) validateFingerPrint(remoteCert *x509.Certificate) error {
for _, fp := range t.remoteParameters.Fingerprints {
hashAlgo, err := fingerprint.HashFromString(fp.Algorithm)
if err != nil {
return err
}
remoteValue, err := fingerprint.Fingerprint(remoteCert, hashAlgo)
if err != nil {
return err
}
if strings.EqualFold(remoteValue, fp.Value) {
return nil
}
}
return errNoMatchingCertificateFingerprint
}
func (t *DTLSTransport) ensureICEConn() error {
if t.iceTransport == nil || t.iceTransport.State() == ICETransportStateNew {
return errICEConnectionNotStarted
}
return nil
}
func (t *DTLSTransport) storeSimulcastStream(s *srtp.ReadStreamSRTP) {
t.lock.Lock()
defer t.lock.Unlock()
t.simulcastStreams = append(t.simulcastStreams, s)
}

View file

@ -0,0 +1,71 @@
package webrtc
// DTLSTransportState indicates the DTLS transport establishment state.
type DTLSTransportState int
const (
// DTLSTransportStateNew indicates that DTLS has not started negotiating
// yet.
DTLSTransportStateNew DTLSTransportState = iota + 1
// DTLSTransportStateConnecting indicates that DTLS is in the process of
// negotiating a secure connection and verifying the remote fingerprint.
DTLSTransportStateConnecting
// DTLSTransportStateConnected indicates that DTLS has completed
// negotiation of a secure connection and verified the remote fingerprint.
DTLSTransportStateConnected
// DTLSTransportStateClosed indicates that the transport has been closed
// intentionally as the result of receipt of a close_notify alert, or
// calling close().
DTLSTransportStateClosed
// DTLSTransportStateFailed indicates that the transport has failed as
// the result of an error (such as receipt of an error alert or failure to
// validate the remote fingerprint).
DTLSTransportStateFailed
)
// This is done this way because of a linter.
const (
dtlsTransportStateNewStr = "new"
dtlsTransportStateConnectingStr = "connecting"
dtlsTransportStateConnectedStr = "connected"
dtlsTransportStateClosedStr = "closed"
dtlsTransportStateFailedStr = "failed"
)
func newDTLSTransportState(raw string) DTLSTransportState {
switch raw {
case dtlsTransportStateNewStr:
return DTLSTransportStateNew
case dtlsTransportStateConnectingStr:
return DTLSTransportStateConnecting
case dtlsTransportStateConnectedStr:
return DTLSTransportStateConnected
case dtlsTransportStateClosedStr:
return DTLSTransportStateClosed
case dtlsTransportStateFailedStr:
return DTLSTransportStateFailed
default:
return DTLSTransportState(Unknown)
}
}
func (t DTLSTransportState) String() string {
switch t {
case DTLSTransportStateNew:
return dtlsTransportStateNewStr
case DTLSTransportStateConnecting:
return dtlsTransportStateConnectingStr
case DTLSTransportStateConnected:
return dtlsTransportStateConnectedStr
case DTLSTransportStateClosed:
return dtlsTransportStateClosedStr
case DTLSTransportStateFailed:
return dtlsTransportStateFailedStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,220 @@
package webrtc
import (
"errors"
)
var (
// ErrUnknownType indicates an error with Unknown info.
ErrUnknownType = errors.New("unknown")
// ErrConnectionClosed indicates an operation executed after connection
// has already been closed.
ErrConnectionClosed = errors.New("connection closed")
// ErrDataChannelNotOpen indicates an operation executed when the data
// channel is not (yet) open.
ErrDataChannelNotOpen = errors.New("data channel not open")
// ErrCertificateExpired indicates that an x509 certificate has expired.
ErrCertificateExpired = errors.New("x509Cert expired")
// ErrNoTurnCredentials indicates that a TURN server URL was provided
// without required credentials.
ErrNoTurnCredentials = errors.New("turn server credentials required")
// ErrTurnCredentials indicates that provided TURN credentials are partial
// or malformed.
ErrTurnCredentials = errors.New("invalid turn server credentials")
// ErrExistingTrack indicates that a track already exists.
ErrExistingTrack = errors.New("track already exists")
// ErrPrivateKeyType indicates that a particular private key encryption
// chosen to generate a certificate is not supported.
ErrPrivateKeyType = errors.New("private key type not supported")
// ErrModifyingPeerIdentity indicates that an attempt to modify
// PeerIdentity was made after PeerConnection has been initialized.
ErrModifyingPeerIdentity = errors.New("peerIdentity cannot be modified")
// ErrModifyingCertificates indicates that an attempt to modify
// Certificates was made after PeerConnection has been initialized.
ErrModifyingCertificates = errors.New("certificates cannot be modified")
// ErrModifyingBundlePolicy indicates that an attempt to modify
// BundlePolicy was made after PeerConnection has been initialized.
ErrModifyingBundlePolicy = errors.New("bundle policy cannot be modified")
// ErrModifyingRTCPMuxPolicy indicates that an attempt to modify
// RTCPMuxPolicy was made after PeerConnection has been initialized.
ErrModifyingRTCPMuxPolicy = errors.New("rtcp mux policy cannot be modified")
// ErrModifyingICECandidatePoolSize indicates that an attempt to modify
// ICECandidatePoolSize was made after PeerConnection has been initialized.
ErrModifyingICECandidatePoolSize = errors.New("ice candidate pool size cannot be modified")
// ErrStringSizeLimit indicates that the character size limit of string is
// exceeded. The limit is hardcoded to 65535 according to specifications.
ErrStringSizeLimit = errors.New("data channel label exceeds size limit")
// ErrMaxDataChannelID indicates that the maximum number ID that could be
// specified for a data channel has been exceeded.
ErrMaxDataChannelID = errors.New("maximum number ID for datachannel specified")
// ErrNegotiatedWithoutID indicates that an attempt to create a data channel
// was made while setting the negotiated option to true without providing
// the negotiated channel ID.
ErrNegotiatedWithoutID = errors.New("negotiated set without channel id")
// ErrRetransmitsOrPacketLifeTime indicates that an attempt to create a data
// channel was made with both options MaxPacketLifeTime and MaxRetransmits
// set together. Such configuration is not supported by the specification
// and is mutually exclusive.
ErrRetransmitsOrPacketLifeTime = errors.New("both MaxPacketLifeTime and MaxRetransmits was set")
// ErrCodecNotFound is returned when a codec search to the Media Engine fails
ErrCodecNotFound = errors.New("codec not found")
// ErrNoRemoteDescription indicates that an operation was rejected because
// the remote description is not set
ErrNoRemoteDescription = errors.New("remote description is not set")
// ErrIncorrectSDPSemantics indicates that the PeerConnection was configured to
// generate SDP Answers with different SDP Semantics than the received Offer
ErrIncorrectSDPSemantics = errors.New("offer SDP semantics does not match configuration")
// ErrIncorrectSignalingState indicates that the signaling state of PeerConnection is not correct
ErrIncorrectSignalingState = errors.New("operation can not be run in current signaling state")
// ErrProtocolTooLarge indicates that value given for a DataChannelInit protocol is
// longer then 65535 bytes
ErrProtocolTooLarge = errors.New("protocol is larger then 65535 bytes")
// ErrSenderNotCreatedByConnection indicates RemoveTrack was called with a RtpSender not created
// by this PeerConnection
ErrSenderNotCreatedByConnection = errors.New("RtpSender not created by this PeerConnection")
// ErrSessionDescriptionNoFingerprint indicates SetRemoteDescription was called with a SessionDescription that has no
// fingerprint
ErrSessionDescriptionNoFingerprint = errors.New("SetRemoteDescription called with no fingerprint")
// ErrSessionDescriptionInvalidFingerprint indicates SetRemoteDescription was called with a SessionDescription that
// has an invalid fingerprint
ErrSessionDescriptionInvalidFingerprint = errors.New("SetRemoteDescription called with an invalid fingerprint")
// ErrSessionDescriptionConflictingFingerprints indicates SetRemoteDescription was called with a SessionDescription that
// has an conflicting fingerprints
ErrSessionDescriptionConflictingFingerprints = errors.New("SetRemoteDescription called with multiple conflicting fingerprint")
// ErrSessionDescriptionMissingIceUfrag indicates SetRemoteDescription was called with a SessionDescription that
// is missing an ice-ufrag value
ErrSessionDescriptionMissingIceUfrag = errors.New("SetRemoteDescription called with no ice-ufrag")
// ErrSessionDescriptionMissingIcePwd indicates SetRemoteDescription was called with a SessionDescription that
// is missing an ice-pwd value
ErrSessionDescriptionMissingIcePwd = errors.New("SetRemoteDescription called with no ice-pwd")
// ErrSessionDescriptionConflictingIceUfrag indicates SetRemoteDescription was called with a SessionDescription that
// contains multiple conflicting ice-ufrag values
ErrSessionDescriptionConflictingIceUfrag = errors.New("SetRemoteDescription called with multiple conflicting ice-ufrag values")
// ErrSessionDescriptionConflictingIcePwd indicates SetRemoteDescription was called with a SessionDescription that
// contains multiple conflicting ice-pwd values
ErrSessionDescriptionConflictingIcePwd = errors.New("SetRemoteDescription called with multiple conflicting ice-pwd values")
// ErrNoSRTPProtectionProfile indicates that the DTLS handshake completed and no SRTP Protection Profile was chosen
ErrNoSRTPProtectionProfile = errors.New("DTLS Handshake completed and no SRTP Protection Profile was chosen")
// ErrFailedToGenerateCertificateFingerprint indicates that we failed to generate the fingerprint used for comparing certificates
ErrFailedToGenerateCertificateFingerprint = errors.New("failed to generate certificate fingerprint")
// ErrNoCodecsAvailable indicates that operation isn't possible because the MediaEngine has no codecs available
ErrNoCodecsAvailable = errors.New("operation failed no codecs are available")
// ErrUnsupportedCodec indicates the remote peer doesn't support the requested codec
ErrUnsupportedCodec = errors.New("unable to start track, codec is not supported by remote")
// ErrUnbindFailed indicates that a TrackLocal was not able to be unbind
ErrUnbindFailed = errors.New("failed to unbind TrackLocal from PeerConnection")
// ErrNoPayloaderForCodec indicates that the requested codec does not have a payloader
ErrNoPayloaderForCodec = errors.New("the requested codec does not have a payloader")
// ErrRegisterHeaderExtensionInvalidDirection indicates that a extension was registered with a direction besides `sendonly` or `recvonly`
ErrRegisterHeaderExtensionInvalidDirection = errors.New("a header extension must be registered as 'recvonly', 'sendonly' or both")
// ErrSimulcastProbeOverflow indicates that too many Simulcast probe streams are in flight and the requested SSRC was ignored
ErrSimulcastProbeOverflow = errors.New("simulcast probe limit has been reached, new SSRC has been discarded")
errDetachNotEnabled = errors.New("enable detaching by calling webrtc.DetachDataChannels()")
errDetachBeforeOpened = errors.New("datachannel not opened yet, try calling Detach from OnOpen")
errDtlsTransportNotStarted = errors.New("the DTLS transport has not started yet")
errDtlsKeyExtractionFailed = errors.New("failed extracting keys from DTLS for SRTP")
errFailedToStartSRTP = errors.New("failed to start SRTP")
errFailedToStartSRTCP = errors.New("failed to start SRTCP")
errInvalidDTLSStart = errors.New("attempted to start DTLSTransport that is not in new state")
errNoRemoteCertificate = errors.New("peer didn't provide certificate via DTLS")
errIdentityProviderNotImplemented = errors.New("identity provider is not implemented")
errNoMatchingCertificateFingerprint = errors.New("remote certificate does not match any fingerprint")
errICEConnectionNotStarted = errors.New("ICE connection not started")
errICECandidateTypeUnknown = errors.New("unknown candidate type")
errICEInvalidConvertCandidateType = errors.New("cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type")
errICEAgentNotExist = errors.New("ICEAgent does not exist")
errICECandiatesCoversionFailed = errors.New("unable to convert ICE candidates to ICECandidates")
errICERoleUnknown = errors.New("unknown ICE Role")
errICEProtocolUnknown = errors.New("unknown protocol")
errICEGathererNotStarted = errors.New("gatherer not started")
errNetworkTypeUnknown = errors.New("unknown network type")
errSDPDoesNotMatchOffer = errors.New("new sdp does not match previous offer")
errSDPDoesNotMatchAnswer = errors.New("new sdp does not match previous answer")
errPeerConnSDPTypeInvalidValue = errors.New("provided value is not a valid enum value of type SDPType")
errPeerConnStateChangeInvalid = errors.New("invalid state change op")
errPeerConnStateChangeUnhandled = errors.New("unhandled state change op")
errPeerConnSDPTypeInvalidValueSetLocalDescription = errors.New("invalid SDP type supplied to SetLocalDescription()")
errPeerConnRemoteDescriptionWithoutMidValue = errors.New("remoteDescription contained media section without mid value")
errPeerConnRemoteDescriptionNil = errors.New("remoteDescription has not been set yet")
errPeerConnSingleMediaSectionHasExplicitSSRC = errors.New("single media section has an explicit SSRC")
errPeerConnRemoteSSRCAddTransceiver = errors.New("could not add transceiver for remote SSRC")
errPeerConnSimulcastMidRTPExtensionRequired = errors.New("mid RTP Extensions required for Simulcast")
errPeerConnSimulcastStreamIDRTPExtensionRequired = errors.New("stream id RTP Extensions required for Simulcast")
errPeerConnSimulcastIncomingSSRCFailed = errors.New("incoming SSRC failed Simulcast probing")
errPeerConnAddTransceiverFromKindOnlyAcceptsOne = errors.New("AddTransceiverFromKind only accepts one RtpTransceiverInit")
errPeerConnAddTransceiverFromTrackOnlyAcceptsOne = errors.New("AddTransceiverFromTrack only accepts one RtpTransceiverInit")
errPeerConnAddTransceiverFromKindSupport = errors.New("AddTransceiverFromKind currently only supports recvonly")
errPeerConnAddTransceiverFromTrackSupport = errors.New("AddTransceiverFromTrack currently only supports sendonly and sendrecv")
errPeerConnSetIdentityProviderNotImplemented = errors.New("TODO SetIdentityProvider")
errPeerConnWriteRTCPOpenWriteStream = errors.New("WriteRTCP failed to open WriteStream")
errPeerConnTranscieverMidNil = errors.New("cannot find transceiver with mid")
errRTPReceiverDTLSTransportNil = errors.New("DTLSTransport must not be nil")
errRTPReceiverReceiveAlreadyCalled = errors.New("Receive has already been called")
errRTPReceiverWithSSRCTrackStreamNotFound = errors.New("unable to find stream for Track with SSRC")
errRTPReceiverForSSRCTrackStreamNotFound = errors.New("no trackStreams found for SSRC")
errRTPReceiverForRIDTrackStreamNotFound = errors.New("no trackStreams found for RID")
errRTPSenderTrackNil = errors.New("Track must not be nil")
errRTPSenderDTLSTransportNil = errors.New("DTLSTransport must not be nil")
errRTPSenderSendAlreadyCalled = errors.New("Send has already been called")
errRTPTransceiverCannotChangeMid = errors.New("errRTPSenderTrackNil")
errRTPTransceiverSetSendingInvalidState = errors.New("invalid state change in RTPTransceiver.setSending")
errSCTPTransportDTLS = errors.New("DTLS not established")
errSDPZeroTransceivers = errors.New("addTransceiverSDP() called with 0 transceivers")
errSDPMediaSectionMediaDataChanInvalid = errors.New("invalid Media Section. Media + DataChannel both enabled")
errSDPMediaSectionMultipleTrackInvalid = errors.New("invalid Media Section. Can not have multiple tracks in one MediaSection in UnifiedPlan")
errSettingEngineSetAnsweringDTLSRole = errors.New("SetAnsweringDTLSRole must DTLSRoleClient or DTLSRoleServer")
errSignalingStateCannotRollback = errors.New("can't rollback from stable state")
errSignalingStateProposedTransitionInvalid = errors.New("invalid proposed signaling state transition")
errStatsICECandidateStateInvalid = errors.New("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state")
errICETransportNotInNew = errors.New("ICETransport can only be called in ICETransportStateNew")
)

View file

@ -0,0 +1,24 @@
package webrtc
import (
"context"
)
// GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete.
// This function may be helpful in cases where you are unable to trickle your ICE Candidates.
//
// It is better to not use this function, and instead trickle candidates. If you use this function you will see longer connection startup times.
// When the call is connected you will see no impact however.
func GatheringCompletePromise(pc *PeerConnection) (gatherComplete <-chan struct{}) {
gatheringComplete, done := context.WithCancel(context.Background())
// It's possible to miss the GatherComplete event since setGatherCompleteHandler is an atomic operation and the
// promise might have been created after the gathering is finished. Therefore, we need to check if the ICE gathering
// state has changed to complete so that we don't block the caller forever.
pc.setGatherCompleteHandler(func() { done() })
if pc.ICEGatheringState() == ICEGatheringStateComplete {
done()
}
return gatheringComplete.Done()
}

View file

@ -0,0 +1,23 @@
module github.com/pion/webrtc/v3
go 1.12
require (
github.com/onsi/ginkgo v1.14.2 // indirect
github.com/onsi/gomega v1.10.3 // indirect
github.com/pion/datachannel v1.4.21
github.com/pion/dtls/v2 v2.0.4
github.com/pion/ice/v2 v2.0.14
github.com/pion/interceptor v0.0.9
github.com/pion/logging v0.2.2
github.com/pion/randutil v0.1.0
github.com/pion/rtcp v1.2.6
github.com/pion/rtp v1.6.2
github.com/pion/sctp v1.7.11
github.com/pion/sdp/v3 v3.0.4
github.com/pion/srtp/v2 v2.0.1
github.com/pion/transport v0.12.2
github.com/sclevine/agouti v3.0.0+incompatible
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7
)

View file

@ -0,0 +1,138 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI=
github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
github.com/pion/ice/v2 v2.0.14 h1:FxXxauyykf89SWAtkQCfnHkno6G8+bhRkNguSh9zU+4=
github.com/pion/ice/v2 v2.0.14/go.mod h1:wqaUbOq5ObDNU5ox1hRsEst0rWfsKuH1zXjQFEWiZwM=
github.com/pion/interceptor v0.0.9 h1:fk5hTdyLO3KURQsf/+RjMpEm4NE3yeTY9Kh97b5BvwA=
github.com/pion/interceptor v0.0.9/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
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.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U=
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE=
github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp/v2 v2.0.1 h1:kgfh65ob3EcnFYA4kUBvU/menCp9u7qaJLXwWgpobzs=
github.com/pion/srtp/v2 v2.0.1/go.mod h1:c8NWHhhkFf/drmHTAblkdu8++lsISEBBdAuiyxgqIsE=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,10 @@
// +build !js
package webrtc
// NewICETransport creates a new NewICETransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewICETransport(gatherer *ICEGatherer) *ICETransport {
return NewICETransport(gatherer, api.settingEngine.LoggerFactory)
}

View file

@ -0,0 +1,169 @@
package webrtc
import (
"fmt"
"github.com/pion/ice/v2"
)
// ICECandidate represents a ice candidate
type ICECandidate struct {
statsID string
Foundation string `json:"foundation"`
Priority uint32 `json:"priority"`
Address string `json:"address"`
Protocol ICEProtocol `json:"protocol"`
Port uint16 `json:"port"`
Typ ICECandidateType `json:"type"`
Component uint16 `json:"component"`
RelatedAddress string `json:"relatedAddress"`
RelatedPort uint16 `json:"relatedPort"`
TCPType string `json:"tcpType"`
}
// Conversion for package ice
func newICECandidatesFromICE(iceCandidates []ice.Candidate) ([]ICECandidate, error) {
candidates := []ICECandidate{}
for _, i := range iceCandidates {
c, err := newICECandidateFromICE(i)
if err != nil {
return nil, err
}
candidates = append(candidates, c)
}
return candidates, nil
}
func newICECandidateFromICE(i ice.Candidate) (ICECandidate, error) {
typ, err := convertTypeFromICE(i.Type())
if err != nil {
return ICECandidate{}, err
}
protocol, err := NewICEProtocol(i.NetworkType().NetworkShort())
if err != nil {
return ICECandidate{}, err
}
c := ICECandidate{
statsID: i.ID(),
Foundation: i.Foundation(),
Priority: i.Priority(),
Address: i.Address(),
Protocol: protocol,
Port: uint16(i.Port()),
Component: i.Component(),
Typ: typ,
TCPType: i.TCPType().String(),
}
if i.RelatedAddress() != nil {
c.RelatedAddress = i.RelatedAddress().Address
c.RelatedPort = uint16(i.RelatedAddress().Port)
}
return c, nil
}
func (c ICECandidate) toICE() (ice.Candidate, error) {
candidateID := c.statsID
switch c.Typ {
case ICECandidateTypeHost:
config := ice.CandidateHostConfig{
CandidateID: candidateID,
Network: c.Protocol.String(),
Address: c.Address,
Port: int(c.Port),
Component: c.Component,
TCPType: ice.NewTCPType(c.TCPType),
Foundation: c.Foundation,
Priority: c.Priority,
}
return ice.NewCandidateHost(&config)
case ICECandidateTypeSrflx:
config := ice.CandidateServerReflexiveConfig{
CandidateID: candidateID,
Network: c.Protocol.String(),
Address: c.Address,
Port: int(c.Port),
Component: c.Component,
Foundation: c.Foundation,
Priority: c.Priority,
RelAddr: c.RelatedAddress,
RelPort: int(c.RelatedPort),
}
return ice.NewCandidateServerReflexive(&config)
case ICECandidateTypePrflx:
config := ice.CandidatePeerReflexiveConfig{
CandidateID: candidateID,
Network: c.Protocol.String(),
Address: c.Address,
Port: int(c.Port),
Component: c.Component,
Foundation: c.Foundation,
Priority: c.Priority,
RelAddr: c.RelatedAddress,
RelPort: int(c.RelatedPort),
}
return ice.NewCandidatePeerReflexive(&config)
case ICECandidateTypeRelay:
config := ice.CandidateRelayConfig{
CandidateID: candidateID,
Network: c.Protocol.String(),
Address: c.Address,
Port: int(c.Port),
Component: c.Component,
Foundation: c.Foundation,
Priority: c.Priority,
RelAddr: c.RelatedAddress,
RelPort: int(c.RelatedPort),
}
return ice.NewCandidateRelay(&config)
default:
return nil, fmt.Errorf("%w: %s", errICECandidateTypeUnknown, c.Typ)
}
}
func convertTypeFromICE(t ice.CandidateType) (ICECandidateType, error) {
switch t {
case ice.CandidateTypeHost:
return ICECandidateTypeHost, nil
case ice.CandidateTypeServerReflexive:
return ICECandidateTypeSrflx, nil
case ice.CandidateTypePeerReflexive:
return ICECandidateTypePrflx, nil
case ice.CandidateTypeRelay:
return ICECandidateTypeRelay, nil
default:
return ICECandidateType(t), fmt.Errorf("%w: %s", errICECandidateTypeUnknown, t)
}
}
func (c ICECandidate) String() string {
ic, err := c.toICE()
if err != nil {
return fmt.Sprintf("%#v failed to convert to ICE: %s", c, err)
}
return ic.String()
}
// ToJSON returns an ICECandidateInit
// as indicated by the spec https://w3c.github.io/webrtc-pc/#dom-rtcicecandidate-tojson
func (c ICECandidate) ToJSON() ICECandidateInit {
zeroVal := uint16(0)
emptyStr := ""
candidateStr := ""
candidate, err := c.toICE()
if err == nil {
candidateStr = candidate.Marshal()
}
return ICECandidateInit{
Candidate: fmt.Sprintf("candidate:%s", candidateStr),
SDPMid: &emptyStr,
SDPMLineIndex: &zeroVal,
}
}

View file

@ -0,0 +1,9 @@
package webrtc
// ICECandidateInit is used to serialize ice candidates
type ICECandidateInit struct {
Candidate string `json:"candidate"`
SDPMid *string `json:"sdpMid"`
SDPMLineIndex *uint16 `json:"sdpMLineIndex"`
UsernameFragment *string `json:"usernameFragment"`
}

View file

@ -0,0 +1,29 @@
package webrtc
import "fmt"
// ICECandidatePair represents an ICE Candidate pair
type ICECandidatePair struct {
statsID string
Local *ICECandidate
Remote *ICECandidate
}
func newICECandidatePairStatsID(localID, remoteID string) string {
return fmt.Sprintf("%s-%s", localID, remoteID)
}
func (p *ICECandidatePair) String() string {
return fmt.Sprintf("(local) %s <-> (remote) %s", p.Local, p.Remote)
}
// NewICECandidatePair returns an initialized *ICECandidatePair
// for the given pair of ICECandidate instances
func NewICECandidatePair(local, remote *ICECandidate) *ICECandidatePair {
statsID := newICECandidatePairStatsID(local.statsID, remote.statsID)
return &ICECandidatePair{
statsID: statsID,
Local: local,
Remote: remote,
}
}

View file

@ -0,0 +1,94 @@
package webrtc
import (
"fmt"
"github.com/pion/ice/v2"
)
// ICECandidateType represents the type of the ICE candidate used.
type ICECandidateType int
const (
// ICECandidateTypeHost indicates that the candidate is of Host type as
// described in https://tools.ietf.org/html/rfc8445#section-5.1.1.1. A
// candidate obtained by binding to a specific port from an IP address on
// the host. This includes IP addresses on physical interfaces and logical
// ones, such as ones obtained through VPNs.
ICECandidateTypeHost ICECandidateType = iota + 1
// ICECandidateTypeSrflx indicates the the candidate is of Server
// Reflexive type as described
// https://tools.ietf.org/html/rfc8445#section-5.1.1.2. A candidate type
// whose IP address and port are a binding allocated by a NAT for an ICE
// agent after it sends a packet through the NAT to a server, such as a
// STUN server.
ICECandidateTypeSrflx
// ICECandidateTypePrflx indicates that the candidate is of Peer
// Reflexive type. A candidate type whose IP address and port are a binding
// allocated by a NAT for an ICE agent after it sends a packet through the
// NAT to its peer.
ICECandidateTypePrflx
// ICECandidateTypeRelay indicates the the candidate is of Relay type as
// described in https://tools.ietf.org/html/rfc8445#section-5.1.1.2. A
// candidate type obtained from a relay server, such as a TURN server.
ICECandidateTypeRelay
)
// This is done this way because of a linter.
const (
iceCandidateTypeHostStr = "host"
iceCandidateTypeSrflxStr = "srflx"
iceCandidateTypePrflxStr = "prflx"
iceCandidateTypeRelayStr = "relay"
)
// NewICECandidateType takes a string and converts it into ICECandidateType
func NewICECandidateType(raw string) (ICECandidateType, error) {
switch raw {
case iceCandidateTypeHostStr:
return ICECandidateTypeHost, nil
case iceCandidateTypeSrflxStr:
return ICECandidateTypeSrflx, nil
case iceCandidateTypePrflxStr:
return ICECandidateTypePrflx, nil
case iceCandidateTypeRelayStr:
return ICECandidateTypeRelay, nil
default:
return ICECandidateType(Unknown), fmt.Errorf("%w: %s", errICECandidateTypeUnknown, raw)
}
}
func (t ICECandidateType) String() string {
switch t {
case ICECandidateTypeHost:
return iceCandidateTypeHostStr
case ICECandidateTypeSrflx:
return iceCandidateTypeSrflxStr
case ICECandidateTypePrflx:
return iceCandidateTypePrflxStr
case ICECandidateTypeRelay:
return iceCandidateTypeRelayStr
default:
return ErrUnknownType.Error()
}
}
func getCandidateType(candidateType ice.CandidateType) (ICECandidateType, error) {
switch candidateType {
case ice.CandidateTypeHost:
return ICECandidateTypeHost, nil
case ice.CandidateTypeServerReflexive:
return ICECandidateTypeSrflx, nil
case ice.CandidateTypePeerReflexive:
return ICECandidateTypePrflx, nil
case ice.CandidateTypeRelay:
return ICECandidateTypeRelay, nil
default:
// NOTE: this should never happen[tm]
err := fmt.Errorf("%w: %s", errICEInvalidConvertCandidateType, candidateType.String())
return ICECandidateType(Unknown), err
}
}

View file

@ -0,0 +1,47 @@
package webrtc
// ICEComponent describes if the ice transport is used for RTP
// (or RTCP multiplexing).
type ICEComponent int
const (
// ICEComponentRTP indicates that the ICE Transport is used for RTP (or
// RTCP multiplexing), as defined in
// https://tools.ietf.org/html/rfc5245#section-4.1.1.1. Protocols
// multiplexed with RTP (e.g. data channel) share its component ID. This
// represents the component-id value 1 when encoded in candidate-attribute.
ICEComponentRTP ICEComponent = iota + 1
// ICEComponentRTCP indicates that the ICE Transport is used for RTCP as
// defined by https://tools.ietf.org/html/rfc5245#section-4.1.1.1. This
// represents the component-id value 2 when encoded in candidate-attribute.
ICEComponentRTCP
)
// This is done this way because of a linter.
const (
iceComponentRTPStr = "rtp"
iceComponentRTCPStr = "rtcp"
)
func newICEComponent(raw string) ICEComponent {
switch raw {
case iceComponentRTPStr:
return ICEComponentRTP
case iceComponentRTCPStr:
return ICEComponentRTCP
default:
return ICEComponent(Unknown)
}
}
func (t ICEComponent) String() string {
switch t {
case ICEComponentRTP:
return iceComponentRTPStr
case ICEComponentRTCP:
return iceComponentRTCPStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,94 @@
package webrtc
// ICEConnectionState indicates signaling state of the ICE Connection.
type ICEConnectionState int
const (
// ICEConnectionStateNew indicates that any of the ICETransports are
// in the "new" state and none of them are in the "checking", "disconnected"
// or "failed" state, or all ICETransports are in the "closed" state, or
// there are no transports.
ICEConnectionStateNew ICEConnectionState = iota + 1
// ICEConnectionStateChecking indicates that any of the ICETransports
// are in the "checking" state and none of them are in the "disconnected"
// or "failed" state.
ICEConnectionStateChecking
// ICEConnectionStateConnected indicates that all ICETransports are
// in the "connected", "completed" or "closed" state and at least one of
// them is in the "connected" state.
ICEConnectionStateConnected
// ICEConnectionStateCompleted indicates that all ICETransports are
// in the "completed" or "closed" state and at least one of them is in the
// "completed" state.
ICEConnectionStateCompleted
// ICEConnectionStateDisconnected indicates that any of the
// ICETransports are in the "disconnected" state and none of them are
// in the "failed" state.
ICEConnectionStateDisconnected
// ICEConnectionStateFailed indicates that any of the ICETransports
// are in the "failed" state.
ICEConnectionStateFailed
// ICEConnectionStateClosed indicates that the PeerConnection's
// isClosed is true.
ICEConnectionStateClosed
)
// This is done this way because of a linter.
const (
iceConnectionStateNewStr = "new"
iceConnectionStateCheckingStr = "checking"
iceConnectionStateConnectedStr = "connected"
iceConnectionStateCompletedStr = "completed"
iceConnectionStateDisconnectedStr = "disconnected"
iceConnectionStateFailedStr = "failed"
iceConnectionStateClosedStr = "closed"
)
// NewICEConnectionState takes a string and converts it to ICEConnectionState
func NewICEConnectionState(raw string) ICEConnectionState {
switch raw {
case iceConnectionStateNewStr:
return ICEConnectionStateNew
case iceConnectionStateCheckingStr:
return ICEConnectionStateChecking
case iceConnectionStateConnectedStr:
return ICEConnectionStateConnected
case iceConnectionStateCompletedStr:
return ICEConnectionStateCompleted
case iceConnectionStateDisconnectedStr:
return ICEConnectionStateDisconnected
case iceConnectionStateFailedStr:
return ICEConnectionStateFailed
case iceConnectionStateClosedStr:
return ICEConnectionStateClosed
default:
return ICEConnectionState(Unknown)
}
}
func (c ICEConnectionState) String() string {
switch c {
case ICEConnectionStateNew:
return iceConnectionStateNewStr
case ICEConnectionStateChecking:
return iceConnectionStateCheckingStr
case ICEConnectionStateConnected:
return iceConnectionStateConnectedStr
case ICEConnectionStateCompleted:
return iceConnectionStateCompletedStr
case ICEConnectionStateDisconnected:
return iceConnectionStateDisconnectedStr
case ICEConnectionStateFailed:
return iceConnectionStateFailedStr
case ICEConnectionStateClosed:
return iceConnectionStateClosedStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,43 @@
package webrtc
// ICECredentialType indicates the type of credentials used to connect to
// an ICE server.
type ICECredentialType int
const (
// ICECredentialTypePassword describes username and password based
// credentials as described in https://tools.ietf.org/html/rfc5389.
ICECredentialTypePassword ICECredentialType = iota
// ICECredentialTypeOauth describes token based credential as described
// in https://tools.ietf.org/html/rfc7635.
ICECredentialTypeOauth
)
// This is done this way because of a linter.
const (
iceCredentialTypePasswordStr = "password"
iceCredentialTypeOauthStr = "oauth"
)
func newICECredentialType(raw string) ICECredentialType {
switch raw {
case iceCredentialTypePasswordStr:
return ICECredentialTypePassword
case iceCredentialTypeOauthStr:
return ICECredentialTypeOauth
default:
return ICECredentialType(Unknown)
}
}
func (t ICECredentialType) String() string {
switch t {
case ICECredentialTypePassword:
return iceCredentialTypePasswordStr
case ICECredentialTypeOauth:
return iceCredentialTypeOauthStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,367 @@
// +build !js
package webrtc
import (
"sync"
"sync/atomic"
"github.com/pion/ice/v2"
"github.com/pion/logging"
)
// ICEGatherer gathers local host, server reflexive and relay
// candidates, as well as enabling the retrieval of local Interactive
// Connectivity Establishment (ICE) parameters which can be
// exchanged in signaling.
type ICEGatherer struct {
lock sync.RWMutex
log logging.LeveledLogger
state ICEGathererState
validatedServers []*ice.URL
gatherPolicy ICETransportPolicy
agent *ice.Agent
onLocalCandidateHandler atomic.Value // func(candidate *ICECandidate)
onStateChangeHandler atomic.Value // func(state ICEGathererState)
// Used for GatheringCompletePromise
onGatheringCompleteHandler atomic.Value // func()
api *API
}
// NewICEGatherer creates a new NewICEGatherer.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewICEGatherer(opts ICEGatherOptions) (*ICEGatherer, error) {
var validatedServers []*ice.URL
if len(opts.ICEServers) > 0 {
for _, server := range opts.ICEServers {
url, err := server.urls()
if err != nil {
return nil, err
}
validatedServers = append(validatedServers, url...)
}
}
return &ICEGatherer{
state: ICEGathererStateNew,
gatherPolicy: opts.ICEGatherPolicy,
validatedServers: validatedServers,
api: api,
log: api.settingEngine.LoggerFactory.NewLogger("ice"),
}, nil
}
func (g *ICEGatherer) createAgent() error {
g.lock.Lock()
defer g.lock.Unlock()
if g.agent != nil || g.State() != ICEGathererStateNew {
return nil
}
candidateTypes := []ice.CandidateType{}
if g.api.settingEngine.candidates.ICELite {
candidateTypes = append(candidateTypes, ice.CandidateTypeHost)
} else if g.gatherPolicy == ICETransportPolicyRelay {
candidateTypes = append(candidateTypes, ice.CandidateTypeRelay)
}
var nat1To1CandiTyp ice.CandidateType
switch g.api.settingEngine.candidates.NAT1To1IPCandidateType {
case ICECandidateTypeHost:
nat1To1CandiTyp = ice.CandidateTypeHost
case ICECandidateTypeSrflx:
nat1To1CandiTyp = ice.CandidateTypeServerReflexive
default:
nat1To1CandiTyp = ice.CandidateTypeUnspecified
}
mDNSMode := g.api.settingEngine.candidates.MulticastDNSMode
if mDNSMode != ice.MulticastDNSModeDisabled && mDNSMode != ice.MulticastDNSModeQueryAndGather {
// If enum is in state we don't recognized default to MulticastDNSModeQueryOnly
mDNSMode = ice.MulticastDNSModeQueryOnly
}
config := &ice.AgentConfig{
Lite: g.api.settingEngine.candidates.ICELite,
Urls: g.validatedServers,
PortMin: g.api.settingEngine.ephemeralUDP.PortMin,
PortMax: g.api.settingEngine.ephemeralUDP.PortMax,
DisconnectedTimeout: g.api.settingEngine.timeout.ICEDisconnectedTimeout,
FailedTimeout: g.api.settingEngine.timeout.ICEFailedTimeout,
KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepaliveInterval,
LoggerFactory: g.api.settingEngine.LoggerFactory,
CandidateTypes: candidateTypes,
HostAcceptanceMinWait: g.api.settingEngine.timeout.ICEHostAcceptanceMinWait,
SrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait,
PrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait,
RelayAcceptanceMinWait: g.api.settingEngine.timeout.ICERelayAcceptanceMinWait,
InterfaceFilter: g.api.settingEngine.candidates.InterfaceFilter,
NAT1To1IPs: g.api.settingEngine.candidates.NAT1To1IPs,
NAT1To1IPCandidateType: nat1To1CandiTyp,
Net: g.api.settingEngine.vnet,
MulticastDNSMode: mDNSMode,
MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName,
LocalUfrag: g.api.settingEngine.candidates.UsernameFragment,
LocalPwd: g.api.settingEngine.candidates.Password,
TCPMux: g.api.settingEngine.iceTCPMux,
ProxyDialer: g.api.settingEngine.iceProxyDialer,
}
requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
if len(requestedNetworkTypes) == 0 {
requestedNetworkTypes = supportedNetworkTypes()
}
for _, typ := range requestedNetworkTypes {
config.NetworkTypes = append(config.NetworkTypes, ice.NetworkType(typ))
}
agent, err := ice.NewAgent(config)
if err != nil {
return err
}
g.agent = agent
return nil
}
// Gather ICE candidates.
func (g *ICEGatherer) Gather() error {
if err := g.createAgent(); err != nil {
return err
}
g.lock.Lock()
agent := g.agent
g.lock.Unlock()
g.setState(ICEGathererStateGathering)
if err := agent.OnCandidate(func(candidate ice.Candidate) {
onLocalCandidateHandler := func(*ICECandidate) {}
if handler, ok := g.onLocalCandidateHandler.Load().(func(candidate *ICECandidate)); ok && handler != nil {
onLocalCandidateHandler = handler
}
onGatheringCompleteHandler := func() {}
if handler, ok := g.onGatheringCompleteHandler.Load().(func()); ok && handler != nil {
onGatheringCompleteHandler = handler
}
if candidate != nil {
c, err := newICECandidateFromICE(candidate)
if err != nil {
g.log.Warnf("Failed to convert ice.Candidate: %s", err)
return
}
onLocalCandidateHandler(&c)
} else {
g.setState(ICEGathererStateComplete)
onGatheringCompleteHandler()
onLocalCandidateHandler(nil)
}
}); err != nil {
return err
}
return agent.GatherCandidates()
}
// Close prunes all local candidates, and closes the ports.
func (g *ICEGatherer) Close() error {
g.lock.Lock()
defer g.lock.Unlock()
if g.agent == nil {
return nil
} else if err := g.agent.Close(); err != nil {
return err
}
g.agent = nil
g.setState(ICEGathererStateClosed)
return nil
}
// GetLocalParameters returns the ICE parameters of the ICEGatherer.
func (g *ICEGatherer) GetLocalParameters() (ICEParameters, error) {
if err := g.createAgent(); err != nil {
return ICEParameters{}, err
}
frag, pwd, err := g.agent.GetLocalUserCredentials()
if err != nil {
return ICEParameters{}, err
}
return ICEParameters{
UsernameFragment: frag,
Password: pwd,
ICELite: false,
}, nil
}
// GetLocalCandidates returns the sequence of valid local candidates associated with the ICEGatherer.
func (g *ICEGatherer) GetLocalCandidates() ([]ICECandidate, error) {
if err := g.createAgent(); err != nil {
return nil, err
}
iceCandidates, err := g.agent.GetLocalCandidates()
if err != nil {
return nil, err
}
return newICECandidatesFromICE(iceCandidates)
}
// OnLocalCandidate sets an event handler which fires when a new local ICE candidate is available
// Take note that the handler is gonna be called with a nil pointer when gathering is finished.
func (g *ICEGatherer) OnLocalCandidate(f func(*ICECandidate)) {
g.onLocalCandidateHandler.Store(f)
}
// OnStateChange fires any time the ICEGatherer changes
func (g *ICEGatherer) OnStateChange(f func(ICEGathererState)) {
g.onStateChangeHandler.Store(f)
}
// State indicates the current state of the ICE gatherer.
func (g *ICEGatherer) State() ICEGathererState {
return atomicLoadICEGathererState(&g.state)
}
func (g *ICEGatherer) setState(s ICEGathererState) {
atomicStoreICEGathererState(&g.state, s)
if handler, ok := g.onStateChangeHandler.Load().(func(state ICEGathererState)); ok && handler != nil {
handler(s)
}
}
func (g *ICEGatherer) getAgent() *ice.Agent {
g.lock.RLock()
defer g.lock.RUnlock()
return g.agent
}
func (g *ICEGatherer) collectStats(collector *statsReportCollector) {
agent := g.getAgent()
if agent == nil {
return
}
collector.Collecting()
go func(collector *statsReportCollector, agent *ice.Agent) {
for _, candidatePairStats := range agent.GetCandidatePairsStats() {
collector.Collecting()
state, err := toStatsICECandidatePairState(candidatePairStats.State)
if err != nil {
g.log.Error(err.Error())
}
pairID := newICECandidatePairStatsID(candidatePairStats.LocalCandidateID,
candidatePairStats.RemoteCandidateID)
stats := ICECandidatePairStats{
Timestamp: statsTimestampFrom(candidatePairStats.Timestamp),
Type: StatsTypeCandidatePair,
ID: pairID,
// TransportID:
LocalCandidateID: candidatePairStats.LocalCandidateID,
RemoteCandidateID: candidatePairStats.RemoteCandidateID,
State: state,
Nominated: candidatePairStats.Nominated,
PacketsSent: candidatePairStats.PacketsSent,
PacketsReceived: candidatePairStats.PacketsReceived,
BytesSent: candidatePairStats.BytesSent,
BytesReceived: candidatePairStats.BytesReceived,
LastPacketSentTimestamp: statsTimestampFrom(candidatePairStats.LastPacketSentTimestamp),
LastPacketReceivedTimestamp: statsTimestampFrom(candidatePairStats.LastPacketReceivedTimestamp),
FirstRequestTimestamp: statsTimestampFrom(candidatePairStats.FirstRequestTimestamp),
LastRequestTimestamp: statsTimestampFrom(candidatePairStats.LastRequestTimestamp),
LastResponseTimestamp: statsTimestampFrom(candidatePairStats.LastResponseTimestamp),
TotalRoundTripTime: candidatePairStats.TotalRoundTripTime,
CurrentRoundTripTime: candidatePairStats.CurrentRoundTripTime,
AvailableOutgoingBitrate: candidatePairStats.AvailableOutgoingBitrate,
AvailableIncomingBitrate: candidatePairStats.AvailableIncomingBitrate,
CircuitBreakerTriggerCount: candidatePairStats.CircuitBreakerTriggerCount,
RequestsReceived: candidatePairStats.RequestsReceived,
RequestsSent: candidatePairStats.RequestsSent,
ResponsesReceived: candidatePairStats.ResponsesReceived,
ResponsesSent: candidatePairStats.ResponsesSent,
RetransmissionsReceived: candidatePairStats.RetransmissionsReceived,
RetransmissionsSent: candidatePairStats.RetransmissionsSent,
ConsentRequestsSent: candidatePairStats.ConsentRequestsSent,
ConsentExpiredTimestamp: statsTimestampFrom(candidatePairStats.ConsentExpiredTimestamp),
}
collector.Collect(stats.ID, stats)
}
for _, candidateStats := range agent.GetLocalCandidatesStats() {
collector.Collecting()
networkType, err := getNetworkType(candidateStats.NetworkType)
if err != nil {
g.log.Error(err.Error())
}
candidateType, err := getCandidateType(candidateStats.CandidateType)
if err != nil {
g.log.Error(err.Error())
}
stats := ICECandidateStats{
Timestamp: statsTimestampFrom(candidateStats.Timestamp),
ID: candidateStats.ID,
Type: StatsTypeLocalCandidate,
NetworkType: networkType,
IP: candidateStats.IP,
Port: int32(candidateStats.Port),
Protocol: networkType.Protocol(),
CandidateType: candidateType,
Priority: int32(candidateStats.Priority),
URL: candidateStats.URL,
RelayProtocol: candidateStats.RelayProtocol,
Deleted: candidateStats.Deleted,
}
collector.Collect(stats.ID, stats)
}
for _, candidateStats := range agent.GetRemoteCandidatesStats() {
collector.Collecting()
networkType, err := getNetworkType(candidateStats.NetworkType)
if err != nil {
g.log.Error(err.Error())
}
candidateType, err := getCandidateType(candidateStats.CandidateType)
if err != nil {
g.log.Error(err.Error())
}
stats := ICECandidateStats{
Timestamp: statsTimestampFrom(candidateStats.Timestamp),
ID: candidateStats.ID,
Type: StatsTypeRemoteCandidate,
NetworkType: networkType,
IP: candidateStats.IP,
Port: int32(candidateStats.Port),
Protocol: networkType.Protocol(),
CandidateType: candidateType,
Priority: int32(candidateStats.Priority),
URL: candidateStats.URL,
RelayProtocol: candidateStats.RelayProtocol,
}
collector.Collect(stats.ID, stats)
}
collector.Done()
}(collector, agent)
}

View file

@ -0,0 +1,48 @@
package webrtc
import (
"sync/atomic"
)
// ICEGathererState represents the current state of the ICE gatherer.
type ICEGathererState uint32
const (
// ICEGathererStateNew indicates object has been created but
// gather() has not been called.
ICEGathererStateNew ICEGathererState = iota + 1
// ICEGathererStateGathering indicates gather() has been called,
// and the ICEGatherer is in the process of gathering candidates.
ICEGathererStateGathering
// ICEGathererStateComplete indicates the ICEGatherer has completed gathering.
ICEGathererStateComplete
// ICEGathererStateClosed indicates the closed state can only be entered
// when the ICEGatherer has been closed intentionally by calling close().
ICEGathererStateClosed
)
func (s ICEGathererState) String() string {
switch s {
case ICEGathererStateNew:
return "new"
case ICEGathererStateGathering:
return "gathering"
case ICEGathererStateComplete:
return "complete"
case ICEGathererStateClosed:
return "closed"
default:
return unknownStr
}
}
func atomicStoreICEGathererState(state *ICEGathererState, newState ICEGathererState) {
atomic.StoreUint32((*uint32)(state), uint32(newState))
}
func atomicLoadICEGathererState(state *ICEGathererState) ICEGathererState {
return ICEGathererState(atomic.LoadUint32((*uint32)(state)))
}

View file

@ -0,0 +1,53 @@
package webrtc
// ICEGatheringState describes the state of the candidate gathering process.
type ICEGatheringState int
const (
// ICEGatheringStateNew indicates that any of the ICETransports are
// in the "new" gathering state and none of the transports are in the
// "gathering" state, or there are no transports.
ICEGatheringStateNew ICEGatheringState = iota + 1
// ICEGatheringStateGathering indicates that any of the ICETransports
// are in the "gathering" state.
ICEGatheringStateGathering
// ICEGatheringStateComplete indicates that at least one ICETransport
// exists, and all ICETransports are in the "completed" gathering state.
ICEGatheringStateComplete
)
// This is done this way because of a linter.
const (
iceGatheringStateNewStr = "new"
iceGatheringStateGatheringStr = "gathering"
iceGatheringStateCompleteStr = "complete"
)
// NewICEGatheringState takes a string and converts it to ICEGatheringState
func NewICEGatheringState(raw string) ICEGatheringState {
switch raw {
case iceGatheringStateNewStr:
return ICEGatheringStateNew
case iceGatheringStateGatheringStr:
return ICEGatheringStateGathering
case iceGatheringStateCompleteStr:
return ICEGatheringStateComplete
default:
return ICEGatheringState(Unknown)
}
}
func (t ICEGatheringState) String() string {
switch t {
case ICEGatheringStateNew:
return iceGatheringStateNewStr
case ICEGatheringStateGathering:
return iceGatheringStateGatheringStr
case ICEGatheringStateComplete:
return iceGatheringStateCompleteStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,7 @@
package webrtc
// ICEGatherOptions provides options relating to the gathering of ICE candidates.
type ICEGatherOptions struct {
ICEServers []ICEServer
ICEGatherPolicy ICETransportPolicy
}

View file

@ -0,0 +1,9 @@
package webrtc
// ICEParameters includes the ICE username fragment
// and password and other ICE-related parameters.
type ICEParameters struct {
UsernameFragment string `json:"usernameFragment"`
Password string `json:"password"`
ICELite bool `json:"iceLite"`
}

View file

@ -0,0 +1,47 @@
package webrtc
import (
"fmt"
"strings"
)
// ICEProtocol indicates the transport protocol type that is used in the
// ice.URL structure.
type ICEProtocol int
const (
// ICEProtocolUDP indicates the URL uses a UDP transport.
ICEProtocolUDP ICEProtocol = iota + 1
// ICEProtocolTCP indicates the URL uses a TCP transport.
ICEProtocolTCP
)
// This is done this way because of a linter.
const (
iceProtocolUDPStr = "udp"
iceProtocolTCPStr = "tcp"
)
// NewICEProtocol takes a string and converts it to ICEProtocol
func NewICEProtocol(raw string) (ICEProtocol, error) {
switch {
case strings.EqualFold(iceProtocolUDPStr, raw):
return ICEProtocolUDP, nil
case strings.EqualFold(iceProtocolTCPStr, raw):
return ICEProtocolTCP, nil
default:
return ICEProtocol(Unknown), fmt.Errorf("%w: %s", errICEProtocolUnknown, raw)
}
}
func (t ICEProtocol) String() string {
switch t {
case ICEProtocolUDP:
return iceProtocolUDPStr
case ICEProtocolTCP:
return iceProtocolTCPStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,45 @@
package webrtc
// ICERole describes the role ice.Agent is playing in selecting the
// preferred the candidate pair.
type ICERole int
const (
// ICERoleControlling indicates that the ICE agent that is responsible
// for selecting the final choice of candidate pairs and signaling them
// through STUN and an updated offer, if needed. In any session, one agent
// is always controlling. The other is the controlled agent.
ICERoleControlling ICERole = iota + 1
// ICERoleControlled indicates that an ICE agent that waits for the
// controlling agent to select the final choice of candidate pairs.
ICERoleControlled
)
// This is done this way because of a linter.
const (
iceRoleControllingStr = "controlling"
iceRoleControlledStr = "controlled"
)
func newICERole(raw string) ICERole {
switch raw {
case iceRoleControllingStr:
return ICERoleControlling
case iceRoleControlledStr:
return ICERoleControlled
default:
return ICERole(Unknown)
}
}
func (t ICERole) String() string {
switch t {
case ICERoleControlling:
return iceRoleControllingStr
case ICERoleControlled:
return iceRoleControlledStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,68 @@
// +build !js
package webrtc
import (
"github.com/pion/ice/v2"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
// ICEServer describes a single STUN and TURN server that can be used by
// the ICEAgent to establish a connection with a peer.
type ICEServer struct {
URLs []string `json:"urls"`
Username string `json:"username,omitempty"`
Credential interface{} `json:"credential,omitempty"`
CredentialType ICECredentialType `json:"credentialType,omitempty"`
}
func (s ICEServer) parseURL(i int) (*ice.URL, error) {
return ice.ParseURL(s.URLs[i])
}
func (s ICEServer) validate() error {
_, err := s.urls()
return err
}
func (s ICEServer) urls() ([]*ice.URL, error) {
urls := []*ice.URL{}
for i := range s.URLs {
url, err := s.parseURL(i)
if err != nil {
return nil, &rtcerr.InvalidAccessError{Err: err}
}
if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS {
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.2)
if s.Username == "" || s.Credential == nil {
return nil, &rtcerr.InvalidAccessError{Err: ErrNoTurnCredentials}
}
url.Username = s.Username
switch s.CredentialType {
case ICECredentialTypePassword:
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.3)
password, ok := s.Credential.(string)
if !ok {
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}
}
url.Password = password
case ICECredentialTypeOauth:
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3.4)
if _, ok := s.Credential.(OAuthCredential); !ok {
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}
}
default:
return nil, &rtcerr.InvalidAccessError{Err: ErrTurnCredentials}
}
}
urls = append(urls, url)
}
return urls, nil
}

View file

@ -0,0 +1,42 @@
// +build js,wasm
package webrtc
import (
"errors"
"github.com/pion/ice/v2"
)
// ICEServer describes a single STUN and TURN server that can be used by
// the ICEAgent to establish a connection with a peer.
type ICEServer struct {
URLs []string
Username string
// Note: TURN is not supported in the WASM bindings yet
Credential interface{}
CredentialType ICECredentialType
}
func (s ICEServer) parseURL(i int) (*ice.URL, error) {
return ice.ParseURL(s.URLs[i])
}
func (s ICEServer) validate() ([]*ice.URL, error) {
urls := []*ice.URL{}
for i := range s.URLs {
url, err := s.parseURL(i)
if err != nil {
return nil, err
}
if url.Scheme == ice.SchemeTypeTURN || url.Scheme == ice.SchemeTypeTURNS {
return nil, errors.New("TURN is not currently supported in the JavaScript/Wasm bindings")
}
urls = append(urls, url)
}
return urls, nil
}

View file

@ -0,0 +1,18 @@
package webrtc
import (
"net"
"github.com/pion/ice/v2"
"github.com/pion/logging"
)
// NewICETCPMux creates a new instance of ice.TCPMuxDefault. It enables use of
// passive ICE TCP candidates.
func NewICETCPMux(logger logging.LeveledLogger, listener net.Listener, readBufferSize int) ice.TCPMux {
return ice.NewTCPMuxDefault(ice.TCPMuxParams{
Listener: listener,
Logger: logger,
ReadBufferSize: readBufferSize,
})
}

View file

@ -0,0 +1,368 @@
// +build !js
package webrtc
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/pion/ice/v2"
"github.com/pion/logging"
"github.com/pion/webrtc/v3/internal/mux"
)
// ICETransport allows an application access to information about the ICE
// transport over which packets are sent and received.
type ICETransport struct {
lock sync.RWMutex
role ICERole
// Component ICEComponent
// State ICETransportState
// gatheringState ICEGathererState
onConnectionStateChangeHandler atomic.Value // func(ICETransportState)
onSelectedCandidatePairChangeHandler atomic.Value // func(*ICECandidatePair)
state atomic.Value // ICETransportState
gatherer *ICEGatherer
conn *ice.Conn
mux *mux.Mux
ctx context.Context
ctxCancel func()
loggerFactory logging.LoggerFactory
log logging.LeveledLogger
}
// func (t *ICETransport) GetLocalCandidates() []ICECandidate {
//
// }
//
// func (t *ICETransport) GetRemoteCandidates() []ICECandidate {
//
// }
//
// func (t *ICETransport) GetSelectedCandidatePair() ICECandidatePair {
//
// }
//
// func (t *ICETransport) GetLocalParameters() ICEParameters {
//
// }
//
// func (t *ICETransport) GetRemoteParameters() ICEParameters {
//
// }
// NewICETransport creates a new NewICETransport.
func NewICETransport(gatherer *ICEGatherer, loggerFactory logging.LoggerFactory) *ICETransport {
iceTransport := &ICETransport{
gatherer: gatherer,
loggerFactory: loggerFactory,
log: loggerFactory.NewLogger("ortc"),
}
iceTransport.setState(ICETransportStateNew)
return iceTransport
}
// Start incoming connectivity checks based on its configured role.
func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *ICERole) error {
t.lock.Lock()
defer t.lock.Unlock()
if t.State() != ICETransportStateNew {
return errICETransportNotInNew
}
if gatherer != nil {
t.gatherer = gatherer
}
if err := t.ensureGatherer(); err != nil {
return err
}
agent := t.gatherer.getAgent()
if agent == nil {
return fmt.Errorf("%w: unable to start ICETransport", errICEAgentNotExist)
}
if err := agent.OnConnectionStateChange(func(iceState ice.ConnectionState) {
state := newICETransportStateFromICE(iceState)
t.setState(state)
t.onConnectionStateChange(state)
}); err != nil {
return err
}
if err := agent.OnSelectedCandidatePairChange(func(local, remote ice.Candidate) {
candidates, err := newICECandidatesFromICE([]ice.Candidate{local, remote})
if err != nil {
t.log.Warnf("%w: %s", errICECandiatesCoversionFailed, err)
return
}
t.onSelectedCandidatePairChange(NewICECandidatePair(&candidates[0], &candidates[1]))
}); err != nil {
return err
}
if role == nil {
controlled := ICERoleControlled
role = &controlled
}
t.role = *role
t.ctx, t.ctxCancel = context.WithCancel(context.Background())
// Drop the lock here to allow ICE candidates to be
// added so that the agent can complete a connection
t.lock.Unlock()
var iceConn *ice.Conn
var err error
switch *role {
case ICERoleControlling:
iceConn, err = agent.Dial(t.ctx,
params.UsernameFragment,
params.Password)
case ICERoleControlled:
iceConn, err = agent.Accept(t.ctx,
params.UsernameFragment,
params.Password)
default:
err = errICERoleUnknown
}
// Reacquire the lock to set the connection/mux
t.lock.Lock()
if err != nil {
return err
}
t.conn = iceConn
config := mux.Config{
Conn: t.conn,
BufferSize: receiveMTU,
LoggerFactory: t.loggerFactory,
}
t.mux = mux.NewMux(config)
return nil
}
// restart is not exposed currently because ORTC has users create a whole new ICETransport
// so for now lets keep it private so we don't cause ORTC users to depend on non-standard APIs
func (t *ICETransport) restart() error {
t.lock.Lock()
defer t.lock.Unlock()
agent := t.gatherer.getAgent()
if agent == nil {
return fmt.Errorf("%w: unable to restart ICETransport", errICEAgentNotExist)
}
if err := agent.Restart(t.gatherer.api.settingEngine.candidates.UsernameFragment, t.gatherer.api.settingEngine.candidates.Password); err != nil {
return err
}
return t.gatherer.Gather()
}
// Stop irreversibly stops the ICETransport.
func (t *ICETransport) Stop() error {
t.lock.Lock()
defer t.lock.Unlock()
t.setState(ICETransportStateClosed)
if t.ctxCancel != nil {
t.ctxCancel()
}
if t.mux != nil {
return t.mux.Close()
} else if t.gatherer != nil {
return t.gatherer.Close()
}
return nil
}
// OnSelectedCandidatePairChange sets a handler that is invoked when a new
// ICE candidate pair is selected
func (t *ICETransport) OnSelectedCandidatePairChange(f func(*ICECandidatePair)) {
t.onSelectedCandidatePairChangeHandler.Store(f)
}
func (t *ICETransport) onSelectedCandidatePairChange(pair *ICECandidatePair) {
handler := t.onSelectedCandidatePairChangeHandler.Load()
if handler != nil {
handler.(func(*ICECandidatePair))(pair)
}
}
// OnConnectionStateChange sets a handler that is fired when the ICE
// connection state changes.
func (t *ICETransport) OnConnectionStateChange(f func(ICETransportState)) {
t.onConnectionStateChangeHandler.Store(f)
}
func (t *ICETransport) onConnectionStateChange(state ICETransportState) {
handler := t.onConnectionStateChangeHandler.Load()
if handler != nil {
handler.(func(ICETransportState))(state)
}
}
// Role indicates the current role of the ICE transport.
func (t *ICETransport) Role() ICERole {
t.lock.RLock()
defer t.lock.RUnlock()
return t.role
}
// SetRemoteCandidates sets the sequence of candidates associated with the remote ICETransport.
func (t *ICETransport) SetRemoteCandidates(remoteCandidates []ICECandidate) error {
t.lock.RLock()
defer t.lock.RUnlock()
if err := t.ensureGatherer(); err != nil {
return err
}
agent := t.gatherer.getAgent()
if agent == nil {
return fmt.Errorf("%w: unable to set remote candidates", errICEAgentNotExist)
}
for _, c := range remoteCandidates {
i, err := c.toICE()
if err != nil {
return err
}
if err = agent.AddRemoteCandidate(i); err != nil {
return err
}
}
return nil
}
// AddRemoteCandidate adds a candidate associated with the remote ICETransport.
func (t *ICETransport) AddRemoteCandidate(remoteCandidate *ICECandidate) error {
t.lock.RLock()
defer t.lock.RUnlock()
var (
c ice.Candidate
err error
)
if err = t.ensureGatherer(); err != nil {
return err
}
if remoteCandidate != nil {
if c, err = remoteCandidate.toICE(); err != nil {
return err
}
}
agent := t.gatherer.getAgent()
if agent == nil {
return fmt.Errorf("%w: unable to add remote candidates", errICEAgentNotExist)
}
return agent.AddRemoteCandidate(c)
}
// State returns the current ice transport state.
func (t *ICETransport) State() ICETransportState {
if v := t.state.Load(); v != nil {
return v.(ICETransportState)
}
return ICETransportState(0)
}
func (t *ICETransport) setState(i ICETransportState) {
t.state.Store(i)
}
// NewEndpoint registers a new endpoint on the underlying mux.
func (t *ICETransport) NewEndpoint(f mux.MatchFunc) *mux.Endpoint {
t.lock.Lock()
defer t.lock.Unlock()
return t.mux.NewEndpoint(f)
}
func (t *ICETransport) ensureGatherer() error {
if t.gatherer == nil {
return errICEGathererNotStarted
} else if t.gatherer.getAgent() == nil {
if err := t.gatherer.createAgent(); err != nil {
return err
}
}
return nil
}
func (t *ICETransport) collectStats(collector *statsReportCollector) {
t.lock.Lock()
conn := t.conn
t.lock.Unlock()
collector.Collecting()
stats := TransportStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeTransport,
ID: "iceTransport",
}
if conn != nil {
stats.BytesSent = conn.BytesSent()
stats.BytesReceived = conn.BytesReceived()
}
collector.Collect(stats.ID, stats)
}
func (t *ICETransport) haveRemoteCredentialsChange(newUfrag, newPwd string) bool {
t.lock.Lock()
defer t.lock.Unlock()
agent := t.gatherer.getAgent()
if agent == nil {
return false
}
uFrag, uPwd, err := agent.GetRemoteUserCredentials()
if err != nil {
return false
}
return uFrag != newUfrag || uPwd != newPwd
}
func (t *ICETransport) setRemoteCredentials(newUfrag, newPwd string) error {
t.lock.Lock()
defer t.lock.Unlock()
agent := t.gatherer.getAgent()
if agent == nil {
return fmt.Errorf("%w: unable to SetRemoteCredentials", errICEAgentNotExist)
}
return agent.SetRemoteCredentials(newUfrag, newPwd)
}

View file

@ -0,0 +1,65 @@
package webrtc
import (
"encoding/json"
)
// ICETransportPolicy defines the ICE candidate policy surface the
// permitted candidates. Only these candidates are used for connectivity checks.
type ICETransportPolicy int
// ICEGatherPolicy is the ORTC equivalent of ICETransportPolicy
type ICEGatherPolicy = ICETransportPolicy
const (
// ICETransportPolicyAll indicates any type of candidate is used.
ICETransportPolicyAll ICETransportPolicy = iota
// ICETransportPolicyRelay indicates only media relay candidates such
// as candidates passing through a TURN server are used.
ICETransportPolicyRelay
)
// This is done this way because of a linter.
const (
iceTransportPolicyRelayStr = "relay"
iceTransportPolicyAllStr = "all"
)
// NewICETransportPolicy takes a string and converts it to ICETransportPolicy
func NewICETransportPolicy(raw string) ICETransportPolicy {
switch raw {
case iceTransportPolicyRelayStr:
return ICETransportPolicyRelay
case iceTransportPolicyAllStr:
return ICETransportPolicyAll
default:
return ICETransportPolicy(Unknown)
}
}
func (t ICETransportPolicy) String() string {
switch t {
case ICETransportPolicyRelay:
return iceTransportPolicyRelayStr
case ICETransportPolicyAll:
return iceTransportPolicyAllStr
default:
return ErrUnknownType.Error()
}
}
// UnmarshalJSON parses the JSON-encoded data and stores the result
func (t *ICETransportPolicy) UnmarshalJSON(b []byte) error {
var val string
if err := json.Unmarshal(b, &val); err != nil {
return err
}
*t = NewICETransportPolicy(val)
return nil
}
// MarshalJSON returns the JSON encoding
func (t ICETransportPolicy) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}

View file

@ -0,0 +1,107 @@
package webrtc
import "github.com/pion/ice/v2"
// ICETransportState represents the current state of the ICE transport.
type ICETransportState int
const (
// ICETransportStateNew indicates the ICETransport is waiting
// for remote candidates to be supplied.
ICETransportStateNew = iota + 1
// ICETransportStateChecking indicates the ICETransport has
// received at least one remote candidate, and a local and remote
// ICECandidateComplete dictionary was not added as the last candidate.
ICETransportStateChecking
// ICETransportStateConnected indicates the ICETransport has
// received a response to an outgoing connectivity check, or has
// received incoming DTLS/media after a successful response to an
// incoming connectivity check, but is still checking other candidate
// pairs to see if there is a better connection.
ICETransportStateConnected
// ICETransportStateCompleted indicates the ICETransport tested
// all appropriate candidate pairs and at least one functioning
// candidate pair has been found.
ICETransportStateCompleted
// ICETransportStateFailed indicates the ICETransport the last
// candidate was added and all appropriate candidate pairs have either
// failed connectivity checks or have lost consent.
ICETransportStateFailed
// ICETransportStateDisconnected indicates the ICETransport has received
// at least one local and remote candidate, but the final candidate was
// received yet and all appropriate candidate pairs thus far have been
// tested and failed.
ICETransportStateDisconnected
// ICETransportStateClosed indicates the ICETransport has shut down
// and is no longer responding to STUN requests.
ICETransportStateClosed
)
func (c ICETransportState) String() string {
switch c {
case ICETransportStateNew:
return "new"
case ICETransportStateChecking:
return "checking"
case ICETransportStateConnected:
return "connected"
case ICETransportStateCompleted:
return "completed"
case ICETransportStateFailed:
return "failed"
case ICETransportStateDisconnected:
return "disconnected"
case ICETransportStateClosed:
return "closed"
default:
return unknownStr
}
}
func newICETransportStateFromICE(i ice.ConnectionState) ICETransportState {
switch i {
case ice.ConnectionStateNew:
return ICETransportStateNew
case ice.ConnectionStateChecking:
return ICETransportStateChecking
case ice.ConnectionStateConnected:
return ICETransportStateConnected
case ice.ConnectionStateCompleted:
return ICETransportStateCompleted
case ice.ConnectionStateFailed:
return ICETransportStateFailed
case ice.ConnectionStateDisconnected:
return ICETransportStateDisconnected
case ice.ConnectionStateClosed:
return ICETransportStateClosed
default:
return ICETransportState(Unknown)
}
}
func (c ICETransportState) toICE() ice.ConnectionState {
switch c {
case ICETransportStateNew:
return ice.ConnectionStateNew
case ICETransportStateChecking:
return ice.ConnectionStateChecking
case ICETransportStateConnected:
return ice.ConnectionStateConnected
case ICETransportStateCompleted:
return ice.ConnectionStateCompleted
case ICETransportStateFailed:
return ice.ConnectionStateFailed
case ICETransportStateDisconnected:
return ice.ConnectionStateDisconnected
case ICETransportStateClosed:
return ice.ConnectionStateClosed
default:
return ice.ConnectionState(Unknown)
}
}

View file

@ -0,0 +1,85 @@
// +build !js
package webrtc
import (
"sync/atomic"
"github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/nack"
"github.com/pion/rtp"
)
// RegisterDefaultInterceptors will register some useful interceptors.
// If you want to customize which interceptors are loaded, you should copy the
// code from this method and remove unwanted interceptors.
func RegisterDefaultInterceptors(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error {
if err := ConfigureNack(mediaEngine, interceptorRegistry); err != nil {
return err
}
return nil
}
// ConfigureNack will setup everything necessary for handling generating/responding to nack messages.
func ConfigureNack(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error {
generator, err := nack.NewGeneratorInterceptor()
if err != nil {
return err
}
responder, err := nack.NewResponderInterceptor()
if err != nil {
return err
}
mediaEngine.RegisterFeedback(RTCPFeedback{Type: "nack"}, RTPCodecTypeVideo)
mediaEngine.RegisterFeedback(RTCPFeedback{Type: "nack", Parameter: "pli"}, RTPCodecTypeVideo)
interceptorRegistry.Add(responder)
interceptorRegistry.Add(generator)
return nil
}
type interceptorToTrackLocalWriter struct{ interceptor atomic.Value } // interceptor.RTPWriter }
func (i *interceptorToTrackLocalWriter) WriteRTP(header *rtp.Header, payload []byte) (int, error) {
if writer, ok := i.interceptor.Load().(interceptor.RTPWriter); ok && writer != nil {
return writer.Write(header, payload, interceptor.Attributes{})
}
return 0, nil
}
func (i *interceptorToTrackLocalWriter) Write(b []byte) (int, error) {
packet := &rtp.Packet{}
if err := packet.Unmarshal(b); err != nil {
return 0, err
}
return i.WriteRTP(&packet.Header, packet.Payload)
}
func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCodecCapability, webrtcHeaderExtensions []RTPHeaderExtensionParameter) interceptor.StreamInfo {
headerExtensions := make([]interceptor.RTPHeaderExtension, 0, len(webrtcHeaderExtensions))
for _, h := range webrtcHeaderExtensions {
headerExtensions = append(headerExtensions, interceptor.RTPHeaderExtension{ID: h.ID, URI: h.URI})
}
feedbacks := make([]interceptor.RTCPFeedback, 0, len(codec.RTCPFeedback))
for _, f := range codec.RTCPFeedback {
feedbacks = append(feedbacks, interceptor.RTCPFeedback{Type: f.Type, Parameter: f.Parameter})
}
return interceptor.StreamInfo{
ID: id,
Attributes: interceptor.Attributes{},
SSRC: uint32(ssrc),
PayloadType: uint8(payloadType),
RTPHeaderExtensions: headerExtensions,
MimeType: codec.MimeType,
ClockRate: codec.ClockRate,
Channels: codec.Channels,
SDPFmtpLine: codec.SDPFmtpLine,
RTCPFeedback: feedbacks,
}
}

View file

@ -0,0 +1,75 @@
package mux
import (
"errors"
"io"
"net"
"time"
"github.com/pion/ice/v2"
"github.com/pion/transport/packetio"
)
// Endpoint implements net.Conn. It is used to read muxed packets.
type Endpoint struct {
mux *Mux
buffer *packetio.Buffer
}
// Close unregisters the endpoint from the Mux
func (e *Endpoint) Close() (err error) {
err = e.close()
if err != nil {
return err
}
e.mux.RemoveEndpoint(e)
return nil
}
func (e *Endpoint) close() error {
return e.buffer.Close()
}
// Read reads a packet of len(p) bytes from the underlying conn
// that are matched by the associated MuxFunc
func (e *Endpoint) Read(p []byte) (int, error) {
return e.buffer.Read(p)
}
// Write writes len(p) bytes to the underlying conn
func (e *Endpoint) Write(p []byte) (int, error) {
n, err := e.mux.nextConn.Write(p)
if errors.Is(err, ice.ErrNoCandidatePairs) {
return 0, nil
} else if errors.Is(err, ice.ErrClosed) {
return 0, io.ErrClosedPipe
}
return n, err
}
// LocalAddr is a stub
func (e *Endpoint) LocalAddr() net.Addr {
return e.mux.nextConn.LocalAddr()
}
// RemoteAddr is a stub
func (e *Endpoint) RemoteAddr() net.Addr {
return e.mux.nextConn.RemoteAddr()
}
// SetDeadline is a stub
func (e *Endpoint) SetDeadline(t time.Time) error {
return nil
}
// SetReadDeadline is a stub
func (e *Endpoint) SetReadDeadline(t time.Time) error {
return nil
}
// SetWriteDeadline is a stub
func (e *Endpoint) SetWriteDeadline(t time.Time) error {
return nil
}

View file

@ -0,0 +1,145 @@
// Package mux multiplexes packets on a single socket (RFC7983)
package mux
import (
"net"
"sync"
"github.com/pion/logging"
"github.com/pion/transport/packetio"
)
// The maximum amount of data that can be buffered before returning errors.
const maxBufferSize = 1000 * 1000 // 1MB
// Config collects the arguments to mux.Mux construction into
// a single structure
type Config struct {
Conn net.Conn
BufferSize int
LoggerFactory logging.LoggerFactory
}
// Mux allows multiplexing
type Mux struct {
lock sync.RWMutex
nextConn net.Conn
endpoints map[*Endpoint]MatchFunc
bufferSize int
closedCh chan struct{}
log logging.LeveledLogger
}
// NewMux creates a new Mux
func NewMux(config Config) *Mux {
m := &Mux{
nextConn: config.Conn,
endpoints: make(map[*Endpoint]MatchFunc),
bufferSize: config.BufferSize,
closedCh: make(chan struct{}),
log: config.LoggerFactory.NewLogger("mux"),
}
go m.readLoop()
return m
}
// NewEndpoint creates a new Endpoint
func (m *Mux) NewEndpoint(f MatchFunc) *Endpoint {
e := &Endpoint{
mux: m,
buffer: packetio.NewBuffer(),
}
// Set a maximum size of the buffer in bytes.
// NOTE: We actually won't get anywhere close to this limit.
// SRTP will constantly read from the endpoint and drop packets if it's full.
e.buffer.SetLimitSize(maxBufferSize)
m.lock.Lock()
m.endpoints[e] = f
m.lock.Unlock()
return e
}
// RemoveEndpoint removes an endpoint from the Mux
func (m *Mux) RemoveEndpoint(e *Endpoint) {
m.lock.Lock()
defer m.lock.Unlock()
delete(m.endpoints, e)
}
// Close closes the Mux and all associated Endpoints.
func (m *Mux) Close() error {
m.lock.Lock()
for e := range m.endpoints {
err := e.close()
if err != nil {
return err
}
delete(m.endpoints, e)
}
m.lock.Unlock()
err := m.nextConn.Close()
if err != nil {
return err
}
// Wait for readLoop to end
<-m.closedCh
return nil
}
func (m *Mux) readLoop() {
defer func() {
close(m.closedCh)
}()
buf := make([]byte, m.bufferSize)
for {
n, err := m.nextConn.Read(buf)
if err != nil {
return
}
err = m.dispatch(buf[:n])
if err != nil {
return
}
}
}
func (m *Mux) dispatch(buf []byte) error {
var endpoint *Endpoint
m.lock.Lock()
for e, f := range m.endpoints {
if f(buf) {
endpoint = e
break
}
}
m.lock.Unlock()
if endpoint == nil {
if len(buf) > 0 {
m.log.Warnf("Warning: mux: no endpoint for packet starting with %d\n", buf[0])
} else {
m.log.Warnf("Warning: mux: no endpoint for zero length packet")
}
return nil
}
_, err := endpoint.buffer.Write(buf)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,101 @@
package mux
import (
"bytes"
"encoding/binary"
)
// MatchFunc allows custom logic for mapping packets to an Endpoint
type MatchFunc func([]byte) bool
// MatchAll always returns true
func MatchAll(b []byte) bool {
return true
}
// MatchNone always returns false
func MatchNone(b []byte) bool {
return false
}
// MatchRange is a MatchFunc that accepts packets with the first byte in [lower..upper]
func MatchRange(lower, upper byte) MatchFunc {
return func(buf []byte) bool {
if len(buf) < 1 {
return false
}
b := buf[0]
return b >= lower && b <= upper
}
}
// MatchFuncs as described in RFC7983
// https://tools.ietf.org/html/rfc7983
// +----------------+
// | [0..3] -+--> forward to STUN
// | |
// | [16..19] -+--> forward to ZRTP
// | |
// packet --> | [20..63] -+--> forward to DTLS
// | |
// | [64..79] -+--> forward to TURN Channel
// | |
// | [128..191] -+--> forward to RTP/RTCP
// +----------------+
// MatchSTUN is a MatchFunc that accepts packets with the first byte in [0..3]
// as defied in RFC7983
func MatchSTUN(b []byte) bool {
return MatchRange(0, 3)(b)
}
// MatchZRTP is a MatchFunc that accepts packets with the first byte in [16..19]
// as defied in RFC7983
func MatchZRTP(b []byte) bool {
return MatchRange(16, 19)(b)
}
// MatchDTLS is a MatchFunc that accepts packets with the first byte in [20..63]
// as defied in RFC7983
func MatchDTLS(b []byte) bool {
return MatchRange(20, 63)(b)
}
// MatchTURN is a MatchFunc that accepts packets with the first byte in [64..79]
// as defied in RFC7983
func MatchTURN(b []byte) bool {
return MatchRange(64, 79)(b)
}
// MatchSRTPOrSRTCP is a MatchFunc that accepts packets with the first byte in [128..191]
// as defied in RFC7983
func MatchSRTPOrSRTCP(b []byte) bool {
return MatchRange(128, 191)(b)
}
func isRTCP(buf []byte) bool {
// Not long enough to determine RTP/RTCP
if len(buf) < 4 {
return false
}
var rtcpPacketType uint8
r := bytes.NewReader([]byte{buf[1]})
if err := binary.Read(r, binary.BigEndian, &rtcpPacketType); err != nil {
return false
} else if rtcpPacketType >= 192 && rtcpPacketType <= 223 {
return true
}
return false
}
// MatchSRTP is a MatchFunc that only matches SRTP and not SRTCP
func MatchSRTP(buf []byte) bool {
return MatchSRTPOrSRTCP(buf) && !isRTCP(buf)
}
// MatchSRTCP is a MatchFunc that only matches SRTCP and not SRTP
func MatchSRTCP(buf []byte) bool {
return MatchSRTPOrSRTCP(buf) && isRTCP(buf)
}

View file

@ -0,0 +1,72 @@
// Package util provides auxiliary functions internally used in webrtc package
package util
import (
"errors"
"strings"
"github.com/pion/randutil"
)
const (
runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// Use global random generator to properly seed by crypto grade random.
var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals
// MathRandAlpha generates a mathmatical random alphabet sequence of the requested length.
func MathRandAlpha(n int) string {
return globalMathRandomGenerator.GenerateString(n, runesAlpha)
}
// RandUint32 generates a mathmatical random uint32.
func RandUint32() uint32 {
return globalMathRandomGenerator.Uint32()
}
// FlattenErrs flattens multiple errors into one
func FlattenErrs(errs []error) error {
errs2 := []error{}
for _, e := range errs {
if e != nil {
errs2 = append(errs2, e)
}
}
if len(errs2) == 0 {
return nil
}
return multiError(errs2)
}
type multiError []error
func (me multiError) Error() string {
var errstrings []string
for _, err := range me {
if err != nil {
errstrings = append(errstrings, err.Error())
}
}
if len(errstrings) == 0 {
return "multiError must contain multiple error but is empty"
}
return strings.Join(errstrings, "\n")
}
func (me multiError) Is(err error) bool {
for _, e := range me {
if errors.Is(e, err) {
return true
}
if me2, ok := e.(multiError); ok {
if me2.Is(err) {
return true
}
}
}
return false
}

View file

@ -0,0 +1,13 @@
// +build js,go1.14
package webrtc
import "syscall/js"
func jsValueIsUndefined(v js.Value) bool {
return v.IsUndefined()
}
func jsValueIsNull(v js.Value) bool {
return v.IsNull()
}

View file

@ -0,0 +1,13 @@
// +build js,!go1.14
package webrtc
import "syscall/js"
func jsValueIsUndefined(v js.Value) bool {
return v == js.Undefined()
}
func jsValueIsNull(v js.Value) bool {
return v == js.Null()
}

View file

@ -0,0 +1,168 @@
// +build js,wasm
package webrtc
import (
"fmt"
"syscall/js"
)
// awaitPromise accepts a js.Value representing a Promise. If the promise
// resolves, it returns (result, nil). If the promise rejects, it returns
// (js.Undefined, error). awaitPromise has a synchronous-like API but does not
// block the JavaScript event loop.
func awaitPromise(promise js.Value) (js.Value, error) {
resultsChan := make(chan js.Value)
errChan := make(chan js.Error)
thenFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
resultsChan <- args[0]
}()
return js.Undefined()
})
defer thenFunc.Release()
promise.Call("then", thenFunc)
catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
errChan <- js.Error{args[0]}
}()
return js.Undefined()
})
defer catchFunc.Release()
promise.Call("catch", catchFunc)
select {
case result := <-resultsChan:
return result, nil
case err := <-errChan:
return js.Undefined(), err
}
}
func valueToUint16Pointer(val js.Value) *uint16 {
if jsValueIsNull(val) || jsValueIsUndefined(val) {
return nil
}
convertedVal := uint16(val.Int())
return &convertedVal
}
func valueToStringPointer(val js.Value) *string {
if jsValueIsNull(val) || jsValueIsUndefined(val) {
return nil
}
stringVal := val.String()
return &stringVal
}
func stringToValueOrUndefined(val string) js.Value {
if val == "" {
return js.Undefined()
}
return js.ValueOf(val)
}
func uint8ToValueOrUndefined(val uint8) js.Value {
if val == 0 {
return js.Undefined()
}
return js.ValueOf(val)
}
func interfaceToValueOrUndefined(val interface{}) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(val)
}
func valueToStringOrZero(val js.Value) string {
if jsValueIsUndefined(val) || jsValueIsNull(val) {
return ""
}
return val.String()
}
func valueToUint8OrZero(val js.Value) uint8 {
if jsValueIsUndefined(val) || jsValueIsNull(val) {
return 0
}
return uint8(val.Int())
}
func valueToUint16OrZero(val js.Value) uint16 {
if jsValueIsNull(val) || jsValueIsUndefined(val) {
return 0
}
return uint16(val.Int())
}
func valueToUint32OrZero(val js.Value) uint32 {
if jsValueIsNull(val) || jsValueIsUndefined(val) {
return 0
}
return uint32(val.Int())
}
func valueToStrings(val js.Value) []string {
result := make([]string, val.Length())
for i := 0; i < val.Length(); i++ {
result[i] = val.Index(i).String()
}
return result
}
func stringPointerToValue(val *string) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func uint16PointerToValue(val *uint16) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func boolPointerToValue(val *bool) js.Value {
if val == nil {
return js.Undefined()
}
return js.ValueOf(*val)
}
func stringsToValue(strings []string) js.Value {
val := make([]interface{}, len(strings))
for i, s := range strings {
val[i] = s
}
return js.ValueOf(val)
}
func stringEnumToValueOrUndefined(s string) js.Value {
if s == "unknown" {
return js.Undefined()
}
return js.ValueOf(s)
}
// Converts the return value of recover() to an error.
func recoveryToError(e interface{}) error {
switch e := e.(type) {
case error:
return e
default:
return fmt.Errorf("recovered with non-error value: (%T) %s", e, e)
}
}
func uint8ArrayValueToBytes(val js.Value) []byte {
result := make([]byte, val.Length())
js.CopyBytesToGo(result, val)
return result
}

View file

@ -0,0 +1,512 @@
// +build !js
package webrtc
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/sdp/v3"
)
const (
// MimeTypeH264 H264 MIME type.
// Note: Matching should be case insensitive.
MimeTypeH264 = "video/h264"
// MimeTypeOpus Opus MIME type
// Note: Matching should be case insensitive.
MimeTypeOpus = "audio/opus"
// MimeTypeVP8 VP8 MIME type
// Note: Matching should be case insensitive.
MimeTypeVP8 = "video/vp8"
// MimeTypeVP9 VP9 MIME type
// Note: Matching should be case insensitive.
MimeTypeVP9 = "video/vp9"
// MimeTypeG722 G722 MIME type
// Note: Matching should be case insensitive.
MimeTypeG722 = "audio/G722"
// MimeTypePCMU PCMU MIME type
// Note: Matching should be case insensitive.
MimeTypePCMU = "audio/PCMU"
// MimeTypePCMA PCMA MIME type
// Note: Matching should be case insensitive.
MimeTypePCMA = "audio/PCMA"
)
type mediaEngineHeaderExtension struct {
uri string
isAudio, isVideo bool
// If set only Transceivers of this direction are allowed
allowedDirections []RTPTransceiverDirection
}
// A MediaEngine defines the codecs supported by a PeerConnection, and the
// configuration of those codecs. A MediaEngine must not be shared between
// PeerConnections.
type MediaEngine struct {
// If we have attempted to negotiate a codec type yet.
negotiatedVideo, negotiatedAudio bool
videoCodecs, audioCodecs []RTPCodecParameters
negotiatedVideoCodecs, negotiatedAudioCodecs []RTPCodecParameters
headerExtensions []mediaEngineHeaderExtension
negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension
}
// RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
// RegisterDefaultCodecs is not safe for concurrent use.
func (m *MediaEngine) RegisterDefaultCodecs() error {
// Default Pion Audio Codecs
for _, codec := range []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
PayloadType: 111,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeG722, 8000, 0, "", nil},
PayloadType: 9,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypePCMU, 8000, 0, "", nil},
PayloadType: 0,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypePCMA, 8000, 0, "", nil},
PayloadType: 8,
},
} {
if err := m.RegisterCodec(codec, RTPCodecTypeAudio); err != nil {
return err
}
}
// Default Pion Audio Header Extensions
for _, extension := range []string{
"urn:ietf:params:rtp-hdrext:sdes:mid",
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
} {
if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{extension}, RTPCodecTypeAudio); err != nil {
return err
}
}
videoRTCPFeedback := []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
for _, codec := range []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", videoRTCPFeedback},
PayloadType: 96,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil},
PayloadType: 97,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", videoRTCPFeedback},
PayloadType: 98,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil},
PayloadType: 99,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=1", videoRTCPFeedback},
PayloadType: 100,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=100", nil},
PayloadType: 101,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 102,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=102", nil},
PayloadType: 121,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 127,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil},
PayloadType: 120,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
PayloadType: 125,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=125", nil},
PayloadType: 107,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", videoRTCPFeedback},
PayloadType: 108,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=108", nil},
PayloadType: 109,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 127,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=127", nil},
PayloadType: 120,
},
{
RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback},
PayloadType: 123,
},
{
RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=123", nil},
PayloadType: 118,
},
{
RTPCodecCapability: RTPCodecCapability{"video/ulpfec", 90000, 0, "", nil},
PayloadType: 116,
},
} {
if err := m.RegisterCodec(codec, RTPCodecTypeVideo); err != nil {
return err
}
}
// Default Pion Video Header Extensions
for _, extension := range []string{
"urn:ietf:params:rtp-hdrext:sdes:mid",
"urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
"urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
} {
if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{extension}, RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
}
// RegisterCodec adds codec to the MediaEngine
// These are the list of codecs supported by this PeerConnection.
// RegisterCodec is not safe for concurrent use.
func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error {
codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
switch typ {
case RTPCodecTypeAudio:
m.audioCodecs = append(m.audioCodecs, codec)
case RTPCodecTypeVideo:
m.videoCodecs = append(m.videoCodecs, codec)
default:
return ErrUnknownType
}
return nil
}
// RegisterHeaderExtension adds a header extension to the MediaEngine
// To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete
func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType, allowedDirections ...RTPTransceiverDirection) error {
if m.negotiatedHeaderExtensions == nil {
m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
if len(allowedDirections) == 0 {
allowedDirections = []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendonly}
}
for _, direction := range allowedDirections {
if direction != RTPTransceiverDirectionRecvonly && direction != RTPTransceiverDirectionSendonly {
return ErrRegisterHeaderExtensionInvalidDirection
}
}
extensionIndex := -1
for i := range m.headerExtensions {
if extension.URI == m.headerExtensions[i].uri {
extensionIndex = i
}
}
if extensionIndex == -1 {
m.headerExtensions = append(m.headerExtensions, mediaEngineHeaderExtension{})
extensionIndex = len(m.headerExtensions) - 1
}
if typ == RTPCodecTypeAudio {
m.headerExtensions[extensionIndex].isAudio = true
} else if typ == RTPCodecTypeVideo {
m.headerExtensions[extensionIndex].isVideo = true
}
m.headerExtensions[extensionIndex].uri = extension.URI
m.headerExtensions[extensionIndex].allowedDirections = allowedDirections
return nil
}
// RegisterFeedback adds feedback mechanism to already registered codecs.
func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) {
switch typ {
case RTPCodecTypeVideo:
for i, v := range m.videoCodecs {
v.RTCPFeedback = append(v.RTCPFeedback, feedback)
m.videoCodecs[i] = v
}
case RTPCodecTypeAudio:
for i, v := range m.audioCodecs {
v.RTCPFeedback = append(v.RTCPFeedback, feedback)
m.audioCodecs[i] = v
}
}
}
// getHeaderExtensionID returns the negotiated ID for a header extension.
// If the Header Extension isn't enabled ok will be false
func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) {
if m.negotiatedHeaderExtensions == nil {
return 0, false, false
}
for id, h := range m.negotiatedHeaderExtensions {
if extension.URI == h.uri {
return id, h.isAudio, h.isVideo
}
}
return
}
func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) {
for _, codec := range m.negotiatedVideoCodecs {
if codec.PayloadType == payloadType {
return codec, RTPCodecTypeVideo, nil
}
}
for _, codec := range m.negotiatedAudioCodecs {
if codec.PayloadType == payloadType {
return codec, RTPCodecTypeAudio, nil
}
}
return RTPCodecParameters{}, 0, ErrCodecNotFound
}
func (m *MediaEngine) collectStats(collector *statsReportCollector) {
statsLoop := func(codecs []RTPCodecParameters) {
for _, codec := range codecs {
collector.Collecting()
stats := CodecStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeCodec,
ID: codec.statsID,
PayloadType: codec.PayloadType,
MimeType: codec.MimeType,
ClockRate: codec.ClockRate,
Channels: uint8(codec.Channels),
SDPFmtpLine: codec.SDPFmtpLine,
}
collector.Collect(stats.ID, stats)
}
}
statsLoop(m.videoCodecs)
statsLoop(m.audioCodecs)
}
// Look up a codec and enable if it exists
func (m *MediaEngine) updateCodecParameters(remoteCodec RTPCodecParameters, typ RTPCodecType) error {
codecs := m.videoCodecs
if typ == RTPCodecTypeAudio {
codecs = m.audioCodecs
}
pushCodec := func(codec RTPCodecParameters) error {
if typ == RTPCodecTypeAudio {
m.negotiatedAudioCodecs = append(m.negotiatedAudioCodecs, codec)
} else if typ == RTPCodecTypeVideo {
m.negotiatedVideoCodecs = append(m.negotiatedVideoCodecs, codec)
}
return nil
}
if strings.HasPrefix(remoteCodec.RTPCodecCapability.SDPFmtpLine, "apt=") {
payloadType, err := strconv.Atoi(strings.TrimPrefix(remoteCodec.RTPCodecCapability.SDPFmtpLine, "apt="))
if err != nil {
return err
}
if _, _, err = m.getCodecByPayload(PayloadType(payloadType)); err != nil {
return nil // not an error, we just ignore this codec we don't support
}
}
if _, err := codecParametersFuzzySearch(remoteCodec, codecs); err == nil {
return pushCodec(remoteCodec)
}
return nil
}
// Look up a header extension and enable if it exists
func (m *MediaEngine) updateHeaderExtension(id int, extension string, typ RTPCodecType) error {
if m.negotiatedHeaderExtensions == nil {
return nil
}
for _, localExtension := range m.headerExtensions {
if localExtension.uri == extension {
h := mediaEngineHeaderExtension{uri: extension, allowedDirections: localExtension.allowedDirections}
if existingValue, ok := m.negotiatedHeaderExtensions[id]; ok {
h = existingValue
}
switch {
case localExtension.isAudio && typ == RTPCodecTypeAudio:
h.isAudio = true
case localExtension.isVideo && typ == RTPCodecTypeVideo:
h.isVideo = true
}
m.negotiatedHeaderExtensions[id] = h
}
}
return nil
}
// Update the MediaEngine from a remote description
func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error {
for _, media := range desc.MediaDescriptions {
var typ RTPCodecType
switch {
case !m.negotiatedAudio && strings.EqualFold(media.MediaName.Media, "audio"):
m.negotiatedAudio = true
typ = RTPCodecTypeAudio
case !m.negotiatedVideo && strings.EqualFold(media.MediaName.Media, "video"):
m.negotiatedVideo = true
typ = RTPCodecTypeVideo
default:
continue
}
codecs, err := codecsFromMediaDescription(media)
if err != nil {
return err
}
for _, codec := range codecs {
if err = m.updateCodecParameters(codec, typ); err != nil {
return err
}
}
extensions, err := rtpExtensionsFromMediaDescription(media)
if err != nil {
return err
}
for extension, id := range extensions {
if err = m.updateHeaderExtension(id, extension, typ); err != nil {
return err
}
}
}
return nil
}
func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
if typ == RTPCodecTypeVideo {
if m.negotiatedVideo {
return m.negotiatedVideoCodecs
}
return m.videoCodecs
} else if typ == RTPCodecTypeAudio {
if m.negotiatedAudio {
return m.negotiatedAudioCodecs
}
return m.audioCodecs
}
return nil
}
func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters {
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
if m.negotiatedVideo && typ == RTPCodecTypeVideo ||
m.negotiatedAudio && typ == RTPCodecTypeAudio {
for id, e := range m.negotiatedHeaderExtensions {
if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
}
}
} else {
for id, e := range m.headerExtensions {
if haveRTPTransceiverDirectionIntersection(e.allowedDirections, directions) && (e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo) {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id + 1, URI: e.uri})
}
}
}
return RTPParameters{
HeaderExtensions: headerExtensions,
Codecs: m.getCodecsByKind(typ),
}
}
func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RTPParameters, error) {
codec, typ, err := m.getCodecByPayload(payloadType)
if err != nil {
return RTPParameters{}, err
}
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
for id, e := range m.negotiatedHeaderExtensions {
if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
headerExtensions = append(headerExtensions, RTPHeaderExtensionParameter{ID: id, URI: e.uri})
}
}
return RTPParameters{
HeaderExtensions: headerExtensions,
Codecs: []RTPCodecParameters{codec},
}, nil
}
func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
switch strings.ToLower(codec.MimeType) {
case strings.ToLower(MimeTypeH264):
return &codecs.H264Payloader{}, nil
case strings.ToLower(MimeTypeOpus):
return &codecs.OpusPayloader{}, nil
case strings.ToLower(MimeTypeVP8):
return &codecs.VP8Payloader{}, nil
case strings.ToLower(MimeTypeVP9):
return &codecs.VP9Payloader{}, nil
case strings.ToLower(MimeTypeG722):
return &codecs.G722Payloader{}, nil
case strings.ToLower(MimeTypePCMU), strings.ToLower(MimeTypePCMA):
return &codecs.G711Payloader{}, nil
default:
return nil, ErrNoPayloaderForCodec
}
}

View file

@ -0,0 +1,104 @@
package webrtc
import (
"fmt"
"github.com/pion/ice/v2"
)
func supportedNetworkTypes() []NetworkType {
return []NetworkType{
NetworkTypeUDP4,
NetworkTypeUDP6,
// NetworkTypeTCP4, // Not supported yet
// NetworkTypeTCP6, // Not supported yet
}
}
// NetworkType represents the type of network
type NetworkType int
const (
// NetworkTypeUDP4 indicates UDP over IPv4.
NetworkTypeUDP4 NetworkType = iota + 1
// NetworkTypeUDP6 indicates UDP over IPv6.
NetworkTypeUDP6
// NetworkTypeTCP4 indicates TCP over IPv4.
NetworkTypeTCP4
// NetworkTypeTCP6 indicates TCP over IPv6.
NetworkTypeTCP6
)
// This is done this way because of a linter.
const (
networkTypeUDP4Str = "udp4"
networkTypeUDP6Str = "udp6"
networkTypeTCP4Str = "tcp4"
networkTypeTCP6Str = "tcp6"
)
func (t NetworkType) String() string {
switch t {
case NetworkTypeUDP4:
return networkTypeUDP4Str
case NetworkTypeUDP6:
return networkTypeUDP6Str
case NetworkTypeTCP4:
return networkTypeTCP4Str
case NetworkTypeTCP6:
return networkTypeTCP6Str
default:
return ErrUnknownType.Error()
}
}
// Protocol returns udp or tcp
func (t NetworkType) Protocol() string {
switch t {
case NetworkTypeUDP4:
return "udp"
case NetworkTypeUDP6:
return "udp"
case NetworkTypeTCP4:
return "tcp"
case NetworkTypeTCP6:
return "tcp"
default:
return ErrUnknownType.Error()
}
}
// NewNetworkType allows create network type from string
// It will be useful for getting custom network types from external config.
func NewNetworkType(raw string) (NetworkType, error) {
switch raw {
case networkTypeUDP4Str:
return NetworkTypeUDP4, nil
case networkTypeUDP6Str:
return NetworkTypeUDP6, nil
case networkTypeTCP4Str:
return NetworkTypeTCP4, nil
case networkTypeTCP6Str:
return NetworkTypeTCP6, nil
default:
return NetworkType(Unknown), fmt.Errorf("%w: %s", errNetworkTypeUnknown, raw)
}
}
func getNetworkType(iceNetworkType ice.NetworkType) (NetworkType, error) {
switch iceNetworkType {
case ice.NetworkTypeUDP4:
return NetworkTypeUDP4, nil
case ice.NetworkTypeUDP6:
return NetworkTypeUDP6, nil
case ice.NetworkTypeTCP4:
return NetworkTypeTCP4, nil
case ice.NetworkTypeTCP6:
return NetworkTypeTCP6, nil
default:
return NetworkType(Unknown), fmt.Errorf("%w: %s", errNetworkTypeUnknown, iceNetworkType.String())
}
}

View file

@ -0,0 +1,15 @@
package webrtc
// OAuthCredential represents OAuth credential information which is used by
// the STUN/TURN client to connect to an ICE server as defined in
// https://tools.ietf.org/html/rfc7635. Note that the kid parameter is not
// located in OAuthCredential, but in ICEServer's username member.
type OAuthCredential struct {
// MACKey is a base64-url encoded format. It is used in STUN message
// integrity hash calculation.
MACKey string
// AccessToken is a base64-encoded format. This is an encrypted
// self-contained token that is opaque to the application.
AccessToken string
}

View file

@ -0,0 +1,26 @@
package webrtc
// OfferAnswerOptions is a base structure which describes the options that
// can be used to control the offer/answer creation process.
type OfferAnswerOptions struct {
// VoiceActivityDetection allows the application to provide information
// about whether it wishes voice detection feature to be enabled or disabled.
VoiceActivityDetection bool
}
// AnswerOptions structure describes the options used to control the answer
// creation process.
type AnswerOptions struct {
OfferAnswerOptions
}
// OfferOptions structure describes the options used to control the offer
// creation process
type OfferOptions struct {
OfferAnswerOptions
// ICERestart forces the underlying ice gathering process to be restarted.
// When this value is true, the generated description will have ICE
// credentials that are different from the current credentials
ICERestart bool
}

View file

@ -0,0 +1,87 @@
package webrtc
import (
"sync"
)
// Operation is a function
type operation func()
// Operations is a task executor.
type operations struct {
mu sync.Mutex
busy bool
ops []operation
}
func newOperations() *operations {
return &operations{}
}
// Enqueue adds a new action to be executed. If there are no actions scheduled,
// the execution will start immediately in a new goroutine.
func (o *operations) Enqueue(op operation) {
if op == nil {
return
}
o.mu.Lock()
running := o.busy
o.ops = append(o.ops, op)
o.busy = true
o.mu.Unlock()
if !running {
go o.start()
}
}
// IsEmpty checks if there are tasks in the queue
func (o *operations) IsEmpty() bool {
o.mu.Lock()
defer o.mu.Unlock()
return len(o.ops) == 0
}
// Done blocks until all currently enqueued operations are finished executing.
// For more complex synchronization, use Enqueue directly.
func (o *operations) Done() {
var wg sync.WaitGroup
wg.Add(1)
o.Enqueue(func() {
wg.Done()
})
wg.Wait()
}
func (o *operations) pop() func() {
o.mu.Lock()
defer o.mu.Unlock()
if len(o.ops) == 0 {
return nil
}
fn := o.ops[0]
o.ops = o.ops[1:]
return fn
}
func (o *operations) start() {
defer func() {
o.mu.Lock()
defer o.mu.Unlock()
if len(o.ops) == 0 {
o.busy = false
return
}
// either a new operation was enqueued while we
// were busy, or an operation panicked
go o.start()
}()
fn := o.pop()
for fn != nil {
fn()
fn = o.pop()
}
}

View file

@ -0,0 +1,11 @@
{
"name": "webrtc",
"repository": "git@github.com:pion/webrtc.git",
"private": true,
"devDependencies": {
"wrtc": "0.4.7"
},
"dependencies": {
"request": "2.88.2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,676 @@
// +build js,wasm
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
package webrtc
import (
"syscall/js"
"github.com/pion/ice/v2"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
// PeerConnection represents a WebRTC connection that establishes a
// peer-to-peer communications with another PeerConnection instance in a
// browser, or to another endpoint implementing the required protocols.
type PeerConnection struct {
// Pointer to the underlying JavaScript RTCPeerConnection object.
underlying js.Value
// Keep track of handlers/callbacks so we can call Release as required by the
// syscall/js API. Initially nil.
onSignalingStateChangeHandler *js.Func
onDataChannelHandler *js.Func
onNegotiationNeededHandler *js.Func
onConnectionStateChangeHandler *js.Func
onICEConnectionStateChangeHandler *js.Func
onICECandidateHandler *js.Func
onICEGatheringStateChangeHandler *js.Func
// Used by GatheringCompletePromise
onGatherCompleteHandler func()
// A reference to the associated API state used by this connection
api *API
}
// NewPeerConnection creates a peerconnection.
func NewPeerConnection(configuration Configuration) (*PeerConnection, error) {
api := NewAPI()
return api.NewPeerConnection(configuration)
}
// NewPeerConnection creates a new PeerConnection with the provided configuration against the received API object
func (api *API) NewPeerConnection(configuration Configuration) (_ *PeerConnection, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
configMap := configurationToValue(configuration)
underlying := js.Global().Get("window").Get("RTCPeerConnection").New(configMap)
return &PeerConnection{
underlying: underlying,
api: api,
}, nil
}
func (pc *PeerConnection) JSValue() js.Value {
return pc.underlying
}
// OnSignalingStateChange sets an event handler which is invoked when the
// peer connection's signaling state changes
func (pc *PeerConnection) OnSignalingStateChange(f func(SignalingState)) {
if pc.onSignalingStateChangeHandler != nil {
oldHandler := pc.onSignalingStateChangeHandler
defer oldHandler.Release()
}
onSignalingStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
state := newSignalingState(args[0].String())
go f(state)
return js.Undefined()
})
pc.onSignalingStateChangeHandler = &onSignalingStateChangeHandler
pc.underlying.Set("onsignalingstatechange", onSignalingStateChangeHandler)
}
// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func (pc *PeerConnection) OnDataChannel(f func(*DataChannel)) {
if pc.onDataChannelHandler != nil {
oldHandler := pc.onDataChannelHandler
defer oldHandler.Release()
}
onDataChannelHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// pion/webrtc/projects/15
// This reference to the underlying DataChannel doesn't know
// about any other references to the same DataChannel. This might result in
// memory leaks where we don't clean up handler functions. Could possibly fix
// by keeping a mutex-protected list of all DataChannel references as a
// property of this PeerConnection, but at the cost of additional overhead.
dataChannel := &DataChannel{
underlying: args[0].Get("channel"),
api: pc.api,
}
go f(dataChannel)
return js.Undefined()
})
pc.onDataChannelHandler = &onDataChannelHandler
pc.underlying.Set("ondatachannel", onDataChannelHandler)
}
// OnNegotiationNeeded sets an event handler which is invoked when
// a change has occurred which requires session negotiation
func (pc *PeerConnection) OnNegotiationNeeded(f func()) {
if pc.onNegotiationNeededHandler != nil {
oldHandler := pc.onNegotiationNeededHandler
defer oldHandler.Release()
}
onNegotiationNeededHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
pc.onNegotiationNeededHandler = &onNegotiationNeededHandler
pc.underlying.Set("onnegotiationneeded", onNegotiationNeededHandler)
}
// OnICEConnectionStateChange sets an event handler which is called
// when an ICE connection state is changed.
func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState)) {
if pc.onICEConnectionStateChangeHandler != nil {
oldHandler := pc.onICEConnectionStateChangeHandler
defer oldHandler.Release()
}
onICEConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
connectionState := NewICEConnectionState(pc.underlying.Get("iceConnectionState").String())
go f(connectionState)
return js.Undefined()
})
pc.onICEConnectionStateChangeHandler = &onICEConnectionStateChangeHandler
pc.underlying.Set("oniceconnectionstatechange", onICEConnectionStateChangeHandler)
}
// OnConnectionStateChange sets an event handler which is called
// when an PeerConnectionState is changed.
func (pc *PeerConnection) OnConnectionStateChange(f func(PeerConnectionState)) {
if pc.onConnectionStateChangeHandler != nil {
oldHandler := pc.onConnectionStateChangeHandler
defer oldHandler.Release()
}
onConnectionStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
connectionState := newPeerConnectionState(pc.underlying.Get("connectionState").String())
go f(connectionState)
return js.Undefined()
})
pc.onConnectionStateChangeHandler = &onConnectionStateChangeHandler
pc.underlying.Set("onconnectionstatechange", onConnectionStateChangeHandler)
}
func (pc *PeerConnection) checkConfiguration(configuration Configuration) error {
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2)
if pc.ConnectionState() == PeerConnectionStateClosed {
return &rtcerr.InvalidStateError{Err: ErrConnectionClosed}
}
existingConfig := pc.GetConfiguration()
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #3)
if configuration.PeerIdentity != "" {
if configuration.PeerIdentity != existingConfig.PeerIdentity {
return &rtcerr.InvalidModificationError{Err: ErrModifyingPeerIdentity}
}
}
// https://github.com/pion/webrtc/issues/513
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #4)
// if len(configuration.Certificates) > 0 {
// if len(configuration.Certificates) != len(existingConfiguration.Certificates) {
// return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
// }
// for i, certificate := range configuration.Certificates {
// if !pc.configuration.Certificates[i].Equals(certificate) {
// return &rtcerr.InvalidModificationError{Err: ErrModifyingCertificates}
// }
// }
// pc.configuration.Certificates = configuration.Certificates
// }
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #5)
if configuration.BundlePolicy != BundlePolicy(Unknown) {
if configuration.BundlePolicy != existingConfig.BundlePolicy {
return &rtcerr.InvalidModificationError{Err: ErrModifyingBundlePolicy}
}
}
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #6)
if configuration.RTCPMuxPolicy != RTCPMuxPolicy(Unknown) {
if configuration.RTCPMuxPolicy != existingConfig.RTCPMuxPolicy {
return &rtcerr.InvalidModificationError{Err: ErrModifyingRTCPMuxPolicy}
}
}
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #7)
if configuration.ICECandidatePoolSize != 0 {
if configuration.ICECandidatePoolSize != existingConfig.ICECandidatePoolSize &&
pc.LocalDescription() != nil {
return &rtcerr.InvalidModificationError{Err: ErrModifyingICECandidatePoolSize}
}
}
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11)
if len(configuration.ICEServers) > 0 {
// https://www.w3.org/TR/webrtc/#set-the-configuration (step #11.3)
for _, server := range configuration.ICEServers {
if _, err := server.validate(); err != nil {
return err
}
}
}
return nil
}
// SetConfiguration updates the configuration of this PeerConnection object.
func (pc *PeerConnection) SetConfiguration(configuration Configuration) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
if err := pc.checkConfiguration(configuration); err != nil {
return err
}
configMap := configurationToValue(configuration)
pc.underlying.Call("setConfiguration", configMap)
return nil
}
// GetConfiguration returns a Configuration object representing the current
// configuration of this PeerConnection object. The returned object is a
// copy and direct mutation on it will not take affect until SetConfiguration
// has been called with Configuration passed as its only argument.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-getconfiguration
func (pc *PeerConnection) GetConfiguration() Configuration {
return valueToConfiguration(pc.underlying.Call("getConfiguration"))
}
// CreateOffer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateOffer(options *OfferOptions) (_ SessionDescription, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("createOffer", offerOptionsToValue(options))
desc, err := awaitPromise(promise)
if err != nil {
return SessionDescription{}, err
}
return *valueToSessionDescription(desc), nil
}
// CreateAnswer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (_ SessionDescription, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("createAnswer", answerOptionsToValue(options))
desc, err := awaitPromise(promise)
if err != nil {
return SessionDescription{}, err
}
return *valueToSessionDescription(desc), nil
}
// SetLocalDescription sets the SessionDescription of the local peer
func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("setLocalDescription", sessionDescriptionToValue(&desc))
_, err = awaitPromise(promise)
return err
}
// LocalDescription returns PendingLocalDescription if it is not null and
// otherwise it returns CurrentLocalDescription. This property is used to
// determine if setLocalDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-localdescription
func (pc *PeerConnection) LocalDescription() *SessionDescription {
return valueToSessionDescription(pc.underlying.Get("localDescription"))
}
// SetRemoteDescription sets the SessionDescription of the remote peer
func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("setRemoteDescription", sessionDescriptionToValue(&desc))
_, err = awaitPromise(promise)
return err
}
// RemoteDescription returns PendingRemoteDescription if it is not null and
// otherwise it returns CurrentRemoteDescription. This property is used to
// determine if setRemoteDescription has already been called.
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-remotedescription
func (pc *PeerConnection) RemoteDescription() *SessionDescription {
return valueToSessionDescription(pc.underlying.Get("remoteDescription"))
}
// AddICECandidate accepts an ICE candidate string and adds it
// to the existing set of candidates
func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
promise := pc.underlying.Call("addIceCandidate", iceCandidateInitToValue(candidate))
_, err = awaitPromise(promise)
return err
}
// ICEConnectionState returns the ICE connection state of the
// PeerConnection instance.
func (pc *PeerConnection) ICEConnectionState() ICEConnectionState {
return NewICEConnectionState(pc.underlying.Get("iceConnectionState").String())
}
// OnICECandidate sets an event handler which is invoked when a new ICE
// candidate is found.
func (pc *PeerConnection) OnICECandidate(f func(candidate *ICECandidate)) {
if pc.onICECandidateHandler != nil {
oldHandler := pc.onICECandidateHandler
defer oldHandler.Release()
}
onICECandidateHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
candidate := valueToICECandidate(args[0].Get("candidate"))
if candidate == nil && pc.onGatherCompleteHandler != nil {
go pc.onGatherCompleteHandler()
}
go f(candidate)
return js.Undefined()
})
pc.onICECandidateHandler = &onICECandidateHandler
pc.underlying.Set("onicecandidate", onICECandidateHandler)
}
// OnICEGatheringStateChange sets an event handler which is invoked when the
// ICE candidate gathering state has changed.
func (pc *PeerConnection) OnICEGatheringStateChange(f func()) {
if pc.onICEGatheringStateChangeHandler != nil {
oldHandler := pc.onICEGatheringStateChangeHandler
defer oldHandler.Release()
}
onICEGatheringStateChangeHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go f()
return js.Undefined()
})
pc.onICEGatheringStateChangeHandler = &onICEGatheringStateChangeHandler
pc.underlying.Set("onicegatheringstatechange", onICEGatheringStateChangeHandler)
}
// CreateDataChannel creates a new DataChannel object with the given label
// and optional DataChannelInit used to configure properties of the
// underlying channel such as data reliability.
func (pc *PeerConnection) CreateDataChannel(label string, options *DataChannelInit) (_ *DataChannel, err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
channel := pc.underlying.Call("createDataChannel", label, dataChannelInitToValue(options))
return &DataChannel{
underlying: channel,
api: pc.api,
}, nil
}
// SetIdentityProvider is used to configure an identity provider to generate identity assertions
func (pc *PeerConnection) SetIdentityProvider(provider string) (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
pc.underlying.Call("setIdentityProvider", provider)
return nil
}
// Close ends the PeerConnection
func (pc *PeerConnection) Close() (err error) {
defer func() {
if e := recover(); e != nil {
err = recoveryToError(e)
}
}()
pc.underlying.Call("close")
// Release any handlers as required by the syscall/js API.
if pc.onSignalingStateChangeHandler != nil {
pc.onSignalingStateChangeHandler.Release()
}
if pc.onDataChannelHandler != nil {
pc.onDataChannelHandler.Release()
}
if pc.onNegotiationNeededHandler != nil {
pc.onNegotiationNeededHandler.Release()
}
if pc.onConnectionStateChangeHandler != nil {
pc.onConnectionStateChangeHandler.Release()
}
if pc.onICEConnectionStateChangeHandler != nil {
pc.onICEConnectionStateChangeHandler.Release()
}
if pc.onICECandidateHandler != nil {
pc.onICECandidateHandler.Release()
}
if pc.onICEGatheringStateChangeHandler != nil {
pc.onICEGatheringStateChangeHandler.Release()
}
return nil
}
// CurrentLocalDescription represents the local description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any local candidates that have been generated
// by the ICEAgent since the offer or answer was created.
func (pc *PeerConnection) CurrentLocalDescription() *SessionDescription {
desc := pc.underlying.Get("currentLocalDescription")
return valueToSessionDescription(desc)
}
// PendingLocalDescription represents a local description that is in the
// process of being negotiated plus any local candidates that have been
// generated by the ICEAgent since the offer or answer was created. If the
// PeerConnection is in the stable state, the value is null.
func (pc *PeerConnection) PendingLocalDescription() *SessionDescription {
desc := pc.underlying.Get("pendingLocalDescription")
return valueToSessionDescription(desc)
}
// CurrentRemoteDescription represents the last remote description that was
// successfully negotiated the last time the PeerConnection transitioned
// into the stable state plus any remote candidates that have been supplied
// via AddICECandidate() since the offer or answer was created.
func (pc *PeerConnection) CurrentRemoteDescription() *SessionDescription {
desc := pc.underlying.Get("currentRemoteDescription")
return valueToSessionDescription(desc)
}
// PendingRemoteDescription represents a remote description that is in the
// process of being negotiated, complete with any remote candidates that
// have been supplied via AddICECandidate() since the offer or answer was
// created. If the PeerConnection is in the stable state, the value is
// null.
func (pc *PeerConnection) PendingRemoteDescription() *SessionDescription {
desc := pc.underlying.Get("pendingRemoteDescription")
return valueToSessionDescription(desc)
}
// SignalingState returns the signaling state of the PeerConnection instance.
func (pc *PeerConnection) SignalingState() SignalingState {
rawState := pc.underlying.Get("signalingState").String()
return newSignalingState(rawState)
}
// ICEGatheringState attribute the ICE gathering state of the PeerConnection
// instance.
func (pc *PeerConnection) ICEGatheringState() ICEGatheringState {
rawState := pc.underlying.Get("iceGatheringState").String()
return NewICEGatheringState(rawState)
}
// ConnectionState attribute the connection state of the PeerConnection
// instance.
func (pc *PeerConnection) ConnectionState() PeerConnectionState {
rawState := pc.underlying.Get("connectionState").String()
return newPeerConnectionState(rawState)
}
func (pc *PeerConnection) setGatherCompleteHandler(handler func()) {
pc.onGatherCompleteHandler = handler
// If no onIceCandidate handler has been set provide an empty one
// otherwise our onGatherCompleteHandler will not be executed
if pc.onICECandidateHandler == nil {
pc.OnICECandidate(func(i *ICECandidate) {})
}
}
// Converts a Configuration to js.Value so it can be passed
// through to the JavaScript WebRTC API. Any zero values are converted to
// js.Undefined(), which will result in the default value being used.
func configurationToValue(configuration Configuration) js.Value {
return js.ValueOf(map[string]interface{}{
"iceServers": iceServersToValue(configuration.ICEServers),
"iceTransportPolicy": stringEnumToValueOrUndefined(configuration.ICETransportPolicy.String()),
"bundlePolicy": stringEnumToValueOrUndefined(configuration.BundlePolicy.String()),
"rtcpMuxPolicy": stringEnumToValueOrUndefined(configuration.RTCPMuxPolicy.String()),
"peerIdentity": stringToValueOrUndefined(configuration.PeerIdentity),
"iceCandidatePoolSize": uint8ToValueOrUndefined(configuration.ICECandidatePoolSize),
// Note: Certificates are not currently supported.
// "certificates": configuration.Certificates,
})
}
func iceServersToValue(iceServers []ICEServer) js.Value {
if len(iceServers) == 0 {
return js.Undefined()
}
maps := make([]interface{}, len(iceServers))
for i, server := range iceServers {
maps[i] = iceServerToValue(server)
}
return js.ValueOf(maps)
}
func iceServerToValue(server ICEServer) js.Value {
return js.ValueOf(map[string]interface{}{
"urls": stringsToValue(server.URLs), // required
"username": stringToValueOrUndefined(server.Username),
// Note: credential and credentialType are not currently supported.
// "credential": interfaceToValueOrUndefined(server.Credential),
// "credentialType": stringEnumToValueOrUndefined(server.CredentialType.String()),
})
}
func valueToConfiguration(configValue js.Value) Configuration {
if jsValueIsNull(configValue) || jsValueIsUndefined(configValue) {
return Configuration{}
}
return Configuration{
ICEServers: valueToICEServers(configValue.Get("iceServers")),
ICETransportPolicy: NewICETransportPolicy(valueToStringOrZero(configValue.Get("iceTransportPolicy"))),
BundlePolicy: newBundlePolicy(valueToStringOrZero(configValue.Get("bundlePolicy"))),
RTCPMuxPolicy: newRTCPMuxPolicy(valueToStringOrZero(configValue.Get("rtcpMuxPolicy"))),
PeerIdentity: valueToStringOrZero(configValue.Get("peerIdentity")),
ICECandidatePoolSize: valueToUint8OrZero(configValue.Get("iceCandidatePoolSize")),
// Note: Certificates are not supported.
// Certificates []Certificate
}
}
func valueToICEServers(iceServersValue js.Value) []ICEServer {
if jsValueIsNull(iceServersValue) || jsValueIsUndefined(iceServersValue) {
return nil
}
iceServers := make([]ICEServer, iceServersValue.Length())
for i := 0; i < iceServersValue.Length(); i++ {
iceServers[i] = valueToICEServer(iceServersValue.Index(i))
}
return iceServers
}
func valueToICEServer(iceServerValue js.Value) ICEServer {
return ICEServer{
URLs: valueToStrings(iceServerValue.Get("urls")), // required
Username: valueToStringOrZero(iceServerValue.Get("username")),
// Note: Credential and CredentialType are not currently supported.
// Credential: iceServerValue.Get("credential"),
// CredentialType: newICECredentialType(valueToStringOrZero(iceServerValue.Get("credentialType"))),
}
}
func valueToICECandidate(val js.Value) *ICECandidate {
if jsValueIsNull(val) || jsValueIsUndefined(val) {
return nil
}
if jsValueIsUndefined(val.Get("protocol")) && !jsValueIsUndefined(val.Get("candidate")) {
// Missing some fields, assume it's Firefox and parse SDP candidate.
c, err := ice.UnmarshalCandidate(val.Get("candidate").String())
if err != nil {
return nil
}
iceCandidate, err := newICECandidateFromICE(c)
if err != nil {
return nil
}
return &iceCandidate
}
protocol, _ := NewICEProtocol(val.Get("protocol").String())
candidateType, _ := NewICECandidateType(val.Get("type").String())
return &ICECandidate{
Foundation: val.Get("foundation").String(),
Priority: valueToUint32OrZero(val.Get("priority")),
Address: val.Get("address").String(),
Protocol: protocol,
Port: valueToUint16OrZero(val.Get("port")),
Typ: candidateType,
Component: stringToComponentIDOrZero(val.Get("component").String()),
RelatedAddress: val.Get("relatedAddress").String(),
RelatedPort: valueToUint16OrZero(val.Get("relatedPort")),
}
}
func stringToComponentIDOrZero(val string) uint16 {
// See: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceComponent
switch val {
case "rtp":
return 1
case "rtcp":
return 2
}
return 0
}
func sessionDescriptionToValue(desc *SessionDescription) js.Value {
if desc == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"type": desc.Type.String(),
"sdp": desc.SDP,
})
}
func valueToSessionDescription(descValue js.Value) *SessionDescription {
if jsValueIsNull(descValue) || jsValueIsUndefined(descValue) {
return nil
}
return &SessionDescription{
Type: NewSDPType(descValue.Get("type").String()),
SDP: descValue.Get("sdp").String(),
}
}
func offerOptionsToValue(offerOptions *OfferOptions) js.Value {
if offerOptions == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"iceRestart": offerOptions.ICERestart,
"voiceActivityDetection": offerOptions.VoiceActivityDetection,
})
}
func answerOptionsToValue(answerOptions *AnswerOptions) js.Value {
if answerOptions == nil {
return js.Undefined()
}
return js.ValueOf(map[string]interface{}{
"voiceActivityDetection": answerOptions.VoiceActivityDetection,
})
}
func iceCandidateInitToValue(candidate ICECandidateInit) js.Value {
return js.ValueOf(map[string]interface{}{
"candidate": candidate.Candidate,
"sdpMid": stringPointerToValue(candidate.SDPMid),
"sdpMLineIndex": uint16PointerToValue(candidate.SDPMLineIndex),
"usernameFragment": stringPointerToValue(candidate.UsernameFragment),
})
}
func dataChannelInitToValue(options *DataChannelInit) js.Value {
if options == nil {
return js.Undefined()
}
maxPacketLifeTime := uint16PointerToValue(options.MaxPacketLifeTime)
return js.ValueOf(map[string]interface{}{
"ordered": boolPointerToValue(options.Ordered),
"maxPacketLifeTime": maxPacketLifeTime,
// See https://bugs.chromium.org/p/chromium/issues/detail?id=696681
// Chrome calls this "maxRetransmitTime"
"maxRetransmitTime": maxPacketLifeTime,
"maxRetransmits": uint16PointerToValue(options.MaxRetransmits),
"protocol": stringPointerToValue(options.Protocol),
"negotiated": boolPointerToValue(options.Negotiated),
"id": uint16PointerToValue(options.ID),
})
}

View file

@ -0,0 +1,94 @@
package webrtc
// PeerConnectionState indicates the state of the PeerConnection.
type PeerConnectionState int
const (
// PeerConnectionStateNew indicates that any of the ICETransports or
// DTLSTransports are in the "new" state and none of the transports are
// in the "connecting", "checking", "failed" or "disconnected" state, or
// all transports are in the "closed" state, or there are no transports.
PeerConnectionStateNew PeerConnectionState = iota + 1
// PeerConnectionStateConnecting indicates that any of the
// ICETransports or DTLSTransports are in the "connecting" or
// "checking" state and none of them is in the "failed" state.
PeerConnectionStateConnecting
// PeerConnectionStateConnected indicates that all ICETransports and
// DTLSTransports are in the "connected", "completed" or "closed" state
// and at least one of them is in the "connected" or "completed" state.
PeerConnectionStateConnected
// PeerConnectionStateDisconnected indicates that any of the
// ICETransports or DTLSTransports are in the "disconnected" state
// and none of them are in the "failed" or "connecting" or "checking" state.
PeerConnectionStateDisconnected
// PeerConnectionStateFailed indicates that any of the ICETransports
// or DTLSTransports are in a "failed" state.
PeerConnectionStateFailed
// PeerConnectionStateClosed indicates the peer connection is closed
// and the isClosed member variable of PeerConnection is true.
PeerConnectionStateClosed
)
// This is done this way because of a linter.
const (
peerConnectionStateNewStr = "new"
peerConnectionStateConnectingStr = "connecting"
peerConnectionStateConnectedStr = "connected"
peerConnectionStateDisconnectedStr = "disconnected"
peerConnectionStateFailedStr = "failed"
peerConnectionStateClosedStr = "closed"
)
func newPeerConnectionState(raw string) PeerConnectionState {
switch raw {
case peerConnectionStateNewStr:
return PeerConnectionStateNew
case peerConnectionStateConnectingStr:
return PeerConnectionStateConnecting
case peerConnectionStateConnectedStr:
return PeerConnectionStateConnected
case peerConnectionStateDisconnectedStr:
return PeerConnectionStateDisconnected
case peerConnectionStateFailedStr:
return PeerConnectionStateFailed
case peerConnectionStateClosedStr:
return PeerConnectionStateClosed
default:
return PeerConnectionState(Unknown)
}
}
func (t PeerConnectionState) String() string {
switch t {
case PeerConnectionStateNew:
return peerConnectionStateNewStr
case PeerConnectionStateConnecting:
return peerConnectionStateConnectingStr
case PeerConnectionStateConnected:
return peerConnectionStateConnectedStr
case PeerConnectionStateDisconnected:
return peerConnectionStateDisconnectedStr
case PeerConnectionStateFailed:
return peerConnectionStateFailedStr
case PeerConnectionStateClosed:
return peerConnectionStateClosedStr
default:
return ErrUnknownType.Error()
}
}
type negotiationNeededState int
const (
// NegotiationNeededStateEmpty not running and queue is empty
negotiationNeededStateEmpty = iota
// NegotiationNeededStateEmpty running and queue is empty
negotiationNeededStateRun
// NegotiationNeededStateEmpty running and queue
negotiationNeededStateQueue
)

View file

@ -0,0 +1,191 @@
// Package h264reader implements a H264 Annex-B Reader
package h264reader
import (
"bytes"
"errors"
"io"
)
// H264Reader reads data from stream and constructs h264 nal units
type H264Reader struct {
stream io.Reader
nalBuffer []byte
countOfConsecutiveZeroBytes int
nalPrefixParsed bool
readBuffer []byte
}
var (
errNilReader = errors.New("stream is nil")
errDataIsNotH264Stream = errors.New("data is not a H264 bitstream")
)
// NewReader creates new H264Reader
func NewReader(in io.Reader) (*H264Reader, error) {
if in == nil {
return nil, errNilReader
}
reader := &H264Reader{
stream: in,
nalBuffer: make([]byte, 0),
nalPrefixParsed: false,
readBuffer: make([]byte, 0),
}
return reader, nil
}
// NAL H.264 Network Abstraction Layer
type NAL struct {
PictureOrderCount uint32
// NAL header
ForbiddenZeroBit bool
RefIdc uint8
UnitType NalUnitType
Data []byte // header byte + rbsp
}
func (reader *H264Reader) read(numToRead int) (data []byte) {
for len(reader.readBuffer) < numToRead {
buf := make([]byte, 4096)
n, err := reader.stream.Read(buf)
if n == 0 || err != nil {
break
}
buf = buf[0:n]
reader.readBuffer = append(reader.readBuffer, buf...)
}
var numShouldRead int
if numToRead <= len(reader.readBuffer) {
numShouldRead = numToRead
} else {
numShouldRead = len(reader.readBuffer)
}
data = reader.readBuffer[0:numShouldRead]
reader.readBuffer = reader.readBuffer[numShouldRead:]
return data
}
func (reader *H264Reader) bitStreamStartsWithH264Prefix() (prefixLength int, e error) {
nalPrefix3Bytes := []byte{0, 0, 1}
nalPrefix4Bytes := []byte{0, 0, 0, 1}
prefixBuffer := reader.read(4)
n := len(prefixBuffer)
if n == 0 {
return 0, io.EOF
}
if n < 3 {
return 0, errDataIsNotH264Stream
}
nalPrefix3BytesFound := bytes.Equal(nalPrefix3Bytes, prefixBuffer[:3])
if n == 3 {
if nalPrefix3BytesFound {
return 0, io.EOF
}
return 0, errDataIsNotH264Stream
}
// n == 4
if nalPrefix3BytesFound {
reader.nalBuffer = append(reader.nalBuffer, prefixBuffer[3])
return 3, nil
}
nalPrefix4BytesFound := bytes.Equal(nalPrefix4Bytes, prefixBuffer)
if nalPrefix4BytesFound {
return 4, nil
}
return 0, errDataIsNotH264Stream
}
// NextNAL reads from stream and returns then next NAL,
// and an error if there is incomplete frame data.
// Returns all nil values when no more NALs are available.
func (reader *H264Reader) NextNAL() (*NAL, error) {
if !reader.nalPrefixParsed {
_, err := reader.bitStreamStartsWithH264Prefix()
if err != nil {
return nil, err
}
reader.nalPrefixParsed = true
}
for {
buffer := reader.read(1)
n := len(buffer)
if n != 1 {
break
}
readByte := buffer[0]
nalFound := reader.processByte(readByte)
if nalFound {
nal := newNal(reader.nalBuffer)
nal.parseHeader()
if nal.UnitType == NalUnitTypeSEI {
reader.nalBuffer = nil
continue
} else {
break
}
}
reader.nalBuffer = append(reader.nalBuffer, readByte)
}
if len(reader.nalBuffer) == 0 {
return nil, io.EOF
}
nal := newNal(reader.nalBuffer)
reader.nalBuffer = nil
nal.parseHeader()
return nal, nil
}
func (reader *H264Reader) processByte(readByte byte) (nalFound bool) {
nalFound = false
switch readByte {
case 0:
reader.countOfConsecutiveZeroBytes++
case 1:
if reader.countOfConsecutiveZeroBytes >= 2 {
countOfConsecutiveZeroBytesInPrefix := 2
if reader.countOfConsecutiveZeroBytes > 2 {
countOfConsecutiveZeroBytesInPrefix = 3
}
nalUnitLength := len(reader.nalBuffer) - countOfConsecutiveZeroBytesInPrefix
reader.nalBuffer = reader.nalBuffer[0:nalUnitLength]
nalFound = true
} else {
reader.countOfConsecutiveZeroBytes = 0
}
default:
reader.countOfConsecutiveZeroBytes = 0
}
return nalFound
}
func newNal(data []byte) *NAL {
return &NAL{PictureOrderCount: 0, ForbiddenZeroBit: false, RefIdc: 0, UnitType: NalUnitTypeUnspecified, Data: data}
}
func (h *NAL) parseHeader() {
firstByte := h.Data[0]
h.ForbiddenZeroBit = (((firstByte & 0x80) >> 7) == 1) // 0x80 = 0b10000000
h.RefIdc = (firstByte & 0x60) >> 5 // 0x60 = 0b01100000
h.UnitType = NalUnitType((firstByte & 0x1F) >> 0) // 0x1F = 0b00011111
}

View file

@ -0,0 +1,68 @@
package h264reader
import "strconv"
// NalUnitType is the type of a NAL
type NalUnitType uint8
// Enums for NalUnitTypes
const (
NalUnitTypeUnspecified NalUnitType = 0 // Unspecified
NalUnitTypeCodedSliceNonIdr NalUnitType = 1 // Coded slice of a non-IDR picture
NalUnitTypeCodedSliceDataPartitionA NalUnitType = 2 // Coded slice data partition A
NalUnitTypeCodedSliceDataPartitionB NalUnitType = 3 // Coded slice data partition B
NalUnitTypeCodedSliceDataPartitionC NalUnitType = 4 // Coded slice data partition C
NalUnitTypeCodedSliceIdr NalUnitType = 5 // Coded slice of an IDR picture
NalUnitTypeSEI NalUnitType = 6 // Supplemental enhancement information (SEI)
NalUnitTypeSPS NalUnitType = 7 // Sequence parameter set
NalUnitTypePPS NalUnitType = 8 // Picture parameter set
NalUnitTypeAUD NalUnitType = 9 // Access unit delimiter
NalUnitTypeEndOfSequence NalUnitType = 10 // End of sequence
NalUnitTypeEndOfStream NalUnitType = 11 // End of stream
NalUnitTypeFiller NalUnitType = 12 // Filler data
NalUnitTypeSpsExt NalUnitType = 13 // Sequence parameter set extension
NalUnitTypeCodedSliceAux NalUnitType = 19 // Coded slice of an auxiliary coded picture without partitioning
// 14..18 // Reserved
// 20..23 // Reserved
// 24..31 // Unspecified
)
func (n *NalUnitType) String() string {
var str string
switch *n {
case NalUnitTypeUnspecified:
str = "Unspecified"
case NalUnitTypeCodedSliceNonIdr:
str = "CodedSliceNonIdr"
case NalUnitTypeCodedSliceDataPartitionA:
str = "CodedSliceDataPartitionA"
case NalUnitTypeCodedSliceDataPartitionB:
str = "CodedSliceDataPartitionB"
case NalUnitTypeCodedSliceDataPartitionC:
str = "CodedSliceDataPartitionC"
case NalUnitTypeCodedSliceIdr:
str = "CodedSliceIdr"
case NalUnitTypeSEI:
str = "SEI"
case NalUnitTypeSPS:
str = "SPS"
case NalUnitTypePPS:
str = "PPS"
case NalUnitTypeAUD:
str = "AUD"
case NalUnitTypeEndOfSequence:
str = "EndOfSequence"
case NalUnitTypeEndOfStream:
str = "EndOfStream"
case NalUnitTypeFiller:
str = "Filler"
case NalUnitTypeSpsExt:
str = "SpsExt"
case NalUnitTypeCodedSliceAux:
str = "NalUnitTypeCodedSliceAux"
default:
str = "Unknown"
}
str = str + "(" + strconv.FormatInt(int64(*n), 10) + ")"
return str
}

View file

@ -0,0 +1,90 @@
// Package h264writer implements H264 media container writer
package h264writer
import (
"bytes"
"encoding/binary"
"io"
"os"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
type (
// H264Writer is used to take RTP packets, parse them and
// write the data to an io.Writer.
// Currently it only supports non-interleaved mode
// Therefore, only 1-23, 24 (STAP-A), 28 (FU-A) NAL types are allowed.
// https://tools.ietf.org/html/rfc6184#section-5.2
H264Writer struct {
writer io.Writer
hasKeyFrame bool
}
)
// New builds a new H264 writer
func New(filename string) (*H264Writer, error) {
f, err := os.Create(filename)
if err != nil {
return nil, err
}
return NewWith(f), nil
}
// NewWith initializes a new H264 writer with an io.Writer output
func NewWith(w io.Writer) *H264Writer {
return &H264Writer{
writer: w,
}
}
// WriteRTP adds a new packet and writes the appropriate headers for it
func (h *H264Writer) WriteRTP(packet *rtp.Packet) error {
if len(packet.Payload) == 0 {
return nil
}
if !h.hasKeyFrame {
if h.hasKeyFrame = isKeyFrame(packet.Payload); !h.hasKeyFrame {
// key frame not defined yet. discarding packet
return nil
}
}
data, err := (&codecs.H264Packet{}).Unmarshal(packet.Payload)
if err != nil {
return err
}
_, err = h.writer.Write(data)
return err
}
// Close closes the underlying writer
func (h *H264Writer) Close() error {
if h.writer != nil {
if closer, ok := h.writer.(io.Closer); ok {
return closer.Close()
}
}
return nil
}
func isKeyFrame(data []byte) bool {
const typeSTAPA = 24
var word uint32
payload := bytes.NewReader(data)
err := binary.Read(payload, binary.BigEndian, &word)
if err != nil || (word&0x1F000000)>>24 != typeSTAPA {
return false
}
return word&0x1F == 7
}

View file

@ -0,0 +1,147 @@
// Package ivfwriter implements IVF media container writer
package ivfwriter
import (
"encoding/binary"
"errors"
"io"
"os"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
var (
errFileNotOpened = errors.New("file not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
)
// IVFWriter is used to take RTP packets and write them to an IVF on disk
type IVFWriter struct {
ioWriter io.Writer
count uint64
seenKeyFrame bool
currentFrame []byte
}
// New builds a new IVF writer
func New(fileName string) (*IVFWriter, error) {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
writer, err := NewWith(f)
if err != nil {
return nil, err
}
writer.ioWriter = f
return writer, nil
}
// NewWith initialize a new IVF writer with an io.Writer output
func NewWith(out io.Writer) (*IVFWriter, error) {
if out == nil {
return nil, errFileNotOpened
}
writer := &IVFWriter{
ioWriter: out,
seenKeyFrame: false,
}
if err := writer.writeHeader(); err != nil {
return nil, err
}
return writer, nil
}
func (i *IVFWriter) writeHeader() error {
header := make([]byte, 32)
copy(header[0:], "DKIF") // DKIF
binary.LittleEndian.PutUint16(header[4:], 0) // Version
binary.LittleEndian.PutUint16(header[6:], 32) // Header size
copy(header[8:], "VP80") // FOURCC
binary.LittleEndian.PutUint16(header[12:], 640) // Width in pixels
binary.LittleEndian.PutUint16(header[14:], 480) // Height in pixels
binary.LittleEndian.PutUint32(header[16:], 30) // Framerate denominator
binary.LittleEndian.PutUint32(header[20:], 1) // Framerate numerator
binary.LittleEndian.PutUint32(header[24:], 900) // Frame count, will be updated on first Close() call
binary.LittleEndian.PutUint32(header[28:], 0) // Unused
_, err := i.ioWriter.Write(header)
return err
}
// WriteRTP adds a new packet and writes the appropriate headers for it
func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error {
if i.ioWriter == nil {
return errFileNotOpened
}
vp8Packet := codecs.VP8Packet{}
if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil {
return err
}
isKeyFrame := vp8Packet.Payload[0] & 0x01
switch {
case !i.seenKeyFrame && isKeyFrame == 1:
return nil
case i.currentFrame == nil && vp8Packet.S != 1:
return nil
}
i.seenKeyFrame = true
i.currentFrame = append(i.currentFrame, vp8Packet.Payload[0:]...)
if !packet.Marker {
return nil
} else if len(i.currentFrame) == 0 {
return nil
}
frameHeader := make([]byte, 12)
binary.LittleEndian.PutUint32(frameHeader[0:], uint32(len(i.currentFrame))) // Frame length
binary.LittleEndian.PutUint64(frameHeader[4:], i.count) // PTS
i.count++
if _, err := i.ioWriter.Write(frameHeader); err != nil {
return err
} else if _, err := i.ioWriter.Write(i.currentFrame); err != nil {
return err
}
i.currentFrame = nil
return nil
}
// Close stops the recording
func (i *IVFWriter) Close() error {
if i.ioWriter == nil {
// Returns no error as it may be convenient to call
// Close() multiple times
return nil
}
defer func() {
i.ioWriter = nil
}()
if ws, ok := i.ioWriter.(io.WriteSeeker); ok {
// Update the framecount
if _, err := ws.Seek(24, 0); err != nil {
return err
}
buff := make([]byte, 4)
binary.LittleEndian.PutUint32(buff, uint32(i.count))
if _, err := ws.Write(buff); err != nil {
return err
}
}
if closer, ok := i.ioWriter.(io.Closer); ok {
return closer.Close()
}
return nil
}

View file

@ -0,0 +1,25 @@
// Package media provides media writer and filters
package media
import (
"time"
"github.com/pion/rtp"
)
// A Sample contains encoded media and timing information
type Sample struct {
Data []byte
Timestamp time.Time
Duration time.Duration
}
// Writer defines an interface to handle
// the creation of media files
type Writer interface {
// Add the content of an RTP packet to the media
WriteRTP(packet *rtp.Packet) error
// Close the media
// Note: Close implementation must be idempotent
Close() error
}

View file

@ -0,0 +1,215 @@
// Package oggreader implements the Ogg media container reader
package oggreader
import (
"encoding/binary"
"errors"
"io"
)
const (
pageHeaderTypeBeginningOfStream = 0x02
pageHeaderSignature = "OggS"
idPageSignature = "OpusHead"
pageHeaderLen = 27
idPagePayloadLength = 19
)
var (
errNilStream = errors.New("stream is nil")
errBadIDPageSignature = errors.New("bad header signature")
errBadIDPageType = errors.New("wrong header, expected beginning of stream")
errBadIDPageLength = errors.New("payload for id page must be 19 bytes")
errBadIDPagePayloadSignature = errors.New("bad payload signature")
errShortPageHeader = errors.New("not enough data for payload header")
errChecksumMismatch = errors.New("expected and actual checksum do not match")
)
// OggReader is used to read Ogg files and return page payloads
type OggReader struct {
stream io.ReadSeeker
bytesReadSuccesfully int64
checksumTable *[256]uint32
doChecksum bool
}
// OggHeader is the metadata from the first two pages
// in the file (ID and Comment)
//
// https://tools.ietf.org/html/rfc7845.html#section-3
type OggHeader struct {
ChannelMap uint8
Channels uint8
OutputGain uint16
PreSkip uint16
SampleRate uint32
Version uint8
}
// OggPageHeader is the metadata for a Page
// Pages are the fundamental unit of multiplexing in an Ogg stream
//
// https://tools.ietf.org/html/rfc7845.html#section-1
type OggPageHeader struct {
GranulePosition uint64
sig [4]byte
version uint8
headerType uint8
serial uint32
index uint32
segmentsCount uint8
}
// NewWith returns a new Ogg reader and Ogg header
// with an io.ReadSeeker input
func NewWith(in io.ReadSeeker) (*OggReader, *OggHeader, error) {
return newWith(in /* doChecksum */, true)
}
func newWith(in io.ReadSeeker, doChecksum bool) (*OggReader, *OggHeader, error) {
if in == nil {
return nil, nil, errNilStream
}
reader := &OggReader{
stream: in,
checksumTable: generateChecksumTable(),
doChecksum: doChecksum,
}
header, err := reader.readHeaders()
if err != nil {
return nil, nil, err
}
return reader, header, nil
}
func (o *OggReader) readHeaders() (*OggHeader, error) {
payload, pageHeader, err := o.ParseNextPage()
if err != nil {
return nil, err
}
header := &OggHeader{}
if string(pageHeader.sig[:]) != pageHeaderSignature {
return nil, errBadIDPageSignature
}
if pageHeader.headerType != pageHeaderTypeBeginningOfStream {
return nil, errBadIDPageType
}
if len(payload) != idPagePayloadLength {
return nil, errBadIDPageLength
}
if s := string(payload[:8]); s != idPageSignature {
return nil, errBadIDPagePayloadSignature
}
header.Version = payload[8]
header.Channels = payload[9]
header.PreSkip = binary.LittleEndian.Uint16(payload[10:12])
header.SampleRate = binary.LittleEndian.Uint32(payload[12:16])
header.OutputGain = binary.LittleEndian.Uint16(payload[16:18])
header.ChannelMap = payload[18]
return header, nil
}
// ParseNextPage reads from stream and returns Ogg page payload, header,
// and an error if there is incomplete page data.
func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) {
h := make([]byte, pageHeaderLen)
n, err := io.ReadFull(o.stream, h)
if err != nil {
return nil, nil, err
} else if n < len(h) {
return nil, nil, errShortPageHeader
}
pageHeader := &OggPageHeader{
sig: [4]byte{h[0], h[1], h[2], h[3]},
}
pageHeader.version = h[4]
pageHeader.headerType = h[5]
pageHeader.GranulePosition = binary.LittleEndian.Uint64(h[6 : 6+8])
pageHeader.serial = binary.LittleEndian.Uint32(h[14 : 14+4])
pageHeader.index = binary.LittleEndian.Uint32(h[18 : 18+4])
pageHeader.segmentsCount = h[26]
sizeBuffer := make([]byte, pageHeader.segmentsCount)
if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil {
return nil, nil, err
}
payloadSize := 0
for _, s := range sizeBuffer {
payloadSize += int(s)
}
payload := make([]byte, payloadSize)
if _, err = io.ReadFull(o.stream, payload); err != nil {
return nil, nil, err
}
if o.doChecksum {
var checksum uint32
updateChecksum := func(v byte) {
checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v]
}
for index := range h {
// Don't include expected checksum in our generation
if index > 21 && index < 26 {
updateChecksum(0)
continue
}
updateChecksum(h[index])
}
for _, s := range sizeBuffer {
updateChecksum(s)
}
for index := range payload {
updateChecksum(payload[index])
}
if binary.LittleEndian.Uint32(h[22:22+4]) != checksum {
return nil, nil, errChecksumMismatch
}
}
return payload, pageHeader, nil
}
// ResetReader resets the internal stream of OggReader. This is useful
// for live streams, where the end of the file might be read without the
// data being finished.
func (o *OggReader) ResetReader(reset func(bytesRead int64) io.ReadSeeker) {
o.stream = reset(o.bytesReadSuccesfully)
}
func generateChecksumTable() *[256]uint32 {
var table [256]uint32
const poly = 0x04c11db7
for i := range table {
r := uint32(i) << 24
for j := 0; j < 8; j++ {
if (r & 0x80000000) != 0 {
r = (r << 1) ^ poly
} else {
r <<= 1
}
table[i] = (r & 0xffffffff)
}
}
return &table
}

View file

@ -0,0 +1,261 @@
// Package oggwriter implements OGG media container writer
package oggwriter
import (
"encoding/binary"
"errors"
"io"
"os"
"github.com/pion/randutil"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
const (
pageHeaderTypeContinuationOfStream = 0x00
pageHeaderTypeBeginningOfStream = 0x02
pageHeaderTypeEndOfStream = 0x04
defaultPreSkip = 3840 // 3840 recommended in the RFC
idPageSignature = "OpusHead"
commentPageSignature = "OpusTags"
pageHeaderSignature = "OggS"
)
var (
errFileNotOpened = errors.New("file not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
)
// OggWriter is used to take RTP packets and write them to an OGG on disk
type OggWriter struct {
stream io.Writer
fd *os.File
sampleRate uint32
channelCount uint16
serial uint32
pageIndex uint32
checksumTable *[256]uint32
previousGranulePosition uint64
previousTimestamp uint32
lastPayloadSize int
}
// New builds a new OGG Opus writer
func New(fileName string, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
writer, err := NewWith(f, sampleRate, channelCount)
if err != nil {
return nil, f.Close()
}
writer.fd = f
return writer, nil
}
// NewWith initialize a new OGG Opus writer with an io.Writer output
func NewWith(out io.Writer, sampleRate uint32, channelCount uint16) (*OggWriter, error) {
if out == nil {
return nil, errFileNotOpened
}
writer := &OggWriter{
stream: out,
sampleRate: sampleRate,
channelCount: channelCount,
serial: randutil.NewMathRandomGenerator().Uint32(),
checksumTable: generateChecksumTable(),
// Timestamp and Granule MUST start from 1
// Only headers can have 0 values
previousTimestamp: 1,
previousGranulePosition: 1,
}
if err := writer.writeHeaders(); err != nil {
return nil, err
}
return writer, nil
}
/*
ref: https://tools.ietf.org/html/rfc7845.html
https://git.xiph.org/?p=opus-tools.git;a=blob;f=src/opus_header.c#l219
Page 0 Pages 1 ... n Pages (n+1) ...
+------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
| | | | | | | | | | | | |
|+----------+| |+-----------------+| |+-------------------+ +-----
|||ID Header|| || Comment Header || ||Audio Data Packet 1| | ...
|+----------+| |+-----------------+| |+-------------------+ +-----
| | | | | | | | | | | | |
+------------+ +---+ +---+ ... +---+ +-----------+ +---------+ +--
^ ^ ^
| | |
| | Mandatory Page Break
| |
| ID header is contained on a single page
|
'Beginning Of Stream'
Figure 1: Example Packet Organization for a Logical Ogg Opus Stream
*/
func (i *OggWriter) writeHeaders() error {
// ID Header
oggIDHeader := make([]byte, 19)
copy(oggIDHeader[0:], idPageSignature) // Magic Signature 'OpusHead'
oggIDHeader[8] = 1 // Version
oggIDHeader[9] = uint8(i.channelCount) // Channel count
binary.LittleEndian.PutUint16(oggIDHeader[10:], defaultPreSkip) // pre-skip
binary.LittleEndian.PutUint32(oggIDHeader[12:], i.sampleRate) // original sample rate, any valid sample e.g 48000
binary.LittleEndian.PutUint16(oggIDHeader[16:], 0) // output gain
oggIDHeader[18] = 0 // channel map 0 = one stream: mono or stereo
// Reference: https://tools.ietf.org/html/rfc7845.html#page-6
// RFC specifies that the ID Header page should have a granule position of 0 and a Header Type set to 2 (StartOfStream)
data := i.createPage(oggIDHeader, pageHeaderTypeBeginningOfStream, 0, i.pageIndex)
if err := i.writeToStream(data); err != nil {
return err
}
i.pageIndex++
// Comment Header
oggCommentHeader := make([]byte, 21)
copy(oggCommentHeader[0:], commentPageSignature) // Magic Signature 'OpusTags'
binary.LittleEndian.PutUint32(oggCommentHeader[8:], 5) // Vendor Length
copy(oggCommentHeader[12:], "pion") // Vendor name 'pion'
binary.LittleEndian.PutUint32(oggCommentHeader[17:], 0) // User Comment List Length
// RFC specifies that the page where the CommentHeader completes should have a granule position of 0
data = i.createPage(oggCommentHeader, pageHeaderTypeContinuationOfStream, 0, i.pageIndex)
if err := i.writeToStream(data); err != nil {
return err
}
i.pageIndex++
return nil
}
const (
pageHeaderSize = 27
)
func (i *OggWriter) createPage(payload []uint8, headerType uint8, granulePos uint64, pageIndex uint32) []byte {
i.lastPayloadSize = len(payload)
page := make([]byte, pageHeaderSize+1+i.lastPayloadSize)
copy(page[0:], pageHeaderSignature) // page headers starts with 'OggS'
page[4] = 0 // Version
page[5] = headerType // 1 = continuation, 2 = beginning of stream, 4 = end of stream
binary.LittleEndian.PutUint64(page[6:], granulePos) // granule position
binary.LittleEndian.PutUint32(page[14:], i.serial) // Bitstream serial number
binary.LittleEndian.PutUint32(page[18:], pageIndex) // Page sequence number
page[26] = 1 // Number of segments in page, giving always 1 segment
page[27] = uint8(i.lastPayloadSize) // Segment Table inserting at 27th position since page header length is 27
copy(page[28:], payload) // inserting at 28th since Segment Table(1) + header length(27)
var checksum uint32
for index := range page {
checksum = (checksum << 8) ^ i.checksumTable[byte(checksum>>24)^page[index]]
}
binary.LittleEndian.PutUint32(page[22:], checksum) // Checksum - generating for page data and inserting at 22th position into 32 bits
return page
}
// WriteRTP adds a new packet and writes the appropriate headers for it
func (i *OggWriter) WriteRTP(packet *rtp.Packet) error {
if packet == nil {
return errInvalidNilPacket
}
opusPacket := codecs.OpusPacket{}
if _, err := opusPacket.Unmarshal(packet.Payload); err != nil {
// Only handle Opus packets
return err
}
payload := opusPacket.Payload[0:]
// Should be equivalent to sampleRate * duration
if i.previousTimestamp != 1 {
increment := packet.Timestamp - i.previousTimestamp
i.previousGranulePosition += uint64(increment)
}
i.previousTimestamp = packet.Timestamp
data := i.createPage(payload, pageHeaderTypeContinuationOfStream, i.previousGranulePosition, i.pageIndex)
i.pageIndex++
return i.writeToStream(data)
}
// Close stops the recording
func (i *OggWriter) Close() error {
defer func() {
i.fd = nil
i.stream = nil
}()
// Returns no error has it may be convenient to call
// Close() multiple times
if i.fd == nil {
// Close stream if we are operating on a stream
if closer, ok := i.stream.(io.Closer); ok {
return closer.Close()
}
return nil
}
// Seek back one page, we need to update the header and generate new CRC
pageOffset, err := i.fd.Seek(-1*int64(i.lastPayloadSize+pageHeaderSize+1), 2)
if err != nil {
return err
}
payload := make([]byte, i.lastPayloadSize)
if _, err := i.fd.ReadAt(payload, pageOffset+pageHeaderSize+1); err != nil {
return err
}
data := i.createPage(payload, pageHeaderTypeEndOfStream, i.previousGranulePosition, i.pageIndex-1)
if err := i.writeToStream(data); err != nil {
return err
}
// Update the last page if we are operating on files
// to mark it as the EOS
return i.fd.Close()
}
// Wraps writing to the stream and maintains state
// so we can set values for EOS
func (i *OggWriter) writeToStream(p []byte) error {
if i.stream == nil {
return errFileNotOpened
}
_, err := i.stream.Write(p)
return err
}
func generateChecksumTable() *[256]uint32 {
var table [256]uint32
const poly = 0x04c11db7
for i := range table {
r := uint32(i) << 24
for j := 0; j < 8; j++ {
if (r & 0x80000000) != 0 {
r = (r << 1) ^ poly
} else {
r <<= 1
}
table[i] = (r & 0xffffffff)
}
}
return &table
}

View file

@ -0,0 +1,160 @@
// Package rtcerr implements the error wrappers defined throughout the
// WebRTC 1.0 specifications.
package rtcerr
import (
"fmt"
)
// UnknownError indicates the operation failed for an unknown transient reason.
type UnknownError struct {
Err error
}
func (e *UnknownError) Error() string {
return fmt.Sprintf("UnknownError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *UnknownError) Unwrap() error {
return e.Err
}
// InvalidStateError indicates the object is in an invalid state.
type InvalidStateError struct {
Err error
}
func (e *InvalidStateError) Error() string {
return fmt.Sprintf("InvalidStateError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *InvalidStateError) Unwrap() error {
return e.Err
}
// InvalidAccessError indicates the object does not support the operation or
// argument.
type InvalidAccessError struct {
Err error
}
func (e *InvalidAccessError) Error() string {
return fmt.Sprintf("InvalidAccessError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *InvalidAccessError) Unwrap() error {
return e.Err
}
// NotSupportedError indicates the operation is not supported.
type NotSupportedError struct {
Err error
}
func (e *NotSupportedError) Error() string {
return fmt.Sprintf("NotSupportedError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *NotSupportedError) Unwrap() error {
return e.Err
}
// InvalidModificationError indicates the object cannot be modified in this way.
type InvalidModificationError struct {
Err error
}
func (e *InvalidModificationError) Error() string {
return fmt.Sprintf("InvalidModificationError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *InvalidModificationError) Unwrap() error {
return e.Err
}
// SyntaxError indicates the string did not match the expected pattern.
type SyntaxError struct {
Err error
}
func (e *SyntaxError) Error() string {
return fmt.Sprintf("SyntaxError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *SyntaxError) Unwrap() error {
return e.Err
}
// TypeError indicates an error when a value is not of the expected type.
type TypeError struct {
Err error
}
func (e *TypeError) Error() string {
return fmt.Sprintf("TypeError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *TypeError) Unwrap() error {
return e.Err
}
// OperationError indicates the operation failed for an operation-specific
// reason.
type OperationError struct {
Err error
}
func (e *OperationError) Error() string {
return fmt.Sprintf("OperationError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *OperationError) Unwrap() error {
return e.Err
}
// NotReadableError indicates the input/output read operation failed.
type NotReadableError struct {
Err error
}
func (e *NotReadableError) Error() string {
return fmt.Sprintf("NotReadableError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *NotReadableError) Unwrap() error {
return e.Err
}
// RangeError indicates an error when a value is not in the set or range
// of allowed values.
type RangeError struct {
Err error
}
func (e *RangeError) Error() string {
return fmt.Sprintf("RangeError: %v", e.Err)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's type contains
// an Unwrap method returning error. Otherwise, Unwrap returns nil.
func (e *RangeError) Unwrap() error {
return e.Err
}

View file

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
],
"commitBody": "Generated by renovateBot",
"packageRules": [
{
"packagePatterns": ["^golang.org/x/"],
"schedule": ["on the first day of the month"]
}
]
}

View file

@ -0,0 +1,31 @@
package webrtc
const (
// TypeRTCPFBTransportCC ..
TypeRTCPFBTransportCC = "transport-cc"
// TypeRTCPFBGoogREMB ..
TypeRTCPFBGoogREMB = "goog-remb"
// TypeRTCPFBACK ..
TypeRTCPFBACK = "ack"
// TypeRTCPFBCCM ..
TypeRTCPFBCCM = "ccm"
// TypeRTCPFBNACK ..
TypeRTCPFBNACK = "nack"
)
// RTCPFeedback signals the connection to use additional RTCP packet types.
// https://draft.ortc.org/#dom-rtcrtcpfeedback
type RTCPFeedback struct {
// Type is the type of feedback.
// see: https://draft.ortc.org/#dom-rtcrtcpfeedback
// valid: ack, ccm, nack, goog-remb, transport-cc
Type string
// The parameter value depends on the type.
// For example, type="nack" parameter="pli" will send Picture Loss Indicator packets.
Parameter string
}

View file

@ -0,0 +1,66 @@
package webrtc
import (
"encoding/json"
)
// RTCPMuxPolicy affects what ICE candidates are gathered to support
// non-multiplexed RTCP.
type RTCPMuxPolicy int
const (
// RTCPMuxPolicyNegotiate indicates to gather ICE candidates for both
// RTP and RTCP candidates. If the remote-endpoint is capable of
// multiplexing RTCP, multiplex RTCP on the RTP candidates. If it is not,
// use both the RTP and RTCP candidates separately.
RTCPMuxPolicyNegotiate RTCPMuxPolicy = iota + 1
// RTCPMuxPolicyRequire indicates to gather ICE candidates only for
// RTP and multiplex RTCP on the RTP candidates. If the remote endpoint is
// not capable of rtcp-mux, session negotiation will fail.
RTCPMuxPolicyRequire
)
// This is done this way because of a linter.
const (
rtcpMuxPolicyNegotiateStr = "negotiate"
rtcpMuxPolicyRequireStr = "require"
)
func newRTCPMuxPolicy(raw string) RTCPMuxPolicy {
switch raw {
case rtcpMuxPolicyNegotiateStr:
return RTCPMuxPolicyNegotiate
case rtcpMuxPolicyRequireStr:
return RTCPMuxPolicyRequire
default:
return RTCPMuxPolicy(Unknown)
}
}
func (t RTCPMuxPolicy) String() string {
switch t {
case RTCPMuxPolicyNegotiate:
return rtcpMuxPolicyNegotiateStr
case RTCPMuxPolicyRequire:
return rtcpMuxPolicyRequireStr
default:
return ErrUnknownType.Error()
}
}
// UnmarshalJSON parses the JSON-encoded data and stores the result
func (t *RTCPMuxPolicy) UnmarshalJSON(b []byte) error {
var val string
if err := json.Unmarshal(b, &val); err != nil {
return err
}
*t = newRTCPMuxPolicy(val)
return nil
}
// MarshalJSON returns the JSON encoding
func (t RTCPMuxPolicy) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}

View file

@ -0,0 +1,9 @@
package webrtc
// RTPCapabilities represents the capabilities of a transceiver
//
// https://w3c.github.io/webrtc-pc/#rtcrtpcapabilities
type RTPCapabilities struct {
Codecs []RTPCodecCapability
HeaderExtensions []RTPHeaderExtensionCapability
}

View file

@ -0,0 +1,107 @@
package webrtc
import (
"strings"
)
// RTPCodecType determines the type of a codec
type RTPCodecType int
const (
// RTPCodecTypeAudio indicates this is an audio codec
RTPCodecTypeAudio RTPCodecType = iota + 1
// RTPCodecTypeVideo indicates this is a video codec
RTPCodecTypeVideo
)
func (t RTPCodecType) String() string {
switch t {
case RTPCodecTypeAudio:
return "audio"
case RTPCodecTypeVideo:
return "video" //nolint: goconst
default:
return ErrUnknownType.Error()
}
}
// NewRTPCodecType creates a RTPCodecType from a string
func NewRTPCodecType(r string) RTPCodecType {
switch {
case strings.EqualFold(r, RTPCodecTypeAudio.String()):
return RTPCodecTypeAudio
case strings.EqualFold(r, RTPCodecTypeVideo.String()):
return RTPCodecTypeVideo
default:
return RTPCodecType(0)
}
}
// RTPCodecCapability provides information about codec capabilities.
//
// https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpcodeccapability-members
type RTPCodecCapability struct {
MimeType string
ClockRate uint32
Channels uint16
SDPFmtpLine string
RTCPFeedback []RTCPFeedback
}
// RTPHeaderExtensionCapability is used to define a RFC5285 RTP header extension supported by the codec.
//
// https://w3c.github.io/webrtc-pc/#dom-rtcrtpcapabilities-headerextensions
type RTPHeaderExtensionCapability struct {
URI string
}
// RTPHeaderExtensionParameter represents a negotiated RFC5285 RTP header extension.
//
// https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpheaderextensionparameters-members
type RTPHeaderExtensionParameter struct {
URI string
ID int
}
// RTPCodecParameters is a sequence containing the media codecs that an RtpSender
// will choose from, as well as entries for RTX, RED and FEC mechanisms. This also
// includes the PayloadType that has been negotiated
//
// https://w3c.github.io/webrtc-pc/#rtcrtpcodecparameters
type RTPCodecParameters struct {
RTPCodecCapability
PayloadType PayloadType
statsID string
}
// RTPParameters is a list of negotiated codecs and header extensions
//
// https://w3c.github.io/webrtc-pc/#dictionary-rtcrtpparameters-members
type RTPParameters struct {
HeaderExtensions []RTPHeaderExtensionParameter
Codecs []RTPCodecParameters
}
// Do a fuzzy find for a codec in the list of codecs
// Used for lookup up a codec in an existing list to find a match
func codecParametersFuzzySearch(needle RTPCodecParameters, haystack []RTPCodecParameters) (RTPCodecParameters, error) {
// First attempt to match on MimeType + SDPFmtpLine
for _, c := range haystack {
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) &&
c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine {
return c, nil
}
}
// Fallback to just MimeType
for _, c := range haystack {
if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) {
return c, nil
}
}
return RTPCodecParameters{}, ErrCodecNotFound
}

View file

@ -0,0 +1,10 @@
package webrtc
// RTPCodingParameters provides information relating to both encoding and decoding.
// This is a subset of the RFC since Pion WebRTC doesn't implement encoding/decoding itself
// http://draft.ortc.org/#dom-rtcrtpcodingparameters
type RTPCodingParameters struct {
RID string `json:"rid"`
SSRC SSRC `json:"ssrc"`
PayloadType PayloadType `json:"payloadType"`
}

View file

@ -0,0 +1,8 @@
package webrtc
// RTPDecodingParameters provides information relating to both encoding and decoding.
// This is a subset of the RFC since Pion WebRTC doesn't implement decoding itself
// http://draft.ortc.org/#dom-rtcrtpdecodingparameters
type RTPDecodingParameters struct {
RTPCodingParameters
}

View file

@ -0,0 +1,8 @@
package webrtc
// RTPEncodingParameters provides information relating to both encoding and decoding.
// This is a subset of the RFC since Pion WebRTC doesn't implement encoding itself
// http://draft.ortc.org/#dom-rtcrtpencodingparameters
type RTPEncodingParameters struct {
RTPCodingParameters
}

View file

@ -0,0 +1,6 @@
package webrtc
// RTPReceiveParameters contains the RTP stack settings used by receivers
type RTPReceiveParameters struct {
Encodings []RTPDecodingParameters
}

View file

@ -0,0 +1,361 @@
// +build !js
package webrtc
import (
"fmt"
"io"
"sync"
"time"
"github.com/pion/interceptor"
"github.com/pion/rtcp"
"github.com/pion/srtp/v2"
"github.com/pion/webrtc/v3/internal/util"
)
// trackStreams maintains a mapping of RTP/RTCP streams to a specific track
// a RTPReceiver may contain multiple streams if we are dealing with Multicast
type trackStreams struct {
track *TrackRemote
rtpReadStream *srtp.ReadStreamSRTP
rtpInterceptor interceptor.RTPReader
rtcpReadStream *srtp.ReadStreamSRTCP
rtcpInterceptor interceptor.RTCPReader
}
// RTPReceiver allows an application to inspect the receipt of a TrackRemote
type RTPReceiver struct {
kind RTPCodecType
transport *DTLSTransport
tracks []trackStreams
closed, received chan interface{}
mu sync.RWMutex
// A reference to the associated api object
api *API
}
// NewRTPReceiver constructs a new RTPReceiver
func (api *API) NewRTPReceiver(kind RTPCodecType, transport *DTLSTransport) (*RTPReceiver, error) {
if transport == nil {
return nil, errRTPReceiverDTLSTransportNil
}
r := &RTPReceiver{
kind: kind,
transport: transport,
api: api,
closed: make(chan interface{}),
received: make(chan interface{}),
tracks: []trackStreams{},
}
return r, nil
}
// Transport returns the currently-configured *DTLSTransport or nil
// if one has not yet been configured
func (r *RTPReceiver) Transport() *DTLSTransport {
r.mu.RLock()
defer r.mu.RUnlock()
return r.transport
}
// GetParameters describes the current configuration for the encoding and
// transmission of media on the receiver's track.
func (r *RTPReceiver) GetParameters() RTPParameters {
return r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
}
// Track returns the RtpTransceiver TrackRemote
func (r *RTPReceiver) Track() *TrackRemote {
r.mu.RLock()
defer r.mu.RUnlock()
if len(r.tracks) != 1 {
return nil
}
return r.tracks[0].track
}
// Tracks returns the RtpTransceiver tracks
// A RTPReceiver to support Simulcast may now have multiple tracks
func (r *RTPReceiver) Tracks() []*TrackRemote {
r.mu.RLock()
defer r.mu.RUnlock()
var tracks []*TrackRemote
for i := range r.tracks {
tracks = append(tracks, r.tracks[i].track)
}
return tracks
}
// Receive initialize the track and starts all the transports
func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
r.mu.Lock()
defer r.mu.Unlock()
select {
case <-r.received:
return errRTPReceiverReceiveAlreadyCalled
default:
}
defer close(r.received)
if len(parameters.Encodings) == 1 && parameters.Encodings[0].SSRC != 0 {
t := trackStreams{
track: newTrackRemote(
r.kind,
parameters.Encodings[0].SSRC,
"",
r,
),
}
globalParams := r.GetParameters()
codec := RTPCodecCapability{}
if len(globalParams.Codecs) != 0 {
codec = globalParams.Codecs[0].RTPCodecCapability
}
streamInfo := createStreamInfo("", parameters.Encodings[0].SSRC, 0, codec, globalParams.HeaderExtensions)
var err error
if t.rtpReadStream, t.rtpInterceptor, t.rtcpReadStream, t.rtcpInterceptor, err = r.streamsForSSRC(parameters.Encodings[0].SSRC, streamInfo); err != nil {
return err
}
r.tracks = append(r.tracks, t)
} else {
for _, encoding := range parameters.Encodings {
r.tracks = append(r.tracks, trackStreams{
track: newTrackRemote(
r.kind,
0,
encoding.RID,
r,
),
})
}
}
return nil
}
// Read reads incoming RTCP for this RTPReceiver
func (r *RTPReceiver) Read(b []byte) (n int, a interceptor.Attributes, err error) {
select {
case <-r.received:
return r.tracks[0].rtcpInterceptor.Read(b, a)
case <-r.closed:
return 0, nil, io.ErrClosedPipe
}
}
// ReadSimulcast reads incoming RTCP for this RTPReceiver for given rid
func (r *RTPReceiver) ReadSimulcast(b []byte, rid string) (n int, a interceptor.Attributes, err error) {
select {
case <-r.received:
for _, t := range r.tracks {
if t.track != nil && t.track.rid == rid {
return t.rtcpInterceptor.Read(b, a)
}
}
return 0, nil, fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid)
case <-r.closed:
return 0, nil, io.ErrClosedPipe
}
}
// ReadRTCP is a convenience method that wraps Read and unmarshal for you.
// It also runs any configured interceptors.
func (r *RTPReceiver) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error) {
b := make([]byte, receiveMTU)
i, attributes, err := r.Read(b)
if err != nil {
return nil, nil, err
}
pkts, err := rtcp.Unmarshal(b[:i])
if err != nil {
return nil, nil, err
}
return pkts, attributes, nil
}
// ReadSimulcastRTCP is a convenience method that wraps ReadSimulcast and unmarshal for you
func (r *RTPReceiver) ReadSimulcastRTCP(rid string) ([]rtcp.Packet, interceptor.Attributes, error) {
b := make([]byte, receiveMTU)
i, attributes, err := r.ReadSimulcast(b, rid)
if err != nil {
return nil, nil, err
}
pkts, err := rtcp.Unmarshal(b[:i])
return pkts, attributes, err
}
func (r *RTPReceiver) haveReceived() bool {
select {
case <-r.received:
return true
default:
return false
}
}
// Stop irreversibly stops the RTPReceiver
func (r *RTPReceiver) Stop() error {
r.mu.Lock()
defer r.mu.Unlock()
var err error
select {
case <-r.closed:
return err
default:
}
select {
case <-r.received:
for i := range r.tracks {
errs := []error{}
if r.tracks[i].rtcpReadStream != nil {
errs = append(errs, r.tracks[i].rtcpReadStream.Close())
}
if r.tracks[i].rtpReadStream != nil {
errs = append(errs, r.tracks[i].rtpReadStream.Close())
}
err = util.FlattenErrs(errs)
}
default:
}
close(r.closed)
return err
}
func (r *RTPReceiver) streamsForTrack(t *TrackRemote) *trackStreams {
for i := range r.tracks {
if r.tracks[i].track == t {
return &r.tracks[i]
}
}
return nil
}
// readRTP should only be called by a track, this only exists so we can keep state in one place
func (r *RTPReceiver) readRTP(b []byte, reader *TrackRemote) (n int, a interceptor.Attributes, err error) {
<-r.received
if t := r.streamsForTrack(reader); t != nil {
return t.rtpInterceptor.Read(b, a)
}
return 0, nil, fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC())
}
// receiveForRid is the sibling of Receive expect for RIDs instead of SSRCs
// It populates all the internal state for the given RID
func (r *RTPReceiver) receiveForRid(rid string, params RTPParameters, ssrc SSRC) (*TrackRemote, error) {
r.mu.Lock()
defer r.mu.Unlock()
for i := range r.tracks {
if r.tracks[i].track.RID() == rid {
r.tracks[i].track.mu.Lock()
r.tracks[i].track.kind = r.kind
r.tracks[i].track.codec = params.Codecs[0]
r.tracks[i].track.params = params
r.tracks[i].track.ssrc = ssrc
streamInfo := createStreamInfo("", ssrc, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions)
r.tracks[i].track.mu.Unlock()
var err error
if r.tracks[i].rtpReadStream, r.tracks[i].rtpInterceptor, r.tracks[i].rtcpReadStream, r.tracks[i].rtcpInterceptor, err = r.streamsForSSRC(ssrc, streamInfo); err != nil {
return nil, err
}
return r.tracks[i].track, nil
}
}
return nil, fmt.Errorf("%w: %d", errRTPReceiverForSSRCTrackStreamNotFound, ssrc)
}
func (r *RTPReceiver) streamsForSSRC(ssrc SSRC, streamInfo interceptor.StreamInfo) (*srtp.ReadStreamSRTP, interceptor.RTPReader, *srtp.ReadStreamSRTCP, interceptor.RTCPReader, error) {
srtpSession, err := r.transport.getSRTPSession()
if err != nil {
return nil, nil, nil, nil, err
}
rtpReadStream, err := srtpSession.OpenReadStream(uint32(ssrc))
if err != nil {
return nil, nil, nil, nil, err
}
rtpInterceptor := r.api.interceptor.BindRemoteStream(&streamInfo, interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) {
n, err = rtpReadStream.Read(in)
return n, a, err
}))
srtcpSession, err := r.transport.getSRTCPSession()
if err != nil {
return nil, nil, nil, nil, err
}
rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(ssrc))
if err != nil {
return nil, nil, nil, nil, err
}
rtcpInterceptor := r.api.interceptor.BindRTCPReader(interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) {
n, err = rtcpReadStream.Read(in)
return n, a, err
}))
return rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, nil
}
// SetReadDeadline sets the max amount of time the RTCP stream will block before returning. 0 is forever.
func (r *RTPReceiver) SetReadDeadline(t time.Time) error {
r.mu.RLock()
defer r.mu.RUnlock()
if err := r.tracks[0].rtcpReadStream.SetReadDeadline(t); err != nil {
return err
}
return nil
}
// SetReadDeadlineSimulcast sets the max amount of time the RTCP stream for a given rid will block before returning. 0 is forever.
func (r *RTPReceiver) SetReadDeadlineSimulcast(deadline time.Time, rid string) error {
r.mu.RLock()
defer r.mu.RUnlock()
for _, t := range r.tracks {
if t.track != nil && t.track.rid == rid {
return t.rtcpReadStream.SetReadDeadline(deadline)
}
}
return fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid)
}
// setRTPReadDeadline sets the max amount of time the RTP stream will block before returning. 0 is forever.
// This should be fired by calling SetReadDeadline on the TrackRemote
func (r *RTPReceiver) setRTPReadDeadline(deadline time.Time, reader *TrackRemote) error {
r.mu.RLock()
defer r.mu.RUnlock()
if t := r.streamsForTrack(reader); t != nil {
return t.rtpReadStream.SetReadDeadline(deadline)
}
return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, reader.SSRC())
}

View file

@ -0,0 +1,254 @@
// +build !js
package webrtc
import (
"io"
"sync"
"github.com/pion/interceptor"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/rtp"
)
// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer
type RTPSender struct {
track TrackLocal
srtpStream *srtpWriterFuture
rtcpInterceptor interceptor.RTCPReader
context TrackLocalContext
transport *DTLSTransport
payloadType PayloadType
ssrc SSRC
// nolint:godox
// TODO(sgotti) remove this when in future we'll avoid replacing
// a transceiver sender since we can just check the
// transceiver negotiation status
negotiated bool
// A reference to the associated api object
api *API
id string
mu sync.RWMutex
sendCalled, stopCalled chan struct{}
}
// NewRTPSender constructs a new RTPSender
func (api *API) NewRTPSender(track TrackLocal, transport *DTLSTransport) (*RTPSender, error) {
if track == nil {
return nil, errRTPSenderTrackNil
} else if transport == nil {
return nil, errRTPSenderDTLSTransportNil
}
id, err := randutil.GenerateCryptoRandomString(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
if err != nil {
return nil, err
}
r := &RTPSender{
track: track,
transport: transport,
api: api,
sendCalled: make(chan struct{}),
stopCalled: make(chan struct{}),
ssrc: SSRC(randutil.NewMathRandomGenerator().Uint32()),
id: id,
srtpStream: &srtpWriterFuture{},
}
r.srtpStream.rtpSender = r
r.rtcpInterceptor = r.api.interceptor.BindRTCPReader(interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) {
n, err = r.srtpStream.Read(in)
return n, a, err
}))
return r, nil
}
func (r *RTPSender) isNegotiated() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.negotiated
}
func (r *RTPSender) setNegotiated() {
r.mu.Lock()
defer r.mu.Unlock()
r.negotiated = true
}
// Transport returns the currently-configured *DTLSTransport or nil
// if one has not yet been configured
func (r *RTPSender) Transport() *DTLSTransport {
r.mu.RLock()
defer r.mu.RUnlock()
return r.transport
}
// GetParameters describes the current configuration for the encoding and
// transmission of media on the sender's track.
func (r *RTPSender) GetParameters() RTPSendParameters {
return RTPSendParameters{
RTPParameters: r.api.mediaEngine.getRTPParametersByKind(
r.track.Kind(),
[]RTPTransceiverDirection{RTPTransceiverDirectionSendonly},
),
Encodings: []RTPEncodingParameters{
{
RTPCodingParameters: RTPCodingParameters{
SSRC: r.ssrc,
PayloadType: r.payloadType,
},
},
},
}
}
// Track returns the RTCRtpTransceiver track, or nil
func (r *RTPSender) Track() TrackLocal {
r.mu.RLock()
defer r.mu.RUnlock()
return r.track
}
// ReplaceTrack replaces the track currently being used as the sender's source with a new TrackLocal.
// The new track must be of the same media kind (audio, video, etc) and switching the track should not
// require negotiation.
func (r *RTPSender) ReplaceTrack(track TrackLocal) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.hasSent() && r.track != nil {
if err := r.track.Unbind(r.context); err != nil {
return err
}
}
if !r.hasSent() || track == nil {
r.track = track
return nil
}
if _, err := track.Bind(r.context); err != nil {
// Re-bind the original track
if _, reBindErr := r.track.Bind(r.context); reBindErr != nil {
return reBindErr
}
return err
}
r.track = track
return nil
}
// Send Attempts to set the parameters controlling the sending of media.
func (r *RTPSender) Send(parameters RTPSendParameters) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.hasSent() {
return errRTPSenderSendAlreadyCalled
}
writeStream := &interceptorToTrackLocalWriter{}
r.context = TrackLocalContext{
id: r.id,
params: r.api.mediaEngine.getRTPParametersByKind(r.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
ssrc: parameters.Encodings[0].SSRC,
writeStream: writeStream,
}
codec, err := r.track.Bind(r.context)
if err != nil {
return err
}
r.context.params.Codecs = []RTPCodecParameters{codec}
streamInfo := createStreamInfo(r.id, parameters.Encodings[0].SSRC, codec.PayloadType, codec.RTPCodecCapability, parameters.HeaderExtensions)
rtpInterceptor := r.api.interceptor.BindLocalStream(&streamInfo, interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) {
return r.srtpStream.WriteRTP(header, payload)
}))
writeStream.interceptor.Store(rtpInterceptor)
close(r.sendCalled)
return nil
}
// Stop irreversibly stops the RTPSender
func (r *RTPSender) Stop() error {
r.mu.Lock()
if stopped := r.hasStopped(); stopped {
r.mu.Unlock()
return nil
}
close(r.stopCalled)
r.mu.Unlock()
if !r.hasSent() {
return nil
}
if err := r.ReplaceTrack(nil); err != nil {
return err
}
return r.srtpStream.Close()
}
// Read reads incoming RTCP for this RTPReceiver
func (r *RTPSender) Read(b []byte) (n int, a interceptor.Attributes, err error) {
select {
case <-r.sendCalled:
return r.rtcpInterceptor.Read(b, a)
case <-r.stopCalled:
return 0, nil, io.ErrClosedPipe
}
}
// ReadRTCP is a convenience method that wraps Read and unmarshals for you.
func (r *RTPSender) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error) {
b := make([]byte, receiveMTU)
i, attributes, err := r.Read(b)
if err != nil {
return nil, nil, err
}
pkts, err := rtcp.Unmarshal(b[:i])
if err != nil {
return nil, nil, err
}
return pkts, attributes, nil
}
// hasSent tells if data has been ever sent for this instance
func (r *RTPSender) hasSent() bool {
select {
case <-r.sendCalled:
return true
default:
return false
}
}
// hasStopped tells if stop has been called
func (r *RTPSender) hasStopped() bool {
select {
case <-r.stopCalled:
return true
default:
return false
}
}

View file

@ -0,0 +1,7 @@
package webrtc
// RTPSendParameters contains the RTP stack settings used by receivers
type RTPSendParameters struct {
RTPParameters
Encodings []RTPEncodingParameters
}

View file

@ -0,0 +1,187 @@
// +build !js
package webrtc
import (
"fmt"
"sync/atomic"
"github.com/pion/rtp"
)
// RTPTransceiver represents a combination of an RTPSender and an RTPReceiver that share a common mid.
type RTPTransceiver struct {
mid atomic.Value // string
sender atomic.Value // *RTPSender
receiver atomic.Value // *RTPReceiver
direction atomic.Value // RTPTransceiverDirection
stopped bool
kind RTPCodecType
}
// Sender returns the RTPTransceiver's RTPSender if it has one
func (t *RTPTransceiver) Sender() *RTPSender {
if v := t.sender.Load(); v != nil {
return v.(*RTPSender)
}
return nil
}
// SetSender sets the RTPSender and Track to current transceiver
func (t *RTPTransceiver) SetSender(s *RTPSender, track TrackLocal) error {
t.setSender(s)
return t.setSendingTrack(track)
}
func (t *RTPTransceiver) setSender(s *RTPSender) {
t.sender.Store(s)
}
// Receiver returns the RTPTransceiver's RTPReceiver if it has one
func (t *RTPTransceiver) Receiver() *RTPReceiver {
if v := t.receiver.Load(); v != nil {
return v.(*RTPReceiver)
}
return nil
}
// setMid sets the RTPTransceiver's mid. If it was already set, will return an error.
func (t *RTPTransceiver) setMid(mid string) error {
if currentMid := t.Mid(); currentMid != "" {
return fmt.Errorf("%w: %s to %s", errRTPTransceiverCannotChangeMid, currentMid, mid)
}
t.mid.Store(mid)
return nil
}
// Mid gets the Transceiver's mid value. When not already set, this value will be set in CreateOffer or CreateAnswer.
func (t *RTPTransceiver) Mid() string {
if v := t.mid.Load(); v != nil {
return v.(string)
}
return ""
}
// Kind returns RTPTransceiver's kind.
func (t *RTPTransceiver) Kind() RTPCodecType {
return t.kind
}
// Direction returns the RTPTransceiver's current direction
func (t *RTPTransceiver) Direction() RTPTransceiverDirection {
return t.direction.Load().(RTPTransceiverDirection)
}
// Stop irreversibly stops the RTPTransceiver
func (t *RTPTransceiver) Stop() error {
if t.Sender() != nil {
if err := t.Sender().Stop(); err != nil {
return err
}
}
if t.Receiver() != nil {
if err := t.Receiver().Stop(); err != nil {
return err
}
}
t.setDirection(RTPTransceiverDirectionInactive)
return nil
}
func (t *RTPTransceiver) setReceiver(r *RTPReceiver) {
t.receiver.Store(r)
}
func (t *RTPTransceiver) setDirection(d RTPTransceiverDirection) {
t.direction.Store(d)
}
func (t *RTPTransceiver) setSendingTrack(track TrackLocal) error {
if err := t.Sender().ReplaceTrack(track); err != nil {
return err
}
if track == nil {
t.setSender(nil)
}
switch {
case track != nil && t.Direction() == RTPTransceiverDirectionRecvonly:
t.setDirection(RTPTransceiverDirectionSendrecv)
case track != nil && t.Direction() == RTPTransceiverDirectionInactive:
t.setDirection(RTPTransceiverDirectionSendonly)
case track == nil && t.Direction() == RTPTransceiverDirectionSendrecv:
t.setDirection(RTPTransceiverDirectionRecvonly)
case track == nil && t.Direction() == RTPTransceiverDirectionSendonly:
t.setDirection(RTPTransceiverDirectionInactive)
default:
return errRTPTransceiverSetSendingInvalidState
}
return nil
}
func findByMid(mid string, localTransceivers []*RTPTransceiver) (*RTPTransceiver, []*RTPTransceiver) {
for i, t := range localTransceivers {
if t.Mid() == mid {
return t, append(localTransceivers[:i], localTransceivers[i+1:]...)
}
}
return nil, localTransceivers
}
// Given a direction+type pluck a transceiver from the passed list
// if no entry satisfies the requested type+direction return a inactive Transceiver
func satisfyTypeAndDirection(remoteKind RTPCodecType, remoteDirection RTPTransceiverDirection, localTransceivers []*RTPTransceiver) (*RTPTransceiver, []*RTPTransceiver) {
// Get direction order from most preferred to least
getPreferredDirections := func() []RTPTransceiverDirection {
switch remoteDirection {
case RTPTransceiverDirectionSendrecv:
return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv}
case RTPTransceiverDirectionSendonly:
return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv}
case RTPTransceiverDirectionRecvonly:
return []RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv}
default:
return []RTPTransceiverDirection{}
}
}
for _, possibleDirection := range getPreferredDirections() {
for i := range localTransceivers {
t := localTransceivers[i]
if t.Mid() == "" && t.kind == remoteKind && possibleDirection == t.Direction() {
return t, append(localTransceivers[:i], localTransceivers[i+1:]...)
}
}
}
return nil, localTransceivers
}
// handleUnknownRTPPacket consumes a single RTP Packet and returns information that is helpful
// for demuxing and handling an unknown SSRC (usually for Simulcast)
func handleUnknownRTPPacket(buf []byte, midExtensionID, streamIDExtensionID uint8) (mid, rid string, payloadType PayloadType, err error) {
rp := &rtp.Packet{}
if err = rp.Unmarshal(buf); err != nil {
return
}
if !rp.Header.Extension {
return
}
payloadType = PayloadType(rp.PayloadType)
if payload := rp.GetExtension(midExtensionID); payload != nil {
mid = string(payload)
}
if payload := rp.GetExtension(streamIDExtensionID); payload != nil {
rid = string(payload)
}
return
}

View file

@ -0,0 +1,85 @@
package webrtc
// RTPTransceiverDirection indicates the direction of the RTPTransceiver.
type RTPTransceiverDirection int
const (
// RTPTransceiverDirectionSendrecv indicates the RTPSender will offer
// to send RTP and RTPReceiver the will offer to receive RTP.
RTPTransceiverDirectionSendrecv RTPTransceiverDirection = iota + 1
// RTPTransceiverDirectionSendonly indicates the RTPSender will offer
// to send RTP.
RTPTransceiverDirectionSendonly
// RTPTransceiverDirectionRecvonly indicates the RTPReceiver the will
// offer to receive RTP.
RTPTransceiverDirectionRecvonly
// RTPTransceiverDirectionInactive indicates the RTPSender won't offer
// to send RTP and RTPReceiver the won't offer to receive RTP.
RTPTransceiverDirectionInactive
)
// This is done this way because of a linter.
const (
rtpTransceiverDirectionSendrecvStr = "sendrecv"
rtpTransceiverDirectionSendonlyStr = "sendonly"
rtpTransceiverDirectionRecvonlyStr = "recvonly"
rtpTransceiverDirectionInactiveStr = "inactive"
)
// NewRTPTransceiverDirection defines a procedure for creating a new
// RTPTransceiverDirection from a raw string naming the transceiver direction.
func NewRTPTransceiverDirection(raw string) RTPTransceiverDirection {
switch raw {
case rtpTransceiverDirectionSendrecvStr:
return RTPTransceiverDirectionSendrecv
case rtpTransceiverDirectionSendonlyStr:
return RTPTransceiverDirectionSendonly
case rtpTransceiverDirectionRecvonlyStr:
return RTPTransceiverDirectionRecvonly
case rtpTransceiverDirectionInactiveStr:
return RTPTransceiverDirectionInactive
default:
return RTPTransceiverDirection(Unknown)
}
}
func (t RTPTransceiverDirection) String() string {
switch t {
case RTPTransceiverDirectionSendrecv:
return rtpTransceiverDirectionSendrecvStr
case RTPTransceiverDirectionSendonly:
return rtpTransceiverDirectionSendonlyStr
case RTPTransceiverDirectionRecvonly:
return rtpTransceiverDirectionRecvonlyStr
case RTPTransceiverDirectionInactive:
return rtpTransceiverDirectionInactiveStr
default:
return ErrUnknownType.Error()
}
}
// Revers indicate the opposite direction
func (t RTPTransceiverDirection) Revers() RTPTransceiverDirection {
switch t {
case RTPTransceiverDirectionSendonly:
return RTPTransceiverDirectionRecvonly
case RTPTransceiverDirectionRecvonly:
return RTPTransceiverDirectionSendonly
default:
return t
}
}
func haveRTPTransceiverDirectionIntersection(haystack []RTPTransceiverDirection, needle []RTPTransceiverDirection) bool {
for _, n := range needle {
for _, h := range haystack {
if n == h {
return true
}
}
}
return false
}

View file

@ -0,0 +1,12 @@
package webrtc
// RTPTransceiverInit dictionary is used when calling the WebRTC function addTransceiver() to provide configuration options for the new transceiver.
type RTPTransceiverInit struct {
Direction RTPTransceiverDirection
SendEncodings []RTPEncodingParameters
// Streams []*Track
}
// RtpTransceiverInit is a temporary mapping while we fix case sensitivity
// Deprecated: Use RTPTransceiverInit instead
type RtpTransceiverInit = RTPTransceiverInit //nolint: stylecheck,golint

View file

@ -0,0 +1,6 @@
package webrtc
// SCTPCapabilities indicates the capabilities of the SCTPTransport.
type SCTPCapabilities struct {
MaxMessageSize uint32 `json:"maxMessageSize"`
}

View file

@ -0,0 +1,379 @@
// +build !js
package webrtc
import (
"io"
"math"
"sync"
"time"
"github.com/pion/datachannel"
"github.com/pion/logging"
"github.com/pion/sctp"
"github.com/pion/webrtc/v3/pkg/rtcerr"
)
const sctpMaxChannels = uint16(65535)
// SCTPTransport provides details about the SCTP transport.
type SCTPTransport struct {
lock sync.RWMutex
dtlsTransport *DTLSTransport
// State represents the current state of the SCTP transport.
state SCTPTransportState
// SCTPTransportState doesn't have an enum to distinguish between New/Connecting
// so we need a dedicated field
isStarted bool
// MaxMessageSize represents the maximum size of data that can be passed to
// DataChannel's send() method.
maxMessageSize float64
// MaxChannels represents the maximum amount of DataChannel's that can
// be used simultaneously.
maxChannels *uint16
// OnStateChange func()
onErrorHandler func(error)
association *sctp.Association
onDataChannelHandler func(*DataChannel)
onDataChannelOpenedHandler func(*DataChannel)
// DataChannels
dataChannels []*DataChannel
dataChannelsOpened uint32
dataChannelsRequested uint32
dataChannelsAccepted uint32
api *API
log logging.LeveledLogger
}
// NewSCTPTransport creates a new SCTPTransport.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
func (api *API) NewSCTPTransport(dtls *DTLSTransport) *SCTPTransport {
res := &SCTPTransport{
dtlsTransport: dtls,
state: SCTPTransportStateConnecting,
api: api,
log: api.settingEngine.LoggerFactory.NewLogger("ortc"),
}
res.updateMessageSize()
res.updateMaxChannels()
return res
}
// Transport returns the DTLSTransport instance the SCTPTransport is sending over.
func (r *SCTPTransport) Transport() *DTLSTransport {
r.lock.RLock()
defer r.lock.RUnlock()
return r.dtlsTransport
}
// GetCapabilities returns the SCTPCapabilities of the SCTPTransport.
func (r *SCTPTransport) GetCapabilities() SCTPCapabilities {
return SCTPCapabilities{
MaxMessageSize: 0,
}
}
// Start the SCTPTransport. Since both local and remote parties must mutually
// create an SCTPTransport, SCTP SO (Simultaneous Open) is used to establish
// a connection over SCTP.
func (r *SCTPTransport) Start(remoteCaps SCTPCapabilities) error {
if r.isStarted {
return nil
}
r.isStarted = true
if err := r.ensureDTLS(); err != nil {
return err
}
sctpAssociation, err := sctp.Client(sctp.Config{
NetConn: r.Transport().conn,
LoggerFactory: r.api.settingEngine.LoggerFactory,
})
if err != nil {
return err
}
r.lock.Lock()
defer r.lock.Unlock()
r.association = sctpAssociation
r.state = SCTPTransportStateConnected
go r.acceptDataChannels(sctpAssociation)
return nil
}
// Stop stops the SCTPTransport
func (r *SCTPTransport) Stop() error {
r.lock.Lock()
defer r.lock.Unlock()
if r.association == nil {
return nil
}
err := r.association.Close()
if err != nil {
return err
}
r.association = nil
r.state = SCTPTransportStateClosed
return nil
}
func (r *SCTPTransport) ensureDTLS() error {
dtlsTransport := r.Transport()
if dtlsTransport == nil || dtlsTransport.conn == nil {
return errSCTPTransportDTLS
}
return nil
}
func (r *SCTPTransport) acceptDataChannels(a *sctp.Association) {
for {
dc, err := datachannel.Accept(a, &datachannel.Config{
LoggerFactory: r.api.settingEngine.LoggerFactory,
})
if err != nil {
if err != io.EOF {
r.log.Errorf("Failed to accept data channel: %v", err)
r.onError(err)
}
return
}
var (
maxRetransmits *uint16
maxPacketLifeTime *uint16
)
val := uint16(dc.Config.ReliabilityParameter)
ordered := true
switch dc.Config.ChannelType {
case datachannel.ChannelTypeReliable:
ordered = true
case datachannel.ChannelTypeReliableUnordered:
ordered = false
case datachannel.ChannelTypePartialReliableRexmit:
ordered = true
maxRetransmits = &val
case datachannel.ChannelTypePartialReliableRexmitUnordered:
ordered = false
maxRetransmits = &val
case datachannel.ChannelTypePartialReliableTimed:
ordered = true
maxPacketLifeTime = &val
case datachannel.ChannelTypePartialReliableTimedUnordered:
ordered = false
maxPacketLifeTime = &val
default:
}
sid := dc.StreamIdentifier()
rtcDC, err := r.api.newDataChannel(&DataChannelParameters{
ID: &sid,
Label: dc.Config.Label,
Protocol: dc.Config.Protocol,
Negotiated: dc.Config.Negotiated,
Ordered: ordered,
MaxPacketLifeTime: maxPacketLifeTime,
MaxRetransmits: maxRetransmits,
}, r.api.settingEngine.LoggerFactory.NewLogger("ortc"))
if err != nil {
r.log.Errorf("Failed to accept data channel: %v", err)
r.onError(err)
return
}
<-r.onDataChannel(rtcDC)
rtcDC.handleOpen(dc)
r.lock.Lock()
r.dataChannelsOpened++
handler := r.onDataChannelOpenedHandler
r.lock.Unlock()
if handler != nil {
handler(rtcDC)
}
}
}
// OnError sets an event handler which is invoked when
// the SCTP connection error occurs.
func (r *SCTPTransport) OnError(f func(err error)) {
r.lock.Lock()
defer r.lock.Unlock()
r.onErrorHandler = f
}
func (r *SCTPTransport) onError(err error) {
r.lock.RLock()
handler := r.onErrorHandler
r.lock.RUnlock()
if handler != nil {
go handler(err)
}
}
// OnDataChannel sets an event handler which is invoked when a data
// channel message arrives from a remote peer.
func (r *SCTPTransport) OnDataChannel(f func(*DataChannel)) {
r.lock.Lock()
defer r.lock.Unlock()
r.onDataChannelHandler = f
}
// OnDataChannelOpened sets an event handler which is invoked when a data
// channel is opened
func (r *SCTPTransport) OnDataChannelOpened(f func(*DataChannel)) {
r.lock.Lock()
defer r.lock.Unlock()
r.onDataChannelOpenedHandler = f
}
func (r *SCTPTransport) onDataChannel(dc *DataChannel) (done chan struct{}) {
r.lock.Lock()
r.dataChannels = append(r.dataChannels, dc)
r.dataChannelsAccepted++
handler := r.onDataChannelHandler
r.lock.Unlock()
done = make(chan struct{})
if handler == nil || dc == nil {
close(done)
return
}
// Run this synchronously to allow setup done in onDataChannelFn()
// to complete before datachannel event handlers might be called.
go func() {
handler(dc)
close(done)
}()
return
}
func (r *SCTPTransport) updateMessageSize() {
r.lock.Lock()
defer r.lock.Unlock()
var remoteMaxMessageSize float64 = 65536 // pion/webrtc#758
var canSendSize float64 = 65536 // pion/webrtc#758
r.maxMessageSize = r.calcMessageSize(remoteMaxMessageSize, canSendSize)
}
func (r *SCTPTransport) calcMessageSize(remoteMaxMessageSize, canSendSize float64) float64 {
switch {
case remoteMaxMessageSize == 0 &&
canSendSize == 0:
return math.Inf(1)
case remoteMaxMessageSize == 0:
return canSendSize
case canSendSize == 0:
return remoteMaxMessageSize
case canSendSize > remoteMaxMessageSize:
return remoteMaxMessageSize
default:
return canSendSize
}
}
func (r *SCTPTransport) updateMaxChannels() {
val := sctpMaxChannels
r.maxChannels = &val
}
// MaxChannels is the maximum number of RTCDataChannels that can be open simultaneously.
func (r *SCTPTransport) MaxChannels() uint16 {
r.lock.Lock()
defer r.lock.Unlock()
if r.maxChannels == nil {
return sctpMaxChannels
}
return *r.maxChannels
}
// State returns the current state of the SCTPTransport
func (r *SCTPTransport) State() SCTPTransportState {
r.lock.RLock()
defer r.lock.RUnlock()
return r.state
}
func (r *SCTPTransport) collectStats(collector *statsReportCollector) {
r.lock.Lock()
association := r.association
r.lock.Unlock()
collector.Collecting()
stats := TransportStats{
Timestamp: statsTimestampFrom(time.Now()),
Type: StatsTypeTransport,
ID: "sctpTransport",
}
if association != nil {
stats.BytesSent = association.BytesSent()
stats.BytesReceived = association.BytesReceived()
}
collector.Collect(stats.ID, stats)
}
func (r *SCTPTransport) generateAndSetDataChannelID(dtlsRole DTLSRole, idOut **uint16) error {
isChannelWithID := func(id uint16) bool {
for _, d := range r.dataChannels {
if d.id != nil && *d.id == id {
return true
}
}
return false
}
var id uint16
if dtlsRole != DTLSRoleClient {
id++
}
max := r.MaxChannels()
r.lock.Lock()
defer r.lock.Unlock()
for ; id < max-1; id += 2 {
if isChannelWithID(id) {
continue
}
*idOut = &id
return nil
}
return &rtcerr.OperationError{Err: ErrMaxDataChannelID}
}

View file

@ -0,0 +1,54 @@
package webrtc
// SCTPTransportState indicates the state of the SCTP transport.
type SCTPTransportState int
const (
// SCTPTransportStateConnecting indicates the SCTPTransport is in the
// process of negotiating an association. This is the initial state of the
// SCTPTransportState when an SCTPTransport is created.
SCTPTransportStateConnecting SCTPTransportState = iota + 1
// SCTPTransportStateConnected indicates the negotiation of an
// association is completed.
SCTPTransportStateConnected
// SCTPTransportStateClosed indicates a SHUTDOWN or ABORT chunk is
// received or when the SCTP association has been closed intentionally,
// such as by closing the peer connection or applying a remote description
// that rejects data or changes the SCTP port.
SCTPTransportStateClosed
)
// This is done this way because of a linter.
const (
sctpTransportStateConnectingStr = "connecting"
sctpTransportStateConnectedStr = "connected"
sctpTransportStateClosedStr = "closed"
)
func newSCTPTransportState(raw string) SCTPTransportState {
switch raw {
case sctpTransportStateConnectingStr:
return SCTPTransportStateConnecting
case sctpTransportStateConnectedStr:
return SCTPTransportStateConnected
case sctpTransportStateClosedStr:
return SCTPTransportStateClosed
default:
return SCTPTransportState(Unknown)
}
}
func (s SCTPTransportState) String() string {
switch s {
case SCTPTransportStateConnecting:
return sctpTransportStateConnectingStr
case SCTPTransportStateConnected:
return sctpTransportStateConnectedStr
case SCTPTransportStateClosed:
return sctpTransportStateClosedStr
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,643 @@
// +build !js
package webrtc
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/pion/ice/v2"
"github.com/pion/logging"
"github.com/pion/sdp/v3"
)
// trackDetails represents any media source that can be represented in a SDP
// This isn't keyed by SSRC because it also needs to support rid based sources
type trackDetails struct {
mid string
kind RTPCodecType
streamID string
id string
ssrc SSRC
rids []string
}
func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails {
for i := range trackDetails {
if trackDetails[i].ssrc == ssrc {
return &trackDetails[i]
}
}
return nil
}
func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails {
filtered := []trackDetails{}
for i := range incomingTracks {
if incomingTracks[i].ssrc != ssrc {
filtered = append(filtered, incomingTracks[i])
}
}
return filtered
}
// extract all trackDetails from an SDP.
func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) []trackDetails { // nolint:gocognit
incomingTracks := []trackDetails{}
rtxRepairFlows := map[uint32]bool{}
for _, media := range s.MediaDescriptions {
// Plan B can have multiple tracks in a signle media section
streamID := ""
trackID := ""
// If media section is recvonly or inactive skip
if _, ok := media.Attribute(sdp.AttrKeyRecvOnly); ok {
continue
} else if _, ok := media.Attribute(sdp.AttrKeyInactive); ok {
continue
}
midValue := getMidValue(media)
if midValue == "" {
continue
}
codecType := NewRTPCodecType(media.MediaName.Media)
if codecType == 0 {
continue
}
for _, attr := range media.Attributes {
switch attr.Key {
case sdp.AttrKeySSRCGroup:
split := strings.Split(attr.Value, " ")
if split[0] == sdp.SemanticTokenFlowIdentification {
// Add rtx ssrcs to blacklist, to avoid adding them as tracks
// Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section
// as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first
// (2231627014) as specified in RFC5576
if len(split) == 3 {
_, err := strconv.ParseUint(split[1], 10, 32)
if err != nil {
log.Warnf("Failed to parse SSRC: %v", err)
continue
}
rtxRepairFlow, err := strconv.ParseUint(split[2], 10, 32)
if err != nil {
log.Warnf("Failed to parse SSRC: %v", err)
continue
}
rtxRepairFlows[uint32(rtxRepairFlow)] = true
incomingTracks = filterTrackWithSSRC(incomingTracks, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before
}
}
// Handle `a=msid:<stream_id> <track_label>` for Unified plan. The first value is the same as MediaStream.id
// in the browser and can be used to figure out which tracks belong to the same stream. The browser should
// figure this out automatically when an ontrack event is emitted on RTCPeerConnection.
case sdp.AttrKeyMsid:
split := strings.Split(attr.Value, " ")
if len(split) == 2 {
streamID = split[0]
trackID = split[1]
}
case sdp.AttrKeySSRC:
split := strings.Split(attr.Value, " ")
ssrc, err := strconv.ParseUint(split[0], 10, 32)
if err != nil {
log.Warnf("Failed to parse SSRC: %v", err)
continue
}
if rtxRepairFlow := rtxRepairFlows[uint32(ssrc)]; rtxRepairFlow {
continue // This ssrc is a RTX repair flow, ignore
}
if len(split) == 3 && strings.HasPrefix(split[1], "msid:") {
streamID = split[1][len("msid:"):]
trackID = split[2]
}
isNewTrack := true
trackDetails := &trackDetails{}
for i := range incomingTracks {
if incomingTracks[i].ssrc == SSRC(ssrc) {
trackDetails = &incomingTracks[i]
isNewTrack = false
}
}
trackDetails.mid = midValue
trackDetails.kind = codecType
trackDetails.streamID = streamID
trackDetails.id = trackID
trackDetails.ssrc = SSRC(ssrc)
if isNewTrack {
incomingTracks = append(incomingTracks, *trackDetails)
}
}
}
if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" {
newTrack := trackDetails{
mid: midValue,
kind: codecType,
streamID: streamID,
id: trackID,
rids: []string{},
}
for rid := range rids {
newTrack.rids = append(newTrack.rids, rid)
}
incomingTracks = append(incomingTracks, newTrack)
}
}
return incomingTracks
}
func getRids(media *sdp.MediaDescription) map[string]string {
rids := map[string]string{}
for _, attr := range media.Attributes {
if attr.Key == "rid" {
split := strings.Split(attr.Value, " ")
rids[split[0]] = attr.Value
}
}
return rids
}
func addCandidatesToMediaDescriptions(candidates []ICECandidate, m *sdp.MediaDescription, iceGatheringState ICEGatheringState) error {
appendCandidateIfNew := func(c ice.Candidate, attributes []sdp.Attribute) {
marshaled := c.Marshal()
for _, a := range attributes {
if marshaled == a.Value {
return
}
}
m.WithValueAttribute("candidate", marshaled)
}
for _, c := range candidates {
candidate, err := c.toICE()
if err != nil {
return err
}
candidate.SetComponent(1)
appendCandidateIfNew(candidate, m.Attributes)
candidate.SetComponent(2)
appendCandidateIfNew(candidate, m.Attributes)
}
if iceGatheringState != ICEGatheringStateComplete {
return nil
}
for _, a := range m.Attributes {
if a.Key == "end-of-candidates" {
return nil
}
}
m.WithPropertyAttribute("end-of-candidates")
return nil
}
func addDataMediaSection(d *sdp.SessionDescription, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState) error {
media := (&sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: mediaSectionApplication,
Port: sdp.RangedPort{Value: 9},
Protos: []string{"UDP", "DTLS", "SCTP"},
Formats: []string{"webrtc-datachannel"},
},
ConnectionInformation: &sdp.ConnectionInformation{
NetworkType: "IN",
AddressType: "IP4",
Address: &sdp.Address{
Address: "0.0.0.0",
},
},
}).
WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()).
WithValueAttribute(sdp.AttrKeyMID, midValue).
WithPropertyAttribute(RTPTransceiverDirectionSendrecv.String()).
WithPropertyAttribute("sctp-port:5000").
WithICECredentials(iceParams.UsernameFragment, iceParams.Password)
for _, f := range dtlsFingerprints {
media = media.WithFingerprint(f.Algorithm, strings.ToUpper(f.Value))
}
if shouldAddCandidates {
if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil {
return err
}
}
d.WithMedia(media)
return nil
}
func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGatherer, iceGatheringState ICEGatheringState) *SessionDescription {
if sessionDescription == nil || i == nil {
return sessionDescription
}
candidates, err := i.GetLocalCandidates()
if err != nil {
return sessionDescription
}
parsed := sessionDescription.parsed
if len(parsed.MediaDescriptions) > 0 {
m := parsed.MediaDescriptions[0]
if err = addCandidatesToMediaDescriptions(candidates, m, iceGatheringState); err != nil {
return sessionDescription
}
}
sdp, err := parsed.Marshal()
if err != nil {
return sessionDescription
}
return &SessionDescription{
SDP: string(sdp),
Type: sessionDescription.Type,
}
}
func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, mediaSection mediaSection) (bool, error) {
transceivers := mediaSection.transceivers
if len(transceivers) < 1 {
return false, errSDPZeroTransceivers
}
// Use the first transceiver to generate the section attributes
t := transceivers[0]
media := sdp.NewJSEPMediaDescription(t.kind.String(), []string{}).
WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()).
WithValueAttribute(sdp.AttrKeyMID, midValue).
WithICECredentials(iceParams.UsernameFragment, iceParams.Password).
WithPropertyAttribute(sdp.AttrKeyRTCPMux).
WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
codecs := mediaEngine.getCodecsByKind(t.kind)
for _, codec := range codecs {
name := strings.TrimPrefix(codec.MimeType, "audio/")
name = strings.TrimPrefix(name, "video/")
media.WithCodec(uint8(codec.PayloadType), name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)
for _, feedback := range codec.RTPCodecCapability.RTCPFeedback {
media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter))
}
}
if len(codecs) == 0 {
// Explicitly reject track if we don't have the codec
d.WithMedia(&sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: t.kind.String(),
Port: sdp.RangedPort{Value: 0},
Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
Formats: []string{"0"},
},
})
return false, nil
}
directions := []RTPTransceiverDirection{}
if t.Sender() != nil {
directions = append(directions, RTPTransceiverDirectionSendonly)
}
if t.Receiver() != nil {
directions = append(directions, RTPTransceiverDirectionRecvonly)
}
parameters := mediaEngine.getRTPParametersByKind(t.kind, directions)
for _, rtpExtension := range parameters.HeaderExtensions {
extURL, err := url.Parse(rtpExtension.URI)
if err != nil {
return false, err
}
media.WithExtMap(sdp.ExtMap{Value: rtpExtension.ID, URI: extURL})
}
if len(mediaSection.ridMap) > 0 {
recvRids := make([]string, 0, len(mediaSection.ridMap))
for rid := range mediaSection.ridMap {
media.WithValueAttribute("rid", rid+" recv")
recvRids = append(recvRids, rid)
}
// Simulcast
media.WithValueAttribute("simulcast", "recv "+strings.Join(recvRids, ";"))
}
for _, mt := range transceivers {
if mt.Sender() != nil && mt.Sender().Track() != nil {
track := mt.Sender().Track()
media = media.WithMediaSource(uint32(mt.Sender().ssrc), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID())
if !isPlanB {
media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID())
break
}
}
}
media = media.WithPropertyAttribute(t.Direction().String())
for _, fingerprint := range dtlsFingerprints {
media = media.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value))
}
if shouldAddCandidates {
if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil {
return false, err
}
}
d.WithMedia(media)
return true, nil
}
type mediaSection struct {
id string
transceivers []*RTPTransceiver
data bool
ridMap map[string]string
}
// populateSDP serializes a PeerConnections state into an SDP
func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) {
var err error
mediaDtlsFingerprints := []DTLSFingerprint{}
if mediaDescriptionFingerprint {
mediaDtlsFingerprints = dtlsFingerprints
}
bundleValue := "BUNDLE"
bundleCount := 0
appendBundle := func(midValue string) {
bundleValue += " " + midValue
bundleCount++
}
for i, m := range mediaSections {
if m.data && len(m.transceivers) != 0 {
return nil, errSDPMediaSectionMediaDataChanInvalid
} else if !isPlanB && len(m.transceivers) > 1 {
return nil, errSDPMediaSectionMultipleTrackInvalid
}
shouldAddID := true
shouldAddCanidates := i == 0
if m.data {
if err = addDataMediaSection(d, shouldAddCanidates, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState); err != nil {
return nil, err
}
} else {
shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCanidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m)
if err != nil {
return nil, err
}
}
if shouldAddID {
appendBundle(m.id)
}
}
if !mediaDescriptionFingerprint {
for _, fingerprint := range dtlsFingerprints {
d.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value))
}
}
if isICELite {
// RFC 5245 S15.3
d = d.WithValueAttribute(sdp.AttrKeyICELite, sdp.AttrKeyICELite)
}
return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil
}
func getMidValue(media *sdp.MediaDescription) string {
for _, attr := range media.Attributes {
if attr.Key == "mid" {
return attr.Value
}
}
return ""
}
func descriptionIsPlanB(desc *SessionDescription) bool {
if desc == nil || desc.parsed == nil {
return false
}
detectionRegex := regexp.MustCompile(`(?i)^(audio|video|data)$`)
for _, media := range desc.parsed.MediaDescriptions {
if len(detectionRegex.FindStringSubmatch(getMidValue(media))) == 2 {
return true
}
}
return false
}
func getPeerDirection(media *sdp.MediaDescription) RTPTransceiverDirection {
for _, a := range media.Attributes {
if direction := NewRTPTransceiverDirection(a.Key); direction != RTPTransceiverDirection(Unknown) {
return direction
}
}
return RTPTransceiverDirection(Unknown)
}
func extractFingerprint(desc *sdp.SessionDescription) (string, string, error) {
fingerprints := []string{}
if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint {
fingerprints = append(fingerprints, fingerprint)
}
for _, m := range desc.MediaDescriptions {
if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint {
fingerprints = append(fingerprints, fingerprint)
}
}
if len(fingerprints) < 1 {
return "", "", ErrSessionDescriptionNoFingerprint
}
for _, m := range fingerprints {
if m != fingerprints[0] {
return "", "", ErrSessionDescriptionConflictingFingerprints
}
}
parts := strings.Split(fingerprints[0], " ")
if len(parts) != 2 {
return "", "", ErrSessionDescriptionInvalidFingerprint
}
return parts[1], parts[0], nil
}
func extractICEDetails(desc *sdp.SessionDescription) (string, string, []ICECandidate, error) {
candidates := []ICECandidate{}
remotePwds := []string{}
remoteUfrags := []string{}
if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag {
remoteUfrags = append(remoteUfrags, ufrag)
}
if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd {
remotePwds = append(remotePwds, pwd)
}
for _, m := range desc.MediaDescriptions {
if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag {
remoteUfrags = append(remoteUfrags, ufrag)
}
if pwd, havePwd := m.Attribute("ice-pwd"); havePwd {
remotePwds = append(remotePwds, pwd)
}
for _, a := range m.Attributes {
if a.IsICECandidate() {
c, err := ice.UnmarshalCandidate(a.Value)
if err != nil {
return "", "", nil, err
}
candidate, err := newICECandidateFromICE(c)
if err != nil {
return "", "", nil, err
}
candidates = append(candidates, candidate)
}
}
}
if len(remoteUfrags) == 0 {
return "", "", nil, ErrSessionDescriptionMissingIceUfrag
} else if len(remotePwds) == 0 {
return "", "", nil, ErrSessionDescriptionMissingIcePwd
}
for _, m := range remoteUfrags {
if m != remoteUfrags[0] {
return "", "", nil, ErrSessionDescriptionConflictingIceUfrag
}
}
for _, m := range remotePwds {
if m != remotePwds[0] {
return "", "", nil, ErrSessionDescriptionConflictingIcePwd
}
}
return remoteUfrags[0], remotePwds[0], candidates, nil
}
func haveApplicationMediaSection(desc *sdp.SessionDescription) bool {
for _, m := range desc.MediaDescriptions {
if m.MediaName.Media == mediaSectionApplication {
return true
}
}
return false
}
func getByMid(searchMid string, desc *SessionDescription) *sdp.MediaDescription {
for _, m := range desc.parsed.MediaDescriptions {
if mid, ok := m.Attribute(sdp.AttrKeyMID); ok && mid == searchMid {
return m
}
}
return nil
}
// haveDataChannel return MediaDescription with MediaName equal application
func haveDataChannel(desc *SessionDescription) *sdp.MediaDescription {
for _, d := range desc.parsed.MediaDescriptions {
if d.MediaName.Media == mediaSectionApplication {
return d
}
}
return nil
}
func codecsFromMediaDescription(m *sdp.MediaDescription) (out []RTPCodecParameters, err error) {
s := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{m},
}
for _, payloadStr := range m.MediaName.Formats {
payloadType, err := strconv.Atoi(payloadStr)
if err != nil {
return nil, err
}
codec, err := s.GetCodecForPayloadType(uint8(payloadType))
if err != nil {
if payloadType == 0 {
continue
}
return nil, err
}
channels := uint16(0)
val, err := strconv.Atoi(codec.EncodingParameters)
if err == nil {
channels = uint16(val)
}
feedback := []RTCPFeedback{}
for _, raw := range codec.RTCPFeedback {
split := strings.Split(raw, " ")
entry := RTCPFeedback{Type: split[0]}
if len(split) == 2 {
entry.Parameter = split[1]
}
feedback = append(feedback, entry)
}
out = append(out, RTPCodecParameters{
RTPCodecCapability: RTPCodecCapability{m.MediaName.Media + "/" + codec.Name, codec.ClockRate, channels, codec.Fmtp, feedback},
PayloadType: PayloadType(payloadType),
})
}
return out, nil
}
func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int, error) {
out := map[string]int{}
for _, a := range m.Attributes {
if a.Key == sdp.AttrKeyExtMap {
e := sdp.ExtMap{}
if err := e.Unmarshal(a.String()); err != nil {
return nil, err
}
out[e.URI.String()] = e.Value
}
}
return out, nil
}

View file

@ -0,0 +1,74 @@
package webrtc
import (
"encoding/json"
)
// SDPSemantics determines which style of SDP offers and answers
// can be used
type SDPSemantics int
const (
// SDPSemanticsUnifiedPlan uses unified-plan offers and answers
// (the default in Chrome since M72)
// https://tools.ietf.org/html/draft-roach-mmusic-unified-plan-00
SDPSemanticsUnifiedPlan SDPSemantics = iota
// SDPSemanticsPlanB uses plan-b offers and answers
// NB: This format should be considered deprecated
// https://tools.ietf.org/html/draft-uberti-rtcweb-plan-00
SDPSemanticsPlanB
// SDPSemanticsUnifiedPlanWithFallback prefers unified-plan
// offers and answers, but will respond to a plan-b offer
// with a plan-b answer
SDPSemanticsUnifiedPlanWithFallback
)
const (
sdpSemanticsUnifiedPlanWithFallback = "unified-plan-with-fallback"
sdpSemanticsUnifiedPlan = "unified-plan"
sdpSemanticsPlanB = "plan-b"
)
func newSDPSemantics(raw string) SDPSemantics {
switch raw {
case sdpSemanticsUnifiedPlan:
return SDPSemanticsUnifiedPlan
case sdpSemanticsPlanB:
return SDPSemanticsPlanB
case sdpSemanticsUnifiedPlanWithFallback:
return SDPSemanticsUnifiedPlanWithFallback
default:
return SDPSemantics(Unknown)
}
}
func (s SDPSemantics) String() string {
switch s {
case SDPSemanticsUnifiedPlanWithFallback:
return sdpSemanticsUnifiedPlanWithFallback
case SDPSemanticsUnifiedPlan:
return sdpSemanticsUnifiedPlan
case SDPSemanticsPlanB:
return sdpSemanticsPlanB
default:
return ErrUnknownType.Error()
}
}
// UnmarshalJSON parses the JSON-encoded data and stores the result
func (s *SDPSemantics) UnmarshalJSON(b []byte) error {
var val string
if err := json.Unmarshal(b, &val); err != nil {
return err
}
*s = newSDPSemantics(val)
return nil
}
// MarshalJSON returns the JSON encoding
func (s SDPSemantics) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

View file

@ -0,0 +1,100 @@
package webrtc
import (
"encoding/json"
"strings"
)
// SDPType describes the type of an SessionDescription.
type SDPType int
const (
// SDPTypeOffer indicates that a description MUST be treated as an SDP
// offer.
SDPTypeOffer SDPType = iota + 1
// SDPTypePranswer indicates that a description MUST be treated as an
// SDP answer, but not a final answer. A description used as an SDP
// pranswer may be applied as a response to an SDP offer, or an update to
// a previously sent SDP pranswer.
SDPTypePranswer
// SDPTypeAnswer indicates that a description MUST be treated as an SDP
// final answer, and the offer-answer exchange MUST be considered complete.
// A description used as an SDP answer may be applied as a response to an
// SDP offer or as an update to a previously sent SDP pranswer.
SDPTypeAnswer
// SDPTypeRollback indicates that a description MUST be treated as
// canceling the current SDP negotiation and moving the SDP offer and
// answer back to what it was in the previous stable state. Note the
// local or remote SDP descriptions in the previous stable state could be
// null if there has not yet been a successful offer-answer negotiation.
SDPTypeRollback
)
// This is done this way because of a linter.
const (
sdpTypeOfferStr = "offer"
sdpTypePranswerStr = "pranswer"
sdpTypeAnswerStr = "answer"
sdpTypeRollbackStr = "rollback"
)
// NewSDPType creates an SDPType from a string
func NewSDPType(raw string) SDPType {
switch raw {
case sdpTypeOfferStr:
return SDPTypeOffer
case sdpTypePranswerStr:
return SDPTypePranswer
case sdpTypeAnswerStr:
return SDPTypeAnswer
case sdpTypeRollbackStr:
return SDPTypeRollback
default:
return SDPType(Unknown)
}
}
func (t SDPType) String() string {
switch t {
case SDPTypeOffer:
return sdpTypeOfferStr
case SDPTypePranswer:
return sdpTypePranswerStr
case SDPTypeAnswer:
return sdpTypeAnswerStr
case SDPTypeRollback:
return sdpTypeRollbackStr
default:
return ErrUnknownType.Error()
}
}
// MarshalJSON enables JSON marshaling of a SDPType
func (t SDPType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
// UnmarshalJSON enables JSON unmarshaling of a SDPType
func (t *SDPType) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
switch strings.ToLower(s) {
default:
return ErrUnknownType
case "offer":
*t = SDPTypeOffer
case "pranswer":
*t = SDPTypePranswer
case "answer":
*t = SDPTypeAnswer
case "rollback":
*t = SDPTypeRollback
}
return nil
}

Some files were not shown because too many files have changed in this diff Show more