diff --git a/.github/assert-contributors.sh b/.github/generate-authors.sh
similarity index 58%
rename from .github/assert-contributors.sh
rename to .github/generate-authors.sh
index 12e6afea145..182e4f5e738 100755
--- a/.github/assert-contributors.sh
+++ b/.github/generate-authors.sh
@@ -12,6 +12,7 @@
set -e
SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+AUTHORS_PATH="$GITHUB_WORKSPACE/AUTHORS.txt"
if [ -f ${SCRIPT_PATH}/.ci.conf ]
then
@@ -21,18 +22,18 @@ fi
#
# DO NOT EDIT THIS
#
-EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot')
+EXCLUDED_CONTRIBUTORS+=('John R. Bradley' 'renovate[bot]' 'Renovate Bot' 'Pion Bot' 'pionbot')
# If you want to exclude a name from all repositories, send a PR to
# https://github.com/pion/.goassets instead of this repository.
# If you want to exclude a name only from this repository,
# add EXCLUDED_CONTRIBUTORS=('name') to .github/.ci.conf
-MISSING_CONTRIBUTORS=()
+CONTRIBUTORS=()
shouldBeIncluded () {
for i in "${EXCLUDED_CONTRIBUTORS[@]}"
do
- if [ "$i" == "$1" ] ; then
+ if [[ $1 =~ "$i" ]]; then
return 1
fi
done
@@ -41,21 +42,25 @@ shouldBeIncluded () {
IFS=$'\n' #Only split on newline
-for contributor in $(git log --format='%aN' | sort -u)
+for contributor in $(git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf)
do
if shouldBeIncluded $contributor; then
- if ! grep -q "$contributor" "$SCRIPT_PATH/../README.md"; then
- MISSING_CONTRIBUTORS+=("$contributor")
- fi
+ CONTRIBUTORS+=("$contributor")
fi
done
unset IFS
-if [ ${#MISSING_CONTRIBUTORS[@]} -ne 0 ]; then
- echo "Please add the following contributors to the README"
- for i in "${MISSING_CONTRIBUTORS[@]}"
+if [ ${#CONTRIBUTORS[@]} -ne 0 ]; then
+ cat >$AUTHORS_PATH <<-'EOH'
+# Thank you to everyone that made Pion possible. If you are interested in contributing
+# we would love to have you https://github.com/pion/webrtc/wiki/Contributing
+#
+# This file is auto generated, using git to list all individuals contributors.
+# see `.github/generate-authors.sh` for the scripting
+EOH
+ for i in "${CONTRIBUTORS[@]}"
do
- echo "$i"
+ echo "$i" >> $AUTHORS_PATH
done
- exit 1
+ exit 0
fi
diff --git a/.github/hooks/pre-push.sh b/.github/hooks/pre-push.sh
index 7cb23653d48..bfe65bc504a 100755
--- a/.github/hooks/pre-push.sh
+++ b/.github/hooks/pre-push.sh
@@ -8,6 +8,6 @@
set -e
-.github/assert-contributors.sh
+.github/generate-authors.sh
exit 0
diff --git a/.github/workflows/browser-e2e.yaml b/.github/workflows/browser-e2e.yaml
index 33016d4ea8c..b45f63710c1 100644
--- a/.github/workflows/browser-e2e.yaml
+++ b/.github/workflows/browser-e2e.yaml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: test
run: |
docker build -t pion-webrtc-e2e -f e2e/Dockerfile .
diff --git a/.github/workflows/generate-authors.yml b/.github/workflows/generate-authors.yml
new file mode 100644
index 00000000000..9a80a48d247
--- /dev/null
+++ b/.github/workflows/generate-authors.yml
@@ -0,0 +1,73 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
+name: generate-authors
+
+on:
+ pull_request:
+
+jobs:
+ checksecret:
+ runs-on: ubuntu-latest
+ outputs:
+ is_PIONBOT_PRIVATE_KEY_set: ${{ steps.checksecret_job.outputs.is_PIONBOT_PRIVATE_KEY_set }}
+ steps:
+ - id: checksecret_job
+ env:
+ PIONBOT_PRIVATE_KEY: ${{ secrets.PIONBOT_PRIVATE_KEY }}
+ run: |
+ echo "is_PIONBOT_PRIVATE_KEY_set: ${{ env.PIONBOT_PRIVATE_KEY != '' }}"
+ echo "::set-output name=is_PIONBOT_PRIVATE_KEY_set::${{ env.PIONBOT_PRIVATE_KEY != '' }}"
+
+ generate-authors:
+ needs: [checksecret]
+ if: needs.checksecret.outputs.is_PIONBOT_PRIVATE_KEY_set == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.head_ref }}
+ fetch-depth: 0
+ token: ${{ secrets.PIONBOT_PRIVATE_KEY }}
+
+ - name: Generate the authors file
+ run: .github/generate-authors.sh
+
+ - name: Add the authors file to git
+ run: git add AUTHORS.txt
+
+ - name: Get last commit message
+ id: last-commit-message
+ run: |
+ COMMIT_MSG=$(git log -1 --pretty=%B)
+ COMMIT_MSG="${COMMIT_MSG//'%'/'%25'}"
+ COMMIT_MSG="${COMMIT_MSG//$'\n'/'%0A'}"
+ COMMIT_MSG="${COMMIT_MSG//$'\r'/'%0D'}"
+ echo "::set-output name=msg::$COMMIT_MSG"
+
+ - name: Get last commit author
+ id: last-commit-author
+ run: |
+ echo "::set-output name=msg::$(git log -1 --pretty='%aN <%ae>')"
+
+ - name: Check if AUTHORS.txt file has changed
+ id: git-status-output
+ run: |
+ echo "::set-output name=msg::$(git status -s | wc -l)"
+
+ - name: Commit and push
+ if: ${{ steps.git-status-output.outputs.msg != '0' }}
+ run: |
+ git config user.email $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\2/')
+ git config user.name $(echo "${{ steps.last-commit-author.outputs.msg }}" | sed 's/\(.\+\) <\(\S\+\)>/\1/')
+ git add AUTHORS.txt
+ git commit --amend --no-edit
+ git push --force https://github.com/${GITHUB_REPOSITORY} $(git symbolic-ref -q --short HEAD)
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 8824c34d9ef..438443f112b 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -1,3 +1,14 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
name: Lint
on:
pull_request:
@@ -12,7 +23,7 @@ jobs:
strategy:
fail-fast: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -22,9 +33,6 @@ jobs:
- name: File names
run: .github/lint-filename.sh
- - name: Contributors
- run: .github/assert-contributors.sh
-
- name: Functions
run: .github/lint-disallowed-functions-in-library.sh
@@ -34,10 +42,10 @@ jobs:
strategy:
fail-fast: false
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: golangci-lint
- uses: golangci/golangci-lint-action@v2
+ uses: golangci/golangci-lint-action@v3
with:
version: v1.31
args: $GOLANGCI_LINT_EXRA_ARGS
diff --git a/.github/workflows/renovate-go-mod-fix.yaml b/.github/workflows/renovate-go-mod-fix.yaml
index 46d2d04c1a8..59918227c74 100644
--- a/.github/workflows/renovate-go-mod-fix.yaml
+++ b/.github/workflows/renovate-go-mod-fix.yaml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
fetch-depth: 2
- name: fix
diff --git a/.github/workflows/standardjs.yaml b/.github/workflows/standardjs.yaml
new file mode 100644
index 00000000000..3211226cd3d
--- /dev/null
+++ b/.github/workflows/standardjs.yaml
@@ -0,0 +1,17 @@
+name: StandardJS
+on:
+ pull_request:
+ types:
+ - opened
+ - edited
+ - synchronize
+jobs:
+ StandardJS:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 12.x
+ - run: npm install standard
+ - run: npx standard
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index a30c38f6bfd..cd788c9bfc2 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,3 +1,14 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+
name: Test
on:
push:
@@ -11,11 +22,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- go: ["1.15", "1.16"]
+ go: ["1.16", "1.17"]
fail-fast: false
name: Go ${{ matrix.go }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: actions/cache@v2
with:
@@ -28,7 +39,7 @@ jobs:
${{ runner.os }}-amd64-go-
- name: Setup Go
- uses: actions/setup-go@v2
+ uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
@@ -39,11 +50,19 @@ jobs:
- name: Run test
run: |
+ TEST_BENCH_OPTION="-bench=."
+ if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
+
go-acc -o cover.out ./... -- \
- -bench=. \
- -v -race
+ ${TEST_BENCH_OPTION} \
+ -v -race
+
+ - name: Run TEST_HOOK
+ run: |
+ if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
+ if [ -n "${TEST_HOOK}" ]; then ${TEST_HOOK}; fi
- - uses: codecov/codecov-action@v1
+ - uses: codecov/codecov-action@v2
with:
file: ./cover.out
name: codecov-umbrella
@@ -54,11 +73,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- go: ["1.15", "1.16"]
+ go: ["1.16", "1.17"]
fail-fast: false
name: Go i386 ${{ matrix.go }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: actions/cache@v2
with:
@@ -73,17 +92,17 @@ jobs:
run: |
mkdir -p $HOME/go/pkg/mod $HOME/.cache
docker run \
- -u $(id -u):$(id -g) \
- -e "GO111MODULE=on" \
- -e "CGO_ENABLED=0" \
- -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
- -v $HOME/go/pkg/mod:/go/pkg/mod \
- -v $HOME/.cache:/.cache \
- -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
- i386/golang:${{matrix.go}}-alpine \
- /usr/local/go/bin/go test \
- ${TEST_EXTRA_ARGS:-} \
- -v ./...
+ -u $(id -u):$(id -g) \
+ -e "GO111MODULE=on" \
+ -e "CGO_ENABLED=0" \
+ -v $GITHUB_WORKSPACE:/go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
+ -v $HOME/go/pkg/mod:/go/pkg/mod \
+ -v $HOME/.cache:/.cache \
+ -w /go/src/github.com/pion/$(basename $GITHUB_WORKSPACE) \
+ i386/golang:${{matrix.go}}-alpine \
+ /usr/local/go/bin/go test \
+ ${TEST_EXTRA_ARGS:-} \
+ -v ./...
test-wasm:
runs-on: ubuntu-latest
@@ -91,12 +110,12 @@ jobs:
fail-fast: false
name: WASM
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Use Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
- node-version: '12.x'
+ node-version: '16.x'
- uses: actions/cache@v2
with:
@@ -110,7 +129,7 @@ jobs:
- name: Download Go
run: curl -sSfL https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz | tar -C ~ -xzf -
env:
- GO_VERSION: 1.16
+ GO_VERSION: 1.17
- name: Set Go Root
run: echo "GOROOT=${HOME}/go" >> $GITHUB_ENV
@@ -119,19 +138,20 @@ jobs:
run: echo "GOPATH=${HOME}/go" >> $GITHUB_ENV
- name: Set Go Path
- run: echo "GO_JS_WASM_EXEC=${PWD}/test-wasm/go_js_wasm_exec" >> $GITHUB_ENV
+ run: echo "GO_JS_WASM_EXEC=${GOROOT}/misc/wasm/go_js_wasm_exec" >> $GITHUB_ENV
- name: Insall NPM modules
run: yarn install
- name: Run Tests
run: |
+ if [ -f .github/.ci.conf ]; then . .github/.ci.conf; fi
GOOS=js GOARCH=wasm $GOPATH/bin/go test \
- -coverprofile=cover.out -covermode=atomic \
- -exec="${GO_JS_WASM_EXEC}" \
- -v ./...
+ -coverprofile=cover.out -covermode=atomic \
+ -exec="${GO_JS_WASM_EXEC}" \
+ -v ./...
- - uses: codecov/codecov-action@v1
+ - uses: codecov/codecov-action@v2
with:
file: ./cover.out
name: codecov-umbrella
diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml
index 03b5189deba..3ab2c35219c 100644
--- a/.github/workflows/tidy-check.yaml
+++ b/.github/workflows/tidy-check.yaml
@@ -23,9 +23,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Setup Go
- uses: actions/setup-go@v2
+ uses: actions/setup-go@v3
- name: check
run: |
go mod download
diff --git a/.gitignore b/.gitignore
index 83db74ba532..f977e748533 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ cover.out
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem
+wasm_exec.js
diff --git a/AUTHORS.txt b/AUTHORS.txt
new file mode 100644
index 00000000000..0ef018685fa
--- /dev/null
+++ b/AUTHORS.txt
@@ -0,0 +1,176 @@
+# Thank you to everyone that made Pion possible. If you are interested in contributing
+# we would love to have you https://github.com/pion/webrtc/wiki/Contributing
+#
+# This file is auto generated, using git to list all individuals contributors.
+# see `.github/generate-authors.sh` for the scripting
+a-wing <1@233.email>
+Aaron France
+Adam Kiss
+adwpc
+aggresss
+akil
+Aleksandr Razumov
+aler9 <46489434+aler9@users.noreply.github.com>
+Alex Browne
+Alex Harford
+AlexWoo(武杰)
+Ali Error
+Andrew N. Shalaev
+Antoine Baché
+Antoine Baché
+Artur Shellunts
+Assad Obaid
+Ato Araki
+Atsushi Watanabe
+backkem
+baiyufei
+Bao Nguyen
+Ben Weitzman
+Benny Daon
+bkim
+Bo Shi
+boks1971
+Brendan Rius
+brian
+Bryan Phelps
+Cameron Elliott
+Cecylia Bocovich
+Cedric Fung
+cgojin
+Chad Retz
+chenkaiC4
+Chris Hiszpanski
+Christopher Fry
+Clayton McCray
+cnderrauber
+cyannuk
+David Hamilton
+David Zhao
+Dean Sheather
+decanus <7621705+decanus@users.noreply.github.com>
+Denis
+digitalix
+donotanswer
+earle
+Egon Elbre
+Eric Daniels
+feixiao
+frank
+Gareth Hayes
+Guilherme
+Hanjun Kim
+Hendrik Hofstadt
+Henry
+Hongchao Ma
+Hugo Arregui
+Hugo Arregui
+Ilya Mayorov
+imalic3
+Ivan Egorov
+JacobZwang <59858341+JacobZwang@users.noreply.github.com>
+Jake B
+Jamie Good
+Jason
+Jeff Tchang
+Jerko Steiner
+jinleileiking
+John Berthels
+John Bradley
+JooYoung
+Jorropo
+juberti
+Juliusz Chroboczek
+Justin Okamoto
+Justin Okamoto
+Kevin Staunton-Lambert
+Kevin Wang
+Konstantin Chugalinskiy
+Konstantin Itskov
+krishna chiatanya
+Kuzmin Vladimir
+lawl
+Len
+Lukas Herman
+Luke
+Luke Curley
+Luke S
+Magnus Wahlstrand
+Markus Tzoe
+Marouane <6729798+nindolabs@users.noreply.github.com>
+Marouane
+Masahiro Nakamura <13937915+tsuu32@users.noreply.github.com>
+Mathis Engelbart
+Max Hawkins
+mchlrhw <4028654+mchlrhw@users.noreply.github.com>
+Michael MacDonald
+Michael MacDonald
+Michiel De Backker <38858977+backkem@users.noreply.github.com>
+Mike Coleman
+Mindgamesnl
+mission-liao
+mxmCherry
+Nam V. Do
+Nick Mykins
+nindolabs <6729798+nindolabs@users.noreply.github.com>
+Norman Rasmussen
+notedit
+o0olele
+obasajujoshua31
+Oleg Kovalov
+opennota
+OrlandoCo
+Pascal Benoit
+pascal-ace <47424881+pascal-ace@users.noreply.github.com>
+Patrick Lange
+Patryk Rogalski
+Pieere Pi
+q191201771 <191201771@qq.com>
+Quentin Renard
+Rafael Viscarra
+rahulnakre
+Raphael Randschau
+Raphael Randschau
+Reese <3253971+figadore@users.noreply.github.com>
+rob-deutsch
+Robert Eperjesi
+Robin Raymond
+Roman Romanenko
+Roman Romanenko
+ronan
+Ryan Shumate
+salmān aljammāz
+Sam Lancia
+Sean DuBois
+Sean DuBois
+Sean DuBois
+Sean DuBois
+Sean Knight
+Sebastian Waisbrot
+Simon Eisenmann
+simonacca-fotokite <47634061+simonacca-fotokite@users.noreply.github.com>
+Simone Gotti
+Slugalisk
+soolaugust
+spaceCh1mp
+Suhas Gaddam
+Suzuki Takeo
+sylba2050
+Tarrence van As
+tarrencev
+Thomas Miller
+Tobias Fridén
+Tomek
+Twometer
+Vicken Simonian
+wattanakorn495
+Will Forcey
+Will Watson
+Woodrow Douglass
+xsbchen
+Yuki Igarashi
+yusuke
+Yutaka Takeda
+ZHENK
+zigazeljko
+Štefan Uram
+박종훈
diff --git a/README.md b/README.md
index 5883ae9e1f2..da1b3d67f3c 100644
--- a/README.md
+++ b/README.md
@@ -21,16 +21,6 @@
-### 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.
@@ -96,8 +86,9 @@ This book is vendor agnostic and will not have any Pion specific information.
* [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
+* [Sender/Receiver Reports](https://github.com/pion/interceptor/tree/master/pkg/report)
+* [Transport Wide Congestion Control Feedback](https://github.com/pion/interceptor/tree/master/pkg/twcc)
+* Bandwidth Estimation is actively being implemented, see [pion/interceptor#25](https://github.com/pion/interceptor/issues/25)
#### Security
* TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 and TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA for DTLS v1.2
@@ -133,149 +124,5 @@ If you need commercial support or don't want to use public methods you can conta
### 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)
-* [Cameron Elliott](https://github.com/cameronelliott)
-* [Pascal Benoit](https://github.com/pascal-ace)
-* [Mats](https://github.com/Mindgamesnl)
-* [donotanswer](https://github.com/f-viktor)
-* [Reese](https://github.com/figadore)
-* [David Zhao](https://github.com/davidzhao)
-* [Nam V. Do](https://github.com/namvdo)
-* [Markus Tzoe](https://github.com/zyxar)
-* [Benny Daon](https://github.com/daonb)
-* [Tomek](https://github.com/trojek)
-* [Jin Gong](https://github.com/cgojin)
-* [yusuke](https://github.com/yusukem99)
-* [Patryk Rogalski](https://github.com/digitalix)
-* [Robin Raymond](https://github.com/robin-raymond)
-
### License
MIT License - see [LICENSE](LICENSE) for full text
diff --git a/api.go b/api.go
index 60ac72809d9..85424df4d9b 100644
--- a/api.go
+++ b/api.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -7,40 +8,36 @@ import (
"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.
+// API allows configuration of a PeerConnection
+// with APIs that are available in the standard. This
+// lets you set custom behavior via the SettingEngine, configure
+// codecs via the MediaEngine and define custom media behaviors via
+// Interceptors.
type API struct {
- settingEngine *SettingEngine
- mediaEngine *MediaEngine
- interceptor interceptor.Interceptor
+ settingEngine *SettingEngine
+ mediaEngine *MediaEngine
+ interceptorRegistry *interceptor.Registry
+
+ interceptor interceptor.Interceptor // Generated per PeerConnection
}
// NewAPI Creates a new API object for keeping semi-global settings to WebRTC objects
func NewAPI(options ...func(*API)) *API {
- a := &API{}
+ a := &API{
+ interceptor: &interceptor.NoOp{},
+ settingEngine: &SettingEngine{},
+ mediaEngine: &MediaEngine{},
+ interceptorRegistry: &interceptor.Registry{},
+ }
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
}
@@ -48,9 +45,8 @@ func NewAPI(options ...func(*API)) *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 = m
+ if a.mediaEngine == nil {
a.mediaEngine = &MediaEngine{}
}
}
@@ -66,8 +62,11 @@ func WithSettingEngine(s SettingEngine) func(a *API) {
// 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) {
+func WithInterceptorRegistry(ir *interceptor.Registry) func(a *API) {
return func(a *API) {
- a.interceptor = interceptorRegistry.Build()
+ a.interceptorRegistry = ir
+ if a.interceptorRegistry == nil {
+ a.interceptorRegistry = &interceptor.Registry{}
+ }
}
}
diff --git a/api_js.go b/api_js.go
index 964b7b05e09..3d81ed7b149 100644
--- a/api_js.go
+++ b/api_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/api_test.go b/api_test.go
index b63756c3da0..d2de28094bf 100644
--- a/api_test.go
+++ b/api_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -18,6 +19,10 @@ func TestNewAPI(t *testing.T) {
if api.mediaEngine == nil {
t.Error("Failed to init media engine")
}
+
+ if api.interceptorRegistry == nil {
+ t.Error("Failed to init interceptor registry")
+ }
}
func TestNewAPI_Options(t *testing.T) {
@@ -39,3 +44,14 @@ func TestNewAPI_Options(t *testing.T) {
t.Error("Failed to set media engine")
}
}
+
+func TestNewAPI_OptionsDefaultize(t *testing.T) {
+ api := NewAPI(
+ WithMediaEngine(nil),
+ WithInterceptorRegistry(nil),
+ )
+
+ assert.NotNil(t, api.settingEngine)
+ assert.NotNil(t, api.mediaEngine)
+ assert.NotNil(t, api.interceptorRegistry)
+}
diff --git a/atomicbool.go b/atomicbool.go
index c5ace62d0c8..1d4bf55ac99 100644
--- a/atomicbool.go
+++ b/atomicbool.go
@@ -18,3 +18,11 @@ func (b *atomicBool) set(value bool) { // nolint: unparam
func (b *atomicBool) get() bool {
return atomic.LoadInt32(&(b.val)) != 0
}
+
+func (b *atomicBool) swap(value bool) bool {
+ var i int32 = 0
+ if value {
+ i = 1
+ }
+ return atomic.SwapInt32(&(b.val), i) != 0
+}
diff --git a/certificate.go b/certificate.go
index 30a76a1b8d4..99e359741b5 100644
--- a/certificate.go
+++ b/certificate.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -10,7 +11,6 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
- "encoding/hex"
"encoding/pem"
"fmt"
"math/big"
@@ -123,12 +123,6 @@ func (c Certificate) GetFingerprints() ([]DTLSFingerprint, error) {
// 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 */
@@ -140,18 +134,12 @@ func GenerateCertificate(secretKey crypto.PrivateKey) (*Certificate, error) {
}
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,
+ Issuer: pkix.Name{CommonName: generatedCertificateOrigin},
+ NotBefore: time.Now().AddDate(0, 0, -1),
+ NotAfter: time.Now().AddDate(0, 1, -1),
+ SerialNumber: serialNumber,
+ Version: 2,
+ Subject: pkix.Name{CommonName: generatedCertificateOrigin},
})
}
diff --git a/certificate_test.go b/certificate_test.go
index 873bee336e9..bae16bfc3ba 100644
--- a/certificate_test.go
+++ b/certificate_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/configuration.go b/configuration.go
index 712bab92c25..608c5ab7e01 100644
--- a/configuration.go
+++ b/configuration.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/configuration_js.go b/configuration_js.go
index 44ab9f3c5a9..2ba4d268e33 100644
--- a/configuration_js.go
+++ b/configuration_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/constants.go b/constants.go
index 95f666ee402..825601ddb6b 100644
--- a/constants.go
+++ b/constants.go
@@ -7,7 +7,6 @@ const (
// comparisons when no value was defined.
Unknown = iota
unknownStr = "unknown"
- ssrcStr = "ssrc"
// Equal to UDP MTU
receiveMTU = 1460
@@ -23,7 +22,17 @@ const (
mediaSectionApplication = "application"
+ sdpAttributeRid = "rid"
+
rtpOutboundMTU = 1200
+
+ rtpPayloadTypeBitmask = 0x7F
+
+ incomingUnhandledRTPSsrc = "Incoming unhandled RTP ssrc(%d), OnTrack will not be fired. %v"
+
+ generatedCertificateOrigin = "WebRTC"
+
+ sdesRepairRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
)
func defaultSrtpProtectionProfiles() []dtls.SRTPProtectionProfile {
diff --git a/datachannel.go b/datachannel.go
index 4bcd9438312..11d09e19283 100644
--- a/datachannel.go
+++ b/datachannel.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -153,10 +154,15 @@ func (d *DataChannel) open(sctpTransport *SCTPTransport, restart bool) error {
}
if d.id == nil {
- err := d.sctpTransport.generateAndSetDataChannelID(d.sctpTransport.dtlsTransport.role(), &d.id)
+ // avoid holding lock when generating ID, since id generation locks
+ d.mu.Unlock()
+ var dcID *uint16
+ err := d.sctpTransport.generateAndSetDataChannelID(d.sctpTransport.dtlsTransport.role(), &dcID)
if err != nil {
return err
}
+ d.mu.Lock()
+ d.id = dcID
}
dc, err := datachannel.Dial(association, *d.id, cfg)
if err != nil {
@@ -174,7 +180,7 @@ func (d *DataChannel) open(sctpTransport *SCTPTransport, restart bool) error {
dc.OnBufferedAmountLow(d.onBufferedAmountLow)
d.mu.Unlock()
- d.handleOpen(dc)
+ d.handleOpen(dc, false, d.negotiated)
return nil
}
@@ -268,13 +274,23 @@ func (d *DataChannel) onMessage(msg DataChannelMessage) {
handler(msg)
}
-func (d *DataChannel) handleOpen(dc *datachannel.DataChannel) {
+func (d *DataChannel) handleOpen(dc *datachannel.DataChannel, isRemote, isAlreadyNegotiated bool) {
d.mu.Lock()
d.dataChannel = dc
d.mu.Unlock()
d.setReadyState(DataChannelStateOpen)
- d.onOpen()
+ // Fire the OnOpen handler immediately not using pion/datachannel
+ // * detached datachannels have no read loop, the user needs to read and query themselves
+ // * remote datachannels should fire OnOpened. This isn't spec compliant, but we can't break behavior yet
+ // * already negotiated datachannels should fire OnOpened
+ if d.api.settingEngine.detach.DataChannels || isRemote || isAlreadyNegotiated {
+ d.onOpen()
+ } else {
+ dc.OnOpen(func() {
+ d.onOpen()
+ })
+ }
d.mu.Lock()
defer d.mu.Unlock()
@@ -424,7 +440,7 @@ func (d *DataChannel) Label() string {
return d.label
}
-// Ordered represents if the DataChannel is ordered, and false if
+// Ordered returns true if the DataChannel is ordered, and false if
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
d.mu.RLock()
diff --git a/datachannel_go_test.go b/datachannel_go_test.go
index 958abb206dc..bbcef83bf64 100644
--- a/datachannel_go_test.go
+++ b/datachannel_go_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/datachannel_js.go b/datachannel_js.go
index 7aa6d99f8fb..55214b55165 100644
--- a/datachannel_js.go
+++ b/datachannel_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
@@ -162,7 +163,7 @@ func (d *DataChannel) Label() string {
// out-of-order delivery is allowed.
func (d *DataChannel) Ordered() bool {
ordered := d.underlying.Get("ordered")
- if jsValueIsUndefined(ordered) {
+ if ordered.IsUndefined() {
return true // default is true
}
return ordered.Bool()
@@ -171,13 +172,13 @@ func (d *DataChannel) 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")) {
+ if !d.underlying.Get("maxPacketLifeTime").IsUndefined() {
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"))
}
+
+ // 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
diff --git a/datachannel_js_detach.go b/datachannel_js_detach.go
index dd0069e0184..43186c5db1e 100644
--- a/datachannel_js_detach.go
+++ b/datachannel_js_detach.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/datachannel_test.go b/datachannel_test.go
index 5c3f78548bc..7dfaf4caf12 100644
--- a/datachannel_test.go
+++ b/datachannel_test.go
@@ -111,6 +111,8 @@ func benchmarkDataChannelSend(b *testing.B, numChannels int) {
}
func TestDataChannel_Open(t *testing.T) {
+ const openOnceChannelCapacity = 2
+
t.Run("handler should be called once", func(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
@@ -121,7 +123,7 @@ func TestDataChannel_Open(t *testing.T) {
}
done := make(chan bool)
- openCalls := make(chan bool, 2)
+ openCalls := make(chan bool, openOnceChannelCapacity)
answerPC.OnDataChannel(func(d *DataChannel) {
if d.Label() != expectedLabel {
@@ -155,6 +157,63 @@ func TestDataChannel_Open(t *testing.T) {
assert.Len(t, openCalls, 1)
})
+
+ t.Run("handler should be called once when already negotiated", func(t *testing.T) {
+ report := test.CheckRoutines(t)
+ defer report()
+
+ offerPC, answerPC, err := newPair()
+ if err != nil {
+ t.Fatalf("Failed to create a PC pair for testing")
+ }
+
+ done := make(chan bool)
+ answerOpenCalls := make(chan bool, openOnceChannelCapacity)
+ offerOpenCalls := make(chan bool, openOnceChannelCapacity)
+
+ negotiated := true
+ ordered := true
+ dataChannelID := uint16(0)
+
+ answerDC, err := answerPC.CreateDataChannel(expectedLabel, &DataChannelInit{
+ ID: &dataChannelID,
+ Negotiated: &negotiated,
+ Ordered: &ordered,
+ })
+ assert.NoError(t, err)
+ offerDC, err := offerPC.CreateDataChannel(expectedLabel, &DataChannelInit{
+ ID: &dataChannelID,
+ Negotiated: &negotiated,
+ Ordered: &ordered,
+ })
+ assert.NoError(t, err)
+
+ answerDC.OnMessage(func(msg DataChannelMessage) {
+ go func() {
+ // Wait a little bit to ensure all messages are processed.
+ time.Sleep(100 * time.Millisecond)
+ done <- true
+ }()
+ })
+ answerDC.OnOpen(func() {
+ answerOpenCalls <- true
+ })
+
+ offerDC.OnOpen(func() {
+ offerOpenCalls <- true
+ e := offerDC.SendText("Ping")
+ if e != nil {
+ t.Fatalf("Failed to send string on data channel")
+ }
+ })
+
+ assert.NoError(t, signalPair(offerPC, answerPC))
+
+ closePair(t, offerPC, answerPC, done)
+
+ assert.Len(t, answerOpenCalls, 1)
+ assert.Len(t, offerOpenCalls, 1)
+ })
}
func TestDataChannel_Send(t *testing.T) {
diff --git a/dtlstransport.go b/dtlstransport.go
index a1a7000faf9..c9ef0d538d7 100644
--- a/dtlstransport.go
+++ b/dtlstransport.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -17,6 +18,7 @@ import (
"github.com/pion/dtls/v2"
"github.com/pion/dtls/v2/pkg/crypto/fingerprint"
+ "github.com/pion/interceptor"
"github.com/pion/logging"
"github.com/pion/rtcp"
"github.com/pion/srtp/v2"
@@ -350,6 +352,10 @@ func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error {
dtlsConfig.ReplayProtectionWindow = int(*t.api.settingEngine.replayProtection.DTLS)
}
+ if t.api.settingEngine.dtls.retransmissionInterval != 0 {
+ dtlsConfig.FlightInterval = t.api.settingEngine.dtls.retransmissionInterval
+ }
+
// Connect as DTLS Client/Server, function is blocking and we
// must not hold the DTLSTransport lock
if role == DTLSRoleClient {
@@ -383,10 +389,6 @@ func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error {
return ErrNoSRTPProtectionProfile
}
- if t.api.settingEngine.disableCertificateFingerprintVerification {
- return nil
- }
-
// Check the fingerprint if a certificate was exchanged
remoteCerts := dtlsConn.ConnectionState().PeerCertificates
if len(remoteCerts) == 0 {
@@ -395,23 +397,25 @@ func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error {
}
t.remoteCertificate = remoteCerts[0]
- parsedRemoteCert, err := x509.ParseCertificate(t.remoteCertificate)
- if err != nil {
- if closeErr := dtlsConn.Close(); closeErr != nil {
- t.log.Error(err.Error())
+ if !t.api.settingEngine.disableCertificateFingerprintVerification {
+ parsedRemoteCert, err := x509.ParseCertificate(t.remoteCertificate)
+ if err != nil {
+ if closeErr := dtlsConn.Close(); closeErr != nil {
+ t.log.Error(err.Error())
+ }
+
+ t.onStateChange(DTLSTransportStateFailed)
+ return err
}
- t.onStateChange(DTLSTransportStateFailed)
- return err
- }
+ if err = t.validateFingerPrint(parsedRemoteCert); err != nil {
+ if closeErr := dtlsConn.Close(); closeErr != nil {
+ t.log.Error(err.Error())
+ }
- if err = t.validateFingerPrint(parsedRemoteCert); err != nil {
- if closeErr := dtlsConn.Close(); closeErr != nil {
- t.log.Error(err.Error())
+ t.onStateChange(DTLSTransportStateFailed)
+ return err
}
-
- t.onStateChange(DTLSTransportStateFailed)
- return err
}
t.conn = dtlsConn
@@ -471,7 +475,7 @@ func (t *DTLSTransport) validateFingerPrint(remoteCert *x509.Certificate) error
}
func (t *DTLSTransport) ensureICEConn() error {
- if t.iceTransport == nil || t.iceTransport.State() == ICETransportStateNew {
+ if t.iceTransport == nil {
return errICEConnectionNotStarted
}
@@ -484,3 +488,37 @@ func (t *DTLSTransport) storeSimulcastStream(s *srtp.ReadStreamSRTP) {
t.simulcastStreams = append(t.simulcastStreams, s)
}
+
+func (t *DTLSTransport) streamsForSSRC(ssrc SSRC, streamInfo interceptor.StreamInfo) (*srtp.ReadStreamSRTP, interceptor.RTPReader, *srtp.ReadStreamSRTCP, interceptor.RTCPReader, error) {
+ srtpSession, err := t.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 := t.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 := t.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 := t.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
+}
diff --git a/dtlstransport_js.go b/dtlstransport_js.go
new file mode 100644
index 00000000000..d4d8611ef77
--- /dev/null
+++ b/dtlstransport_js.go
@@ -0,0 +1,28 @@
+//go:build js && wasm
+// +build js,wasm
+
+package webrtc
+
+import "syscall/js"
+
+// 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 {
+ // Pointer to the underlying JavaScript DTLSTransport object.
+ underlying js.Value
+}
+
+// ICETransport returns the currently-configured *ICETransport or nil
+// if one has not been configured
+func (r *DTLSTransport) ICETransport() *ICETransport {
+ underlying := r.underlying.Get("iceTransport")
+ if underlying.IsNull() || underlying.IsUndefined() {
+ return nil
+ }
+
+ return &ICETransport{
+ underlying: underlying,
+ }
+}
diff --git a/dtlstransport_test.go b/dtlstransport_test.go
index a4aabe53891..4d6d39341e1 100644
--- a/dtlstransport_test.go
+++ b/dtlstransport_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/e2e/Dockerfile b/e2e/Dockerfile
index 751f3b7d9bf..c871222afb7 100644
--- a/e2e/Dockerfile
+++ b/e2e/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.16-alpine3.13
+FROM golang:1.17-alpine3.13
RUN apk add --no-cache \
chromium \
diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go
index ba203baaa32..8cc51278ad1 100644
--- a/e2e/e2e_test.go
+++ b/e2e/e2e_test.go
@@ -1,3 +1,4 @@
+//go:build e2e
// +build e2e
package main
@@ -333,7 +334,7 @@ func createTrack(offer webrtc.SessionDescription) (*webrtc.PeerConnection, *webr
return nil, nil, nil, errPc
}
- track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion")
+ track, errTrack := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
if errTrack != nil {
return nil, nil, nil, errTrack
}
diff --git a/errors.go b/errors.go
index f37cfa06aa8..e152b27c04c 100644
--- a/errors.go
+++ b/errors.go
@@ -82,7 +82,7 @@ var (
// 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")
+ ErrIncorrectSDPSemantics = errors.New("remote SessionDescription 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")
@@ -135,6 +135,16 @@ var (
// ErrUnsupportedCodec indicates the remote peer doesn't support the requested codec
ErrUnsupportedCodec = errors.New("unable to start track, codec is not supported by remote")
+ // ErrSenderWithNoCodecs indicates that a RTPSender was created without any codecs. To send media the MediaEngine needs at
+ // least one configured codec.
+ ErrSenderWithNoCodecs = errors.New("unable to populate media section, RTPSender created with no codecs")
+
+ // ErrRTPSenderNewTrackHasIncorrectKind indicates that the new track is of a different kind than the previous/original
+ ErrRTPSenderNewTrackHasIncorrectKind = errors.New("new track must be of the same kind as previous")
+
+ // ErrRTPSenderNewTrackHasIncorrectEnvelope indicates that the new track has a different envelope than the previous/original
+ ErrRTPSenderNewTrackHasIncorrectEnvelope = errors.New("new track must have the same envelope as previous")
+
// ErrUnbindFailed indicates that a TrackLocal was not able to be unbind
ErrUnbindFailed = errors.New("failed to unbind TrackLocal from PeerConnection")
@@ -193,15 +203,22 @@ var (
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")
+ errRTPSenderTrackNil = errors.New("Track must not be nil")
+ errRTPSenderDTLSTransportNil = errors.New("DTLSTransport must not be nil")
+ errRTPSenderSendAlreadyCalled = errors.New("Send has already been called")
+ errRTPSenderStopped = errors.New("Sender has already been stopped")
+ errRTPSenderTrackRemoved = errors.New("Sender Track has been removed or replaced to nil")
+ errRTPSenderRidNil = errors.New("Sender cannot add encoding as rid is empty")
+ errRTPSenderNoBaseEncoding = errors.New("Sender cannot add encoding as there is no base track")
+ errRTPSenderBaseEncodingMismatch = errors.New("Sender cannot add encoding as provided track does not match base track")
+ errRTPSenderRIDCollision = errors.New("Sender cannot encoding due to RID collision")
+ errRTPSenderNoTrackForRID = errors.New("Sender does not have track for RID")
errRTPTransceiverCannotChangeMid = errors.New("errRTPSenderTrackNil")
errRTPTransceiverSetSendingInvalidState = errors.New("invalid state change in RTPTransceiver.setSending")
+ errRTPTransceiverCodecUnsupported = errors.New("unsupported codec type by this transceiver")
errSCTPTransportDTLS = errors.New("DTLS not established")
@@ -219,4 +236,8 @@ var (
errICETransportNotInNew = errors.New("ICETransport can only be called in ICETransportStateNew")
errCertificatePEMFormatError = errors.New("bad Certificate PEM format")
+
+ errRTPTooShort = errors.New("not long enough to be a RTP Packet")
+
+ errExcessiveRetries = errors.New("excessive retries in CreateOffer")
)
diff --git a/examples/README.md b/examples/README.md
index af5140cb6c9..8677a5765ef 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -17,18 +17,23 @@ For more full featured examples that use 3rd party libraries see our **[example-
* [RTP Forwarder](rtp-forwarder): The rtp-forwarder example demonstrates how to forward your audio/video streams using RTP.
* [RTP to WebRTC](rtp-to-webrtc): The rtp-to-webrtc example demonstrates how to take RTP packets sent to a Pion process into your browser.
* [Simulcast](simulcast): The simulcast example demonstrates how to accept and demux 1 Track that contains 3 Simulcast streams. It then returns the media as 3 independent Tracks back to the sender.
+* [Swap Tracks](swap-tracks): The swap-tracks example demonstrates deeper usage of the Pion Media API. The server accepts 3 media streams, and then dynamically routes them back as a single stream to the user.
+* [RTCP Processing](rtcp-processing) The rtcp-processing example demonstrates Pion's RTCP APIs. This allow access to media statistics and control information.
#### Data Channel API
* [Data Channels](data-channels): The data-channels example shows how you can send/recv DataChannel messages from a web browser.
-* [Data Channels Create](data-channels-create): Example data-channels-create shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the data channel is initialized from the server side in this example.
-* [Data Channels Close](data-channels-close): Example data-channels-close is a variant of data-channels that allow playing with the life cycle of data channels.
* [Data Channels Detach](data-channels-detach): The data-channels-detach example shows how you can send/recv DataChannel messages using the underlying DataChannel implementation directly. This provides a more idiomatic way of interacting with Data Channels.
-* [Data Channels Detach Create](data-channels-detach-create): Example data-channels-detach-create shows how you can send/recv DataChannel messages using the underlying DataChannel implementation directly. This provides a more idiomatic way of interacting with Data Channels. The difference with the data-channels-detach example is that the data channel is initialized in this example.
+* [Data Channels Flow Control](data-channels-flow-control): Example data-channels-flow-control shows how to use the DataChannel API efficiently. You can measure the amount the rate at which the remote peer is receiving data, and structure your application accordingly.
* [ORTC](ortc): Example ortc shows how you an use the ORTC API for DataChannel communication.
* [Pion to Pion](pion-to-pion): Example pion-to-pion is an example of two pion instances communicating directly! It therefore has no corresponding web page.
#### Miscellaneous
* [Custom Logger](custom-logger) The custom-logger demonstrates how the user can override the logging and process messages instead of printing to stdout. It has no corresponding web page.
+* [ICE Restart](ice-restart) Example ice-restart demonstrates how a WebRTC connection can roam between networks. This example restarts ICE in a loop and prints the new addresses it uses each time.
+* [ICE Single Port](ice-single-port) Example ice-single-port demonstrates how multiple WebRTC connections can be served from a single port. By default Pion listens on a new port for every PeerConnection. Pion can be configured to use a single port for multiple connections.
+* [ICE TCP](ice-tcp) Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By default Pion only does UDP. Pion can be configured to use a TCP port, and this TCP port can be used for many connections.
+* [Trickle ICE](trickle-ice) Example trickle-ice example demonstrates Pion WebRTC's Trickle ICE APIs. This is important to use since it allows ICE Gathering and Connecting to happen concurrently.
+* [VNet](vnet) Example vnet demonstrates Pion's network virtualisation library. This example connects two PeerConnections over a virtual network and prints statistics about the data traveling over it.
### Usage
We've made it easy to run the browser based examples on your local machine.
diff --git a/examples/bandwidth-estimation-from-disk/README.md b/examples/bandwidth-estimation-from-disk/README.md
new file mode 100644
index 00000000000..4ab6e7d5a21
--- /dev/null
+++ b/examples/bandwidth-estimation-from-disk/README.md
@@ -0,0 +1,48 @@
+# bandwidth-estimation-from-disk
+bandwidth-estimation-from-disk demonstrates how to use Pion's Bandwidth Estimation APIs.
+
+Pion provides multiple Bandwidth Estimators, but they all satisfy one interface. This interface
+emits an int for how much bandwidth is available to send. It is then up to the sender to meet that number.
+
+## Instructions
+### Create IVF files named `high.ivf` `med.ivf` and `low.ivf`
+```
+ffmpeg -i $INPUT_FILE -g 30 -b:v .3M -s 320x240 low.ivf
+ffmpeg -i $INPUT_FILE -g 30 -b:v 1M -s 858x480 med.ivf
+ffmpeg -i $INPUT_FILE -g 30 -b:v 2.5M -s 1280x720 high.ivf
+```
+
+### Download bandwidth-estimation-from-disk
+
+```
+go get github.com/pion/webrtc/v3/examples/bandwidth-estimation-from-disk
+```
+
+### Open bandwidth-estimation-from-disk example page
+[jsfiddle.net](https://jsfiddle.net/a1cz42op/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard'
+
+### Run bandwidth-estimation-from-disk with your browsers Session Description as stdin
+The `output.ivf` you created should be in the same directory as `bandwidth-estimation-from-disk`. In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually.
+
+Now use this value you just copied as the input to `bandwidth-estimation-from-disk`
+
+#### Linux/macOS
+Run `echo $BROWSER_SDP | bandwidth-estimation-from-disk`
+#### Windows
+1. Paste the SessionDescription into a file.
+1. Run `bandwidth-estimation-from-disk < my_file`
+
+### Input bandwidth-estimation-from-disk's Session Description into your browser
+Copy the text that `bandwidth-estimation-from-disk` just emitted and copy into the second text area in the jsfiddle
+
+### Hit 'Start Session' in jsfiddle, enjoy your video!
+A video should start playing in your browser above the input boxes. When `bandwidth-estimation-from-disk` switches quality levels it will print the old and new file like so.
+
+```
+Switching from low.ivf to med.ivf
+Switching from med.ivf to high.ivf
+Switching from high.ivf to med.ivf
+```
+
+
+Congrats, you have used Pion WebRTC! Now start building something cool
diff --git a/examples/bandwidth-estimation-from-disk/main.go b/examples/bandwidth-estimation-from-disk/main.go
new file mode 100644
index 00000000000..b0c13f84b55
--- /dev/null
+++ b/examples/bandwidth-estimation-from-disk/main.go
@@ -0,0 +1,248 @@
+// +build !js
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/pion/interceptor"
+ "github.com/pion/interceptor/pkg/cc"
+ "github.com/pion/interceptor/pkg/gcc"
+ "github.com/pion/webrtc/v3"
+ "github.com/pion/webrtc/v3/examples/internal/signal"
+ "github.com/pion/webrtc/v3/pkg/media"
+ "github.com/pion/webrtc/v3/pkg/media/ivfreader"
+)
+
+const (
+ lowFile = "low.ivf"
+ lowBitrate = 300_000
+
+ medFile = "med.ivf"
+ medBitrate = 1_000_000
+
+ highFile = "high.ivf"
+ highBitrate = 2_500_000
+
+ ivfHeaderSize = 32
+)
+
+func main() {
+ qualityLevels := []struct {
+ fileName string
+ bitrate int
+ }{
+ {lowFile, lowBitrate},
+ {medFile, medBitrate},
+ {highFile, highBitrate},
+ }
+ currentQuality := 0
+
+ for _, level := range qualityLevels {
+ _, err := os.Stat(level.fileName)
+ if os.IsNotExist(err) {
+ panic(fmt.Sprintf("File %s was not found", level.fileName))
+ }
+ }
+
+ i := &interceptor.Registry{}
+ m := &webrtc.MediaEngine{}
+ if err := m.RegisterDefaultCodecs(); err != nil {
+ panic(err)
+ }
+
+ // Create a Congestion Controller. This analyzes inbound and outbound data and provides
+ // suggestions on how much we should be sending.
+ //
+ // Passing `nil` means we use the default Estimation Algorithm which is Google Congestion Control.
+ // You can use the other ones that Pion provides, or write your own!
+ congestionController, err := cc.NewInterceptor(func() (cc.BandwidthEstimator, error) {
+ return gcc.NewSendSideBWE(gcc.SendSideBWEInitialBitrate(lowBitrate))
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ estimatorChan := make(chan cc.BandwidthEstimator, 1)
+ congestionController.OnNewPeerConnection(func(id string, estimator cc.BandwidthEstimator) {
+ estimatorChan <- estimator
+ })
+
+ i.Add(congestionController)
+ if err = webrtc.ConfigureTWCCHeaderExtensionSender(m, i); err != nil {
+ panic(err)
+ }
+
+ if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil {
+ panic(err)
+ }
+
+ // Create a new RTCPeerConnection
+ peerConnection, err := webrtc.NewAPI(webrtc.WithInterceptorRegistry(i), webrtc.WithMediaEngine(m)).NewPeerConnection(webrtc.Configuration{
+ ICEServers: []webrtc.ICEServer{
+ {
+ URLs: []string{"stun:stun.l.google.com:19302"},
+ },
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
+
+ // Wait until our Bandwidth Estimator has been created
+ estimator := <-estimatorChan
+
+ // Create a video track
+ videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
+ if err != nil {
+ panic(err)
+ }
+
+ rtpSender, err := peerConnection.AddTrack(videoTrack)
+ if err != nil {
+ panic(err)
+ }
+
+ // Read incoming RTCP packets
+ // Before these packets are returned they are processed by interceptors. For things
+ // like NACK this needs to be called.
+ go func() {
+ rtcpBuf := make([]byte, 1500)
+ for {
+ if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil {
+ return
+ }
+ }
+ }()
+
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("Connection State has changed %s \n", connectionState.String())
+ })
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+ })
+
+ // Wait for the offer to be pasted
+ offer := webrtc.SessionDescription{}
+ signal.Decode(signal.MustReadStdin(), &offer)
+
+ // Set the remote SessionDescription
+ if err = peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ // Create answer
+ answer, err := peerConnection.CreateAnswer(nil)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create channel that is blocked until ICE Gathering is complete
+ gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
+
+ // Sets the LocalDescription, and starts our UDP listeners
+ if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+
+ // Block until ICE Gathering is complete, disabling trickle ICE
+ // we do this because we only can exchange one signaling message
+ // in a production application you should exchange ICE Candidates via OnICECandidate
+ <-gatherComplete
+
+ // Output the answer in base64 so we can paste it in browser
+ fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
+
+ // Open a IVF file and start reading using our IVFReader
+ file, err := os.Open(qualityLevels[currentQuality].fileName)
+ if err != nil {
+ panic(err)
+ }
+
+ ivf, header, err := ivfreader.NewWith(file)
+ if err != nil {
+ panic(err)
+ }
+
+ // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as.
+ // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
+ //
+ // It is important to use a time.Ticker instead of time.Sleep because
+ // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
+ // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
+ ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000))
+ frame := []byte{}
+ frameHeader := &ivfreader.IVFFrameHeader{}
+ currentTimestamp := uint64(0)
+
+ switchQualityLevel := func(newQualityLevel int) {
+ fmt.Printf("Switching from %s to %s \n", qualityLevels[currentQuality].fileName, qualityLevels[newQualityLevel].fileName)
+ currentQuality = newQualityLevel
+ ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName))
+ for {
+ if frame, frameHeader, err = ivf.ParseNextFrame(); err != nil {
+ break
+ } else if frameHeader.Timestamp >= currentTimestamp && frame[0]&0x1 == 0 {
+ break
+ }
+ }
+ }
+
+ for ; true; <-ticker.C {
+ targetBitrate := estimator.GetTargetBitrate()
+ switch {
+ // If current quality level is below target bitrate drop to level below
+ case currentQuality != 0 && targetBitrate < qualityLevels[currentQuality].bitrate:
+ switchQualityLevel(currentQuality - 1)
+
+ // If next quality level is above target bitrate move to next level
+ case len(qualityLevels) > (currentQuality+1) && targetBitrate > qualityLevels[currentQuality+1].bitrate:
+ switchQualityLevel(currentQuality + 1)
+
+ // Adjust outbound bandwidth for probing
+ default:
+ frame, _, err = ivf.ParseNextFrame()
+ }
+
+ switch err {
+ // No error write the video frame
+ case nil:
+ currentTimestamp = frameHeader.Timestamp
+ if err = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil {
+ panic(err)
+ }
+ // If we have reached the end of the file start again
+ case io.EOF:
+ ivf.ResetReader(setReaderFile(qualityLevels[currentQuality].fileName))
+ // Error besides io.EOF that we dont know how to handle
+ default:
+ panic(err)
+ }
+ }
+}
+
+func setReaderFile(filename string) func(_ int64) io.Reader {
+ return func(_ int64) io.Reader {
+ file, err := os.Open(filename) // nolint
+ if err != nil {
+ panic(err)
+ }
+ if _, err = file.Seek(ivfHeaderSize, io.SeekStart); err != nil {
+ panic(err)
+ }
+ return file
+ }
+}
diff --git a/examples/broadcast/README.md b/examples/broadcast/README.md
index f9544b38acc..07fe41e2be3 100644
--- a/examples/broadcast/README.md
+++ b/examples/broadcast/README.md
@@ -11,7 +11,7 @@ go get github.com/pion/webrtc/v3/examples/broadcast
```
### Open broadcast example page
-[jsfiddle.net](https://jsfiddle.net/1jc4go7v/) You should see two buttons 'Publish a Broadcast' and 'Join a Broadcast'
+[jsfiddle.net](https://jsfiddle.net/ypcsbnu3/) You should see two buttons `Publish a Broadcast` and `Join a Broadcast`
### Run Broadcast
#### Linux/macOS
@@ -20,7 +20,7 @@ Run `broadcast` OR run `main.go` in `github.com/pion/webrtc/examples/broadcast`
### Start a publisher
* Click `Publish a Broadcast`
-* Copy the string in the first input labelled `Browser base64 Session Description`
+* Press `Copy browser SDP to clipboard` or copy the `Browser base64 Session Description` string manually
* Run `curl localhost:8080/sdp -d "$BROWSER_OFFER"`. `$BROWSER_OFFER` is the value you copied in the last step.
* The `broadcast` terminal application will respond with an answer, paste this into the second input field in your browser.
* Press `Start Session`
diff --git a/examples/broadcast/jsfiddle/demo.html b/examples/broadcast/jsfiddle/demo.html
index dd6fed92d36..d873331e8d6 100644
--- a/examples/broadcast/jsfiddle/demo.html
+++ b/examples/broadcast/jsfiddle/demo.html
@@ -1,7 +1,12 @@
Browser base64 Session Description
-
+
+
+
+
Golang base64 Session Description
diff --git a/examples/broadcast/jsfiddle/demo.js b/examples/broadcast/jsfiddle/demo.js
index 026a9df08a7..946a5551ab7 100644
--- a/examples/broadcast/jsfiddle/demo.js
+++ b/examples/broadcast/jsfiddle/demo.js
@@ -1,10 +1,10 @@
/* eslint-env browser */
-var log = msg => {
+const log = msg => {
document.getElementById('logs').innerHTML += msg + ' '
}
window.createSession = isPublisher => {
- let pc = new RTCPeerConnection({
+ const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
@@ -21,7 +21,7 @@ window.createSession = isPublisher => {
if (isPublisher) {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
- stream.getTracks().forEach(track => pc.addTrack(track, stream));
+ stream.getTracks().forEach(track => pc.addTrack(track, stream))
document.getElementById('video1').srcObject = stream
pc.createOffer()
.then(d => pc.setLocalDescription(d))
@@ -34,7 +34,7 @@ window.createSession = isPublisher => {
.catch(log)
pc.ontrack = function (event) {
- var el = document.getElementById('video1')
+ const el = document.getElementById('video1')
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
@@ -42,7 +42,7 @@ window.createSession = isPublisher => {
}
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -54,7 +54,22 @@ window.createSession = isPublisher => {
}
}
- let btns = document.getElementsByClassName('createSessionButton')
+ window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SDP was ' + msg)
+ } catch (err) {
+ log('Unable to copy SDP ' + err)
+ }
+ }
+
+ const btns = document.getElementsByClassName('createSessionButton')
for (let i = 0; i < btns.length; i++) {
btns[i].style = 'display: none'
}
diff --git a/examples/broadcast/main.go b/examples/broadcast/main.go
index e11f76732fc..8f15776b7f7 100644
--- a/examples/broadcast/main.go
+++ b/examples/broadcast/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -38,6 +39,11 @@ func main() { // nolint:gocognit
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// Allow us to receive 1 video track
if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo); err != nil {
diff --git a/examples/custom-logger/main.go b/examples/custom-logger/main.go
index 49c187d8ac9..c09cd2717fd 100644
--- a/examples/custom-logger/main.go
+++ b/examples/custom-logger/main.go
@@ -1,9 +1,11 @@
+//go:build !js
// +build !js
package main
import (
"fmt"
+ "os"
"github.com/pion/logging"
"github.com/pion/webrtc/v3"
@@ -60,6 +62,11 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := offerPeerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close offerPeerConnection: %v\n", cErr)
+ }
+ }()
// We need a DataChannel so we can have ICE Candidates
if _, err = offerPeerConnection.CreateDataChannel("custom-logger", nil); err != nil {
@@ -71,6 +78,39 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := answerPeerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close answerPeerConnection: %v\n", cErr)
+ }
+ }()
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ offerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ answerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
// Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate
// send it to the other peer
@@ -126,5 +166,6 @@ func main() {
panic(err)
}
+ // Block forever
select {}
}
diff --git a/examples/data-channels-close/README.md b/examples/data-channels-close/README.md
deleted file mode 100644
index 7deb096eaf1..00000000000
--- a/examples/data-channels-close/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# data-channels-close
-data-channels-close is a variant of the data-channels example that allow playing with the life cycle of data channels.
diff --git a/examples/data-channels-close/jsfiddle/demo.details b/examples/data-channels-close/jsfiddle/demo.details
deleted file mode 100644
index c7729b4ddda..00000000000
--- a/examples/data-channels-close/jsfiddle/demo.details
+++ /dev/null
@@ -1,5 +0,0 @@
----
- name: data-channels
- description: Example of using Pion WebRTC to communicate with a web browser using bi-direction DataChannels
- authors:
- - Sean DuBois
diff --git a/examples/data-channels-close/jsfiddle/demo.html b/examples/data-channels-close/jsfiddle/demo.html
deleted file mode 100644
index 0fe3b899094..00000000000
--- a/examples/data-channels-close/jsfiddle/demo.html
+++ /dev/null
@@ -1,21 +0,0 @@
-Browser base64 Session Description
-
-Golang base64 Session Description
-
-
-
-
-
-Message
-
-
-
-
-Open channels
-
-
-
-
-
-Logs
-
diff --git a/examples/data-channels-close/jsfiddle/demo.js b/examples/data-channels-close/jsfiddle/demo.js
deleted file mode 100644
index 230a41e44b4..00000000000
--- a/examples/data-channels-close/jsfiddle/demo.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/* eslint-env browser */
-
-let pc = new RTCPeerConnection({
- iceServers: [
- {
- urls: 'stun:stun.l.google.com:19302'
- }
- ]
-})
-let log = msg => {
- document.getElementById('logs').innerHTML += msg + ' '
-}
-
-window.createDataChannel = name => {
- let dc = pc.createDataChannel(name)
- let fullName = `Data channel '${dc.label}' (${dc.id})`
- dc.onopen = () => {
- log(`${fullName}: has opened`)
- dc.onmessage = e => log(`${fullName}: '${e.data}'`)
-
- let ul = document.getElementById('ul-open')
- let li = document.createElement('li')
- li.appendChild(document.createTextNode(`${fullName}: `))
-
- let btnSend = document.createElement('BUTTON')
- btnSend.appendChild(document.createTextNode('Send message'))
- btnSend.onclick = () => {
- let message = document.getElementById('message').value
- if (message === '') {
- return alert('Message must not be empty')
- }
-
- dc.send(message)
- }
- li.appendChild(btnSend)
-
- let btnClose = document.createElement('BUTTON')
- btnClose.appendChild(document.createTextNode('Close'))
- btnClose.onclick = () => {
- dc.close()
- ul.removeChild(li)
- }
- li.appendChild(btnClose)
-
- dc.onclose = () => {
- log(`${fullName}: closed.`)
- ul.removeChild(li)
- }
-
- ul.appendChild(li)
- }
-}
-
-pc.oniceconnectionstatechange = e => log(`ICE state: ${pc.iceConnectionState}`)
-pc.onicecandidate = event => {
- if (event.candidate === null) {
- document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
- }
-}
-
-pc.onnegotiationneeded = e =>
- pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
-
-window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
- if (sd === '') {
- return alert('Session Description must not be empty')
- }
-
- try {
- pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))))
- } catch (e) {
- alert(e)
- }
-}
diff --git a/examples/data-channels-create/README.md b/examples/data-channels-create/README.md
deleted file mode 100644
index 42fac6f7ef3..00000000000
--- a/examples/data-channels-create/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# data-channels-create
-data-channels-create is a Pion WebRTC application that shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the datachannel is initialized from the pion side in this example.
-
-## Instructions
-### Download data-channels-create
-```
-export GO111MODULE=on
-go get github.com/pion/webrtc/v3/examples/data-channels-create
-```
-
-### Open data-channels-create example page
-[jsfiddle.net](https://jsfiddle.net/swgxrp94/20/)
-
-### Run data-channels-create
-Just run run `data-channels-create`.
-
-### Input data-channels-create's SessionDescription into your browser
-Copy the text that `data-channels-create` just emitted and copy into first text area of the jsfiddle.
-
-### Hit 'Start Session' in jsfiddle
-Hit the 'Start Session' button in the browser. You should see `have-remote-offer` below the `Send Message` button.
-
-### Input browser's SessionDescription into data-channels-create
-Meanwhile text has appeared in the second text area of the jsfiddle. Copy the text and paste it into `data-channels-create` and hit ENTER.
-In the browser you'll now see `connected` as the connection is created. If everything worked you should see `New DataChannel data`.
-
-Now you can put whatever you want in the `Message` textarea, and when you hit `Send Message` it should appear in your terminal!
-
-Pion WebRTC will send random messages every 5 seconds that will appear in your browser.
-
-Congrats, you have used Pion WebRTC! Now start building something cool
diff --git a/examples/data-channels-create/jsfiddle/demo.css b/examples/data-channels-create/jsfiddle/demo.css
deleted file mode 100644
index 9e43d340755..00000000000
--- a/examples/data-channels-create/jsfiddle/demo.css
+++ /dev/null
@@ -1,4 +0,0 @@
-textarea {
- width: 500px;
- min-height: 75px;
-}
\ No newline at end of file
diff --git a/examples/data-channels-create/jsfiddle/demo.details b/examples/data-channels-create/jsfiddle/demo.details
deleted file mode 100644
index c7729b4ddda..00000000000
--- a/examples/data-channels-create/jsfiddle/demo.details
+++ /dev/null
@@ -1,5 +0,0 @@
----
- name: data-channels
- description: Example of using Pion WebRTC to communicate with a web browser using bi-direction DataChannels
- authors:
- - Sean DuBois
diff --git a/examples/data-channels-create/jsfiddle/demo.html b/examples/data-channels-create/jsfiddle/demo.html
deleted file mode 100644
index 541129c11d1..00000000000
--- a/examples/data-channels-create/jsfiddle/demo.html
+++ /dev/null
@@ -1,17 +0,0 @@
-Golang base64 Session Description
-
-
-
-Browser base64 Session Description
-
-
-
-
-Message
-
-
-
-
-
-Logs
-
diff --git a/examples/data-channels-create/jsfiddle/demo.js b/examples/data-channels-create/jsfiddle/demo.js
deleted file mode 100644
index d2882b6de55..00000000000
--- a/examples/data-channels-create/jsfiddle/demo.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* eslint-env browser */
-
-let pc = new RTCPeerConnection({
- iceServers: [
- {
- urls: 'stun:stun.l.google.com:19302'
- }
- ]
-})
-let log = msg => {
- document.getElementById('logs').innerHTML += msg + ' '
-}
-
-pc.onsignalingstatechange = e => log(pc.signalingState)
-pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
-pc.onicecandidate = event => {
- if (event.candidate === null) {
- document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
- }
-}
-
-pc.ondatachannel = e => {
- let dc = e.channel
- log('New DataChannel ' + dc.label)
- dc.onclose = () => console.log('dc has closed')
- dc.onopen = () => console.log('dc has opened')
- dc.onmessage = e => log(`Message from DataChannel '${dc.label}' payload '${e.data}'`)
- window.sendMessage = () => {
- let message = document.getElementById('message').value
- if (message === '') {
- return alert('Message must not be empty')
- }
-
- dc.send(message)
- }
-}
-
-window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
- if (sd === '') {
- return alert('Session Description must not be empty')
- }
-
- pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd)))).catch(log)
- pc.createAnswer().then(d => pc.setLocalDescription(d)).catch(log)
-}
diff --git a/examples/data-channels-create/main.go b/examples/data-channels-create/main.go
deleted file mode 100644
index deeee9a514e..00000000000
--- a/examples/data-channels-create/main.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package main
-
-import (
- "fmt"
- "time"
-
- "github.com/pion/webrtc/v3"
- "github.com/pion/webrtc/v3/examples/internal/signal"
-)
-
-func main() {
- // Everything below is the Pion WebRTC API! Thanks for using it ❤️.
-
- // Prepare the configuration
- config := webrtc.Configuration{
- ICEServers: []webrtc.ICEServer{
- {
- URLs: []string{"stun:stun.l.google.com:19302"},
- },
- },
- }
-
- // Create a new RTCPeerConnection
- peerConnection, err := webrtc.NewPeerConnection(config)
- if err != nil {
- panic(err)
- }
-
- // Create a datachannel with label 'data'
- dataChannel, err := peerConnection.CreateDataChannel("data", nil)
- if err != nil {
- panic(err)
- }
-
- // Set the handler for ICE connection state
- // This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
- })
-
- // Register channel opening handling
- dataChannel.OnOpen(func() {
- fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", dataChannel.Label(), dataChannel.ID())
-
- for range time.NewTicker(5 * time.Second).C {
- message := signal.RandSeq(15)
- fmt.Printf("Sending '%s'\n", message)
-
- // Send the message as text
- sendErr := dataChannel.SendText(message)
- if sendErr != nil {
- panic(sendErr)
- }
- }
- })
-
- // Register text message handling
- dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
- fmt.Printf("Message from DataChannel '%s': '%s'\n", dataChannel.Label(), string(msg.Data))
- })
-
- // Create an offer to send to the browser
- offer, err := peerConnection.CreateOffer(nil)
- if err != nil {
- panic(err)
- }
-
- // Create channel that is blocked until ICE Gathering is complete
- gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
-
- // Sets the LocalDescription, and starts our UDP listeners
- err = peerConnection.SetLocalDescription(offer)
- if err != nil {
- panic(err)
- }
-
- // Block until ICE Gathering is complete, disabling trickle ICE
- // we do this because we only can exchange one signaling message
- // in a production application you should exchange ICE Candidates via OnICECandidate
- <-gatherComplete
-
- // Output the answer in base64 so we can paste it in browser
- fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
-
- // Wait for the answer to be pasted
- answer := webrtc.SessionDescription{}
- signal.Decode(signal.MustReadStdin(), &answer)
-
- // Apply the answer as the remote description
- err = peerConnection.SetRemoteDescription(answer)
- if err != nil {
- panic(err)
- }
-
- // Block forever
- select {}
-}
diff --git a/examples/data-channels-detach-create/README.md b/examples/data-channels-detach-create/README.md
deleted file mode 100644
index 7bff37d15dc..00000000000
--- a/examples/data-channels-detach-create/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# data-channels-detach-create
-data-channels-detach-create is an example that shows how you can detach a data channel. This allows direct access the the underlying [pion/datachannel](https://github.com/pion/datachannel). This allows you to interact with the data channel using a more idiomatic API based on the `io.ReadWriteCloser` interface.
-
-The example mirrors the data-channels-create example.
-
-## Install
-```
-export GO111MODULE=on
-go get github.com/pion/webrtc/v3/examples/data-channels-detach-create
-```
-
-## Usage
-The example can be used in the same way as the data-channel example or can be paired with the data-channels-detach example. In the latter case; run both example and exchange the offer/answer text by copy-pasting them on the other terminal.
diff --git a/examples/data-channels-detach-create/main.go b/examples/data-channels-detach-create/main.go
deleted file mode 100644
index ecaec3263ff..00000000000
--- a/examples/data-channels-detach-create/main.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
- "time"
-
- "github.com/pion/webrtc/v3"
- "github.com/pion/webrtc/v3/examples/internal/signal"
-)
-
-const messageSize = 15
-
-func main() {
- // Since this behavior diverges from the WebRTC API it has to be
- // enabled using a settings engine. Mixing both detached and the
- // OnMessage DataChannel API is not supported.
-
- // Create a SettingEngine and enable Detach
- s := webrtc.SettingEngine{}
- s.DetachDataChannels()
-
- // Create an API object with the engine
- api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
-
- // Everything below is the Pion WebRTC API! Thanks for using it ❤️.
-
- // Prepare the configuration
- config := webrtc.Configuration{
- ICEServers: []webrtc.ICEServer{
- {
- URLs: []string{"stun:stun.l.google.com:19302"},
- },
- },
- }
-
- // Create a new RTCPeerConnection using the API object
- peerConnection, err := api.NewPeerConnection(config)
- if err != nil {
- panic(err)
- }
-
- // Create a datachannel with label 'data'
- dataChannel, err := peerConnection.CreateDataChannel("data", nil)
- if err != nil {
- panic(err)
- }
-
- // Set the handler for ICE connection state
- // This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
- })
-
- // Register channel opening handling
- dataChannel.OnOpen(func() {
- fmt.Printf("Data channel '%s'-'%d' open.\n", dataChannel.Label(), dataChannel.ID())
-
- // Detach the data channel
- raw, dErr := dataChannel.Detach()
- if dErr != nil {
- panic(dErr)
- }
-
- // Handle reading from the data channel
- go ReadLoop(raw)
-
- // Handle writing to the data channel
- go WriteLoop(raw)
- })
-
- // Create an offer to send to the browser
- offer, err := peerConnection.CreateOffer(nil)
- if err != nil {
- panic(err)
- }
-
- // Create channel that is blocked until ICE Gathering is complete
- gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
-
- // Sets the LocalDescription, and starts our UDP listeners
- err = peerConnection.SetLocalDescription(offer)
- if err != nil {
- panic(err)
- }
-
- // Block until ICE Gathering is complete, disabling trickle ICE
- // we do this because we only can exchange one signaling message
- // in a production application you should exchange ICE Candidates via OnICECandidate
- <-gatherComplete
-
- // Output the offer in base64 so we can paste it in browser
- fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
-
- // Wait for the answer to be pasted
- answer := webrtc.SessionDescription{}
- signal.Decode(signal.MustReadStdin(), &answer)
-
- // Apply the answer as the remote description
- err = peerConnection.SetRemoteDescription(answer)
- if err != nil {
- panic(err)
- }
-
- // Block forever
- select {}
-}
-
-// ReadLoop shows how to read from the datachannel directly
-func ReadLoop(d io.Reader) {
- for {
- buffer := make([]byte, messageSize)
- n, err := d.Read(buffer)
- if err != nil {
- fmt.Println("Datachannel closed; Exit the readloop:", err)
- return
- }
-
- fmt.Printf("Message from DataChannel: %s\n", string(buffer[:n]))
- }
-}
-
-// WriteLoop shows how to write to the datachannel directly
-func WriteLoop(d io.Writer) {
- for range time.NewTicker(5 * time.Second).C {
- message := signal.RandSeq(messageSize)
- fmt.Printf("Sending %s \n", message)
-
- _, err := d.Write([]byte(message))
- if err != nil {
- panic(err)
- }
- }
-}
diff --git a/examples/data-channels-detach/jsfiddle/main.go b/examples/data-channels-detach/jsfiddle/main.go
index 91109d29890..050195427cf 100644
--- a/examples/data-channels-detach/jsfiddle/main.go
+++ b/examples/data-channels-detach/jsfiddle/main.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package main
diff --git a/examples/data-channels-detach/main.go b/examples/data-channels-detach/main.go
index ebfa7b86cea..f1a724f124f 100644
--- a/examples/data-channels-detach/main.go
+++ b/examples/data-channels-detach/main.go
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"io"
+ "os"
"time"
"github.com/pion/webrtc/v3"
@@ -39,11 +40,24 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
- // Set the handler for ICE connection state
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Register data channel creation handling
diff --git a/examples/data-channels-flow-control/main.go b/examples/data-channels-flow-control/main.go
index 0fa897fce89..1abb2092d79 100644
--- a/examples/data-channels-flow-control/main.go
+++ b/examples/data-channels-flow-control/main.go
@@ -2,7 +2,9 @@ package main
import (
"encoding/json"
+ "fmt"
"log"
+ "os"
"sync/atomic"
"time"
@@ -120,7 +122,18 @@ func createAnswerer() *webrtc.PeerConnection {
func main() {
offerPC := createOfferer()
+ defer func() {
+ if err := offerPC.Close(); err != nil {
+ fmt.Printf("cannot close offerPC: %v\n", err)
+ }
+ }()
+
answerPC := createAnswerer()
+ defer func() {
+ if err := answerPC.Close(); err != nil {
+ fmt.Printf("cannot close answerPC: %v\n", err)
+ }
+ }()
// Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate
// send it to the other peer
@@ -138,6 +151,34 @@ func main() {
}
})
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ offerPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ answerPC.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
// Now, create an offer
offer, err := offerPC.CreateOffer(nil)
check(err)
diff --git a/examples/data-channels/README.md b/examples/data-channels/README.md
index 11872b3df23..c9d9c45d351 100644
--- a/examples/data-channels/README.md
+++ b/examples/data-channels/README.md
@@ -9,10 +9,10 @@ go get github.com/pion/webrtc/v3/examples/data-channels
```
### Open data-channels example page
-[jsfiddle.net](https://jsfiddle.net/9tsx15mg/90/)
+[jsfiddle.net](https://jsfiddle.net/t3johb5g/2/)
### Run data-channels, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser's session description, copy that and:
+In the jsfiddle the top textarea is your browser's session description, press `Copy browser SDP to clipboard` or copy the base64 string manually and:
#### Linux/macOS
Run `echo $BROWSER_SDP | data-channels`
#### Windows
diff --git a/examples/data-channels/jsfiddle/demo.html b/examples/data-channels/jsfiddle/demo.html
index 5a8823b580a..b50aa880c4e 100644
--- a/examples/data-channels/jsfiddle/demo.html
+++ b/examples/data-channels/jsfiddle/demo.html
@@ -1,5 +1,10 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/data-channels/jsfiddle/demo.js b/examples/data-channels/jsfiddle/demo.js
index 16b466d3b89..c0e88a69522 100644
--- a/examples/data-channels/jsfiddle/demo.js
+++ b/examples/data-channels/jsfiddle/demo.js
@@ -1,17 +1,17 @@
/* eslint-env browser */
-let pc = new RTCPeerConnection({
+const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
-let log = msg => {
+const log = msg => {
document.getElementById('logs').innerHTML += msg + ' '
}
-let sendChannel = pc.createDataChannel('foo')
+const sendChannel = pc.createDataChannel('foo')
sendChannel.onclose = () => console.log('sendChannel has closed')
sendChannel.onopen = () => console.log('sendChannel has opened')
sendChannel.onmessage = e => log(`Message from DataChannel '${sendChannel.label}' payload '${e.data}'`)
@@ -27,7 +27,7 @@ pc.onnegotiationneeded = e =>
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
window.sendMessage = () => {
- let message = document.getElementById('message').value
+ const message = document.getElementById('message').value
if (message === '') {
return alert('Message must not be empty')
}
@@ -36,7 +36,7 @@ window.sendMessage = () => {
}
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -47,3 +47,18 @@ window.startSession = () => {
alert(e)
}
}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SDP was ' + msg)
+ } catch (err) {
+ log('Unable to copy SDP ' + err)
+ }
+}
diff --git a/examples/data-channels/jsfiddle/main.go b/examples/data-channels/jsfiddle/main.go
index 506e37ab57f..bee901bbf43 100644
--- a/examples/data-channels/jsfiddle/main.go
+++ b/examples/data-channels/jsfiddle/main.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package main
@@ -34,6 +35,11 @@ func main() {
})
sendChannel.OnOpen(func() {
fmt.Println("sendChannel has opened")
+
+ candidatePair, err := pc.SCTP().Transport().ICETransport().GetSelectedCandidatePair()
+
+ fmt.Println(candidatePair)
+ fmt.Println(err)
})
sendChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
log(fmt.Sprintf("Message from DataChannel %s payload %s", sendChannel.Label(), string(msg.Data)))
@@ -92,6 +98,33 @@ func main() {
}()
return js.Undefined()
}))
+ js.Global().Set("copySDP", js.FuncOf(func(_ js.Value, _ []js.Value) interface{} {
+ go func() {
+ defer func() {
+ if e := recover(); e != nil {
+ switch e := e.(type) {
+ case error:
+ handleError(e)
+ default:
+ handleError(fmt.Errorf("recovered with non-error value: (%T) %s", e, e))
+ }
+ }
+ }()
+
+ browserSDP := getElementByID("localSessionDescription")
+
+ browserSDP.Call("focus")
+ browserSDP.Call("select")
+
+ copyStatus := js.Global().Get("document").Call("execCommand", "copy")
+ if copyStatus.Bool() {
+ log("Copying SDP was successful")
+ } else {
+ log("Copying SDP was unsuccessful")
+ }
+ }()
+ return js.Undefined()
+ }))
// Stay alive
select {}
diff --git a/examples/data-channels/main.go b/examples/data-channels/main.go
index 89f74002cee..902222eac22 100644
--- a/examples/data-channels/main.go
+++ b/examples/data-channels/main.go
@@ -2,6 +2,7 @@ package main
import (
"fmt"
+ "os"
"time"
"github.com/pion/webrtc/v3"
@@ -25,11 +26,24 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
- // Set the handler for ICE connection state
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Register data channel creation handling
diff --git a/examples/examples.json b/examples/examples.json
index 54534c6850c..22608fdf941 100644
--- a/examples/examples.json
+++ b/examples/examples.json
@@ -5,24 +5,18 @@
"description": "The data-channels example shows how you can send/recv DataChannel messages from a web browser.",
"type": "browser"
},
- {
- "title": "Data Channels Create",
- "link": "data-channels-create",
- "description": "Example data-channels-create shows how you can send/recv DataChannel messages from a web browser. The difference with the data-channels example is that the data channel is initialized from the server side in this example.",
- "type": "browser"
- },
- {
- "title": "Data Channels Close",
- "link": "data-channels-close",
- "description": "Example data-channels-close is a variant of data-channels that allow playing with the life cycle of data channels.",
- "type": "browser"
- },
{
"title": "Data Channels Detach",
"link": "data-channels-detach",
"description": "The data-channels-detach is an example that shows how you can detach a data channel.",
"type": "browser"
},
+ {
+ "title": "Data Channels Flow Control",
+ "link": "data-channels-flow-control",
+ "description": "The data-channels-detach data-channels-flow-control shows how to use the DataChannel API efficiently. You can measure the amount the rate at which the remote peer is receiving data, and structure your application accordingly",
+ "type": "browser"
+ },
{
"title": "Reflect",
"link": "reflect",
@@ -88,5 +82,47 @@
"link": "simulcast",
"description": "Example simulcast demonstrates how to accept and demux 1 Track that contains 3 Simulcast streams. It then returns the media as 3 independent Tracks back to the sender.",
"type": "browser"
+ },
+ {
+ "title": "ICE Restart",
+ "link": "#",
+ "description": "Example ice-restart demonstrates how a WebRTC connection can roam between networks. This example restarts ICE in a loop and prints the new addresses it uses each time.",
+ "type": "browser"
+ },
+ {
+ "title": "ICE Single Port",
+ "link": "#",
+ "description": "Example ice-single-port demonstrates how multiple WebRTC connections can be served from a single port. By default Pion listens on a new port for every PeerConnection. Pion can be configured to use a single port for multiple connections.",
+ "type": "browser"
+ },
+ {
+ "title": "ICE TCP",
+ "link": "#",
+ "description": "Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By default Pion only does UDP. Pion can be configured to use a TCP port, and this TCP port can be used for many connections.",
+ "type": "browser"
+ },
+ {
+ "title": "Swap Tracks",
+ "link": "swap-tracks",
+ "description": "The swap-tracks example demonstrates deeper usage of the Pion Media API. The server accepts 3 media streams, and then dynamically routes them back as a single stream to the user.",
+ "type": "browser"
+ },
+ {
+ "title": "VNet",
+ "link": "#",
+ "description": "The vnet example demonstrates Pion's network virtualisation library. This example connects two PeerConnections over a virtual network and prints statistics about the data traveling over it.",
+ "type": "browser"
+ },
+ {
+ "title": "rtcp-processing",
+ "link": "rtcp-processing",
+ "description": "The rtcp-processing example demonstrates Pion's RTCP APIs. This allow access to media statistics and control information.",
+ "type": "browser"
+ },
+ {
+ "title": "trickle-ice",
+ "link": "#",
+ "description": "The trickle-ice example demonstrates Pion WebRTC's Trickle ICE APIs.",
+ "type": "browser"
}
]
diff --git a/examples/ice-single-port/main.go b/examples/ice-single-port/main.go
index 5cca522d50a..21f8e50a775 100644
--- a/examples/ice-single-port/main.go
+++ b/examples/ice-single-port/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
diff --git a/examples/ice-tcp/main.go b/examples/ice-tcp/main.go
index 105d2912357..d9738c04a2a 100644
--- a/examples/ice-tcp/main.go
+++ b/examples/ice-tcp/main.go
@@ -1,10 +1,13 @@
+//go:build !js
// +build !js
package main
import (
"encoding/json"
+ "errors"
"fmt"
+ "io"
"net"
"net/http"
"time"
@@ -12,57 +15,33 @@ import (
"github.com/pion/webrtc/v3"
)
-var peerConnection *webrtc.PeerConnection //nolint
+var api *webrtc.API //nolint
func doSignaling(w http.ResponseWriter, r *http.Request) {
- var err error
-
- if peerConnection == nil {
- settingEngine := webrtc.SettingEngine{}
-
- // Enable support only for TCP ICE candidates.
- settingEngine.SetNetworkTypes([]webrtc.NetworkType{
- webrtc.NetworkTypeTCP4,
- webrtc.NetworkTypeTCP6,
- })
-
- var tcpListener net.Listener
- tcpListener, err = net.ListenTCP("tcp", &net.TCPAddr{
- IP: net.IP{0, 0, 0, 0},
- Port: 8443,
- })
-
- if err != nil {
- panic(err)
- }
-
- fmt.Printf("Listening for ICE TCP at %s\n", tcpListener.Addr())
-
- tcpMux := webrtc.NewICETCPMux(nil, tcpListener, 8)
- settingEngine.SetICETCPMux(tcpMux)
-
- api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine))
- if peerConnection, err = api.NewPeerConnection(webrtc.Configuration{}); err != nil {
- panic(err)
- }
-
- // Set the handler for ICE connection state
- // This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
- })
+ peerConnection, err := api.NewPeerConnection(webrtc.Configuration{})
+ if err != nil {
+ panic(err)
+ }
- // Send the current time via a DataChannel to the remote peer every 3 seconds
- peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
- d.OnOpen(func() {
- for range time.Tick(time.Second * 3) {
- if err = d.SendText(time.Now().String()); err != nil {
- panic(err)
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ })
+
+ // Send the current time via a DataChannel to the remote peer every 3 seconds
+ peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
+ d.OnOpen(func() {
+ for range time.Tick(time.Second * 3) {
+ if err = d.SendText(time.Now().String()); err != nil {
+ if errors.Is(io.ErrClosedPipe, err) {
+ return
}
+ panic(err)
}
- })
+ }
})
- }
+ })
var offer webrtc.SessionDescription
if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
@@ -100,6 +79,29 @@ func doSignaling(w http.ResponseWriter, r *http.Request) {
}
func main() {
+ settingEngine := webrtc.SettingEngine{}
+
+ // Enable support only for TCP ICE candidates.
+ settingEngine.SetNetworkTypes([]webrtc.NetworkType{
+ webrtc.NetworkTypeTCP4,
+ webrtc.NetworkTypeTCP6,
+ })
+
+ tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
+ IP: net.IP{0, 0, 0, 0},
+ Port: 8443,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("Listening for ICE TCP at %s\n", tcpListener.Addr())
+
+ tcpMux := webrtc.NewICETCPMux(nil, tcpListener, 8)
+ settingEngine.SetICETCPMux(tcpMux)
+
+ api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine))
+
http.Handle("/", http.FileServer(http.Dir(".")))
http.HandleFunc("/doSignaling", doSignaling)
diff --git a/examples/insertable-streams/jsfiddle/demo.js b/examples/insertable-streams/jsfiddle/demo.js
index 6638df02313..ff2be92d135 100644
--- a/examples/insertable-streams/jsfiddle/demo.js
+++ b/examples/insertable-streams/jsfiddle/demo.js
@@ -3,17 +3,17 @@
// cipherKey that video is encrypted with
const cipherKey = 0xAA
-let pc = new RTCPeerConnection({encodedInsertableStreams: true, forceEncodedVideoInsertableStreams: true})
-let log = msg => {
+const pc = new RTCPeerConnection({ encodedInsertableStreams: true, forceEncodedVideoInsertableStreams: true })
+const log = msg => {
document.getElementById('div').innerHTML += msg + ' '
}
// Offer to receive 1 video
-let transceiver = pc.addTransceiver('video')
+const transceiver = pc.addTransceiver('video')
// The API has seen two iterations, support both
// In the future this will just be `createEncodedStreams`
-let receiverStreams = getInsertableStream(transceiver)
+const receiverStreams = getInsertableStream(transceiver)
// boolean controlled by checkbox to enable/disable encryption
let applyDecryption = true
@@ -24,8 +24,8 @@ window.toggleDecryption = () => {
// Loop that is called for each video frame
const reader = receiverStreams.readable.getReader()
const writer = receiverStreams.writable.getWriter()
-reader.read().then(function processVideo({ done, value }) {
- let decrypted = new DataView(value.data)
+reader.read().then(function processVideo ({ done, value }) {
+ const decrypted = new DataView(value.data)
if (applyDecryption) {
for (let i = 0; i < decrypted.buffer.byteLength; i++) {
@@ -41,7 +41,7 @@ reader.read().then(function processVideo({ done, value }) {
// Fire when remote video arrives
pc.ontrack = function (event) {
document.getElementById('remote-video').srcObject = event.streams[0]
- document.getElementById('remote-video').style = ""
+ document.getElementById('remote-video').style = ''
}
// Populate SDP field when finished gathering
@@ -54,7 +54,7 @@ pc.onicecandidate = event => {
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -68,8 +68,8 @@ window.startSession = () => {
// DOM code to show banner if insertable streams not supported
let insertableStreamsSupported = true
-let updateSupportBanner = () => {
- let el = document.getElementById('no-support-banner')
+const updateSupportBanner = () => {
+ const el = document.getElementById('no-support-banner')
if (insertableStreamsSupported && el) {
el.style = 'display: none'
}
@@ -77,7 +77,7 @@ let updateSupportBanner = () => {
document.addEventListener('DOMContentLoaded', updateSupportBanner)
// Shim to support both versions of API
-function getInsertableStream(transceiver) {
+function getInsertableStream (transceiver) {
let insertableStreams = null
if (transceiver.receiver.createEncodedVideoStreams) {
insertableStreams = transceiver.receiver.createEncodedVideoStreams()
@@ -88,7 +88,7 @@ function getInsertableStream(transceiver) {
if (!insertableStreams) {
insertableStreamsSupported = false
updateSupportBanner()
- throw 'Insertable Streams are not supported'
+ throw new Error('Insertable Streams are not supported')
}
return insertableStreams
diff --git a/examples/insertable-streams/main.go b/examples/insertable-streams/main.go
index 652e40aacb8..1dd0fe0bc40 100644
--- a/examples/insertable-streams/main.go
+++ b/examples/insertable-streams/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -28,9 +29,14 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// Create a video track
- videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
if err != nil {
panic(err)
}
@@ -102,6 +108,20 @@ func main() {
}
})
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
diff --git a/examples/ortc/main.go b/examples/ortc/main.go
index 2954106cd95..0f61e0698ba 100644
--- a/examples/ortc/main.go
+++ b/examples/ortc/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
diff --git a/examples/pion-to-pion/answer/Dockerfile b/examples/pion-to-pion/answer/Dockerfile
index 33e5965cc28..5f6da9e01d5 100644
--- a/examples/pion-to-pion/answer/Dockerfile
+++ b/examples/pion-to-pion/answer/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.16
+FROM golang:1.17
ENV GO111MODULE=on
RUN go get -u github.com/pion/webrtc/v3/examples/pion-to-pion/answer
diff --git a/examples/pion-to-pion/answer/main.go b/examples/pion-to-pion/answer/main.go
index 81ad7c8acbf..84cd39d77ff 100644
--- a/examples/pion-to-pion/answer/main.go
+++ b/examples/pion-to-pion/answer/main.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "os"
"sync"
"time"
@@ -52,6 +53,11 @@ func main() { // nolint:gocognit
if err != nil {
panic(err)
}
+ defer func() {
+ if err := peerConnection.Close(); err != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", err)
+ }
+ }()
// When an ICE candidate is available send to the other Pion instance
// the other Pion instance will add this candidate by calling AddICECandidate
@@ -129,10 +135,18 @@ func main() { // nolint:gocognit
candidatesMux.Unlock()
})
- // Set the handler for ICE connection state
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Register data channel creation handling
diff --git a/examples/pion-to-pion/offer/Dockerfile b/examples/pion-to-pion/offer/Dockerfile
index 20b9f6dd41e..d291331ea8a 100644
--- a/examples/pion-to-pion/offer/Dockerfile
+++ b/examples/pion-to-pion/offer/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.16
+FROM golang:1.17
ENV GO111MODULE=on
RUN go get -u github.com/pion/webrtc/v3/examples/pion-to-pion/offer
diff --git a/examples/pion-to-pion/offer/main.go b/examples/pion-to-pion/offer/main.go
index c153b72b7bf..ef845c9f763 100644
--- a/examples/pion-to-pion/offer/main.go
+++ b/examples/pion-to-pion/offer/main.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "os"
"sync"
"time"
@@ -52,6 +53,11 @@ func main() { //nolint:gocognit
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// When an ICE candidate is available send to the other Pion instance
// the other Pion instance will add this candidate by calling AddICECandidate
@@ -113,10 +119,18 @@ func main() { //nolint:gocognit
panic(err)
}
- // Set the handler for ICE connection state
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Register channel opening handling
diff --git a/examples/play-from-disk-renegotation/README.md b/examples/play-from-disk-renegotation/README.md
index 68434adc783..17669e531bd 100644
--- a/examples/play-from-disk-renegotation/README.md
+++ b/examples/play-from-disk-renegotation/README.md
@@ -16,11 +16,15 @@ cd webrtc/examples/play-from-disk-renegotiation
```
### Create IVF named `output.ivf` that contains a VP8 track
+
```
-ffmpeg -i $INPUT_FILE -g 30 output.ivf
+ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf
```
+**Note**: In the `ffmpeg` command, the argument `-b:v 2M` specifies the video bitrate to be 2 megabits per second. We provide this default value to produce decent video quality, but if you experience problems with this configuration (such as dropped frames etc.), you can decrease this. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options) for more information on the format of the value.
+
### Run play-from-disk-renegotiation
+
The `output.ivf` you created should be in the same directory as `play-from-disk-renegotiation`. Execute `go run *.go`
### Open the Web UI
diff --git a/examples/play-from-disk-renegotation/main.go b/examples/play-from-disk-renegotation/main.go
index 4aed7b60d74..04df604748b 100644
--- a/examples/play-from-disk-renegotation/main.go
+++ b/examples/play-from-disk-renegotation/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -69,7 +70,7 @@ func createPeerConnection(w http.ResponseWriter, r *http.Request) {
// Add a single video track
func addVideo(w http.ResponseWriter, r *http.Request) {
videoTrack, err := webrtc.NewTrackLocalStaticSample(
- webrtc.RTPCodecCapability{MimeType: "video/vp8"},
+ webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8},
fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()),
fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()),
)
@@ -117,11 +118,24 @@ func main() {
if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
- // Set the handler for ICE connection state
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
http.Handle("/", http.FileServer(http.Dir(".")))
@@ -129,8 +143,13 @@ func main() {
http.HandleFunc("/addVideo", addVideo)
http.HandleFunc("/removeVideo", removeVideo)
- fmt.Println("Open http://localhost:8080 to access this demo")
- panic(http.ListenAndServe(":8080", nil))
+ go func() {
+ fmt.Println("Open http://localhost:8080 to access this demo")
+ panic(http.ListenAndServe(":8080", nil))
+ }()
+
+ // Block forever
+ select {}
}
// Read a video file from disk and write it to a webrtc.Track
@@ -149,15 +168,18 @@ func writeVideoToTrack(t *webrtc.TrackLocalStaticSample) {
// Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as.
// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
- sleepTime := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)
- for {
+ //
+ // It is important to use a time.Ticker instead of time.Sleep because
+ // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
+ // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
+ ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000))
+ for ; true; <-ticker.C {
frame, _, err := ivf.ParseNextFrame()
if err != nil {
fmt.Printf("Finish writing video track: %s ", err)
return
}
- time.Sleep(sleepTime)
if err = t.WriteSample(media.Sample{Data: frame, Duration: time.Second}); err != nil {
fmt.Printf("Finish writing video track: %s ", err)
return
diff --git a/examples/play-from-disk/README.md b/examples/play-from-disk/README.md
index d033fad530e..15204ec9445 100644
--- a/examples/play-from-disk/README.md
+++ b/examples/play-from-disk/README.md
@@ -1,24 +1,31 @@
# play-from-disk
play-from-disk demonstrates how to send video and/or audio to your browser from files saved to disk.
+For an example of playing H264 from disk see [play-from-disk-h264](https://github.com/pion/example-webrtc-applications/tree/master/play-from-disk-h264)
+
## Instructions
### Create IVF named `output.ivf` that contains a VP8 track and/or `output.ogg` that contains a Opus track
```
-ffmpeg -i $INPUT_FILE -g 30 output.ivf
+ffmpeg -i $INPUT_FILE -g 30 -b:v 2M output.ivf
ffmpeg -i $INPUT_FILE -c:a libopus -page_duration 20000 -vn output.ogg
```
+**Note**: In the `ffmpeg` command which produces the .ivf file, the argument `-b:v 2M` specifies the video bitrate to be 2 megabits per second. We provide this default value to produce decent video quality, but if you experience problems with this configuration (such as dropped frames etc.), you can decrease this. See the [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#Options) for more information on the format of the value.
+
### Download play-from-disk
+
```
export GO111MODULE=on
go get github.com/pion/webrtc/v3/examples/play-from-disk
```
### Open play-from-disk example page
-[jsfiddle.net](https://jsfiddle.net/9s10amwL/) you should see two text-areas and a 'Start Session' button
+[jsfiddle.net](https://jsfiddle.net/a1cz42op/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard'
+
+### Run play-from-disk with your browsers Session Description as stdin
+The `output.ivf` you created should be in the same directory as `play-from-disk`. In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually.
-### Run play-from-disk with your browsers SessionDescription as stdin
-The `output.ivf` you created should be in the same directory as `play-from-disk`. In the jsfiddle the top textarea is your browser, copy that and:
+Now use this value you just copied as the input to `play-from-disk`
#### Linux/macOS
Run `echo $BROWSER_SDP | play-from-disk`
@@ -26,8 +33,8 @@ Run `echo $BROWSER_SDP | play-from-disk`
1. Paste the SessionDescription into a file.
1. Run `play-from-disk < my_file`
-### Input play-from-disk's SessionDescription into your browser
-Copy the text that `play-from-disk` just emitted and copy into second text area
+### Input play-from-disk's Session Description into your browser
+Copy the text that `play-from-disk` just emitted and copy into the second text area in the jsfiddle
### Hit 'Start Session' in jsfiddle, enjoy your video!
A video should start playing in your browser above the input boxes. `play-from-disk` will exit when the file reaches the end
diff --git a/examples/play-from-disk/jsfiddle/demo.html b/examples/play-from-disk/jsfiddle/demo.html
index 6dbbf2558b5..a068aa98f34 100644
--- a/examples/play-from-disk/jsfiddle/demo.html
+++ b/examples/play-from-disk/jsfiddle/demo.html
@@ -1,14 +1,26 @@
-Browser base64 Session Description
-
+Browser Session Description
+
+
+
-Golang base64 Session Description
-
-
+
-
+
+
+
-Video
+Remote Session Description
+
+
+
+
+
+
+
+Video
+
-Logs
+Logs
+
diff --git a/examples/play-from-disk/jsfiddle/demo.js b/examples/play-from-disk/jsfiddle/demo.js
index c32ddaf0dd8..f9afa945c9b 100644
--- a/examples/play-from-disk/jsfiddle/demo.js
+++ b/examples/play-from-disk/jsfiddle/demo.js
@@ -1,18 +1,16 @@
/* eslint-env browser */
-let pc = new RTCPeerConnection({
- iceServers: [
- {
- urls: 'stun:stun.l.google.com:19302'
- }
- ]
+const pc = new RTCPeerConnection({
+ iceServers: [{
+ urls: 'stun:stun.l.google.com:19302'
+ }]
})
-let log = msg => {
+const log = msg => {
document.getElementById('div').innerHTML += msg + ' '
}
pc.ontrack = function (event) {
- var el = document.createElement(event.track.kind)
+ const el = document.createElement(event.track.kind)
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
@@ -28,13 +26,17 @@ pc.onicecandidate = event => {
}
// Offer to receive 1 audio, and 1 video track
-pc.addTransceiver('video', {'direction': 'sendrecv'})
-pc.addTransceiver('audio', {'direction': 'sendrecv'})
+pc.addTransceiver('video', {
+ direction: 'sendrecv'
+})
+pc.addTransceiver('audio', {
+ direction: 'sendrecv'
+})
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -45,3 +47,18 @@ window.startSession = () => {
alert(e)
}
}
+
+window.copySessionDescription = () => {
+ const browserSessionDescription = document.getElementById('localSessionDescription')
+
+ browserSessionDescription.focus()
+ browserSessionDescription.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SessionDescription was ' + msg)
+ } catch (err) {
+ log('Oops, unable to copy SessionDescription ' + err)
+ }
+}
diff --git a/examples/play-from-disk/main.go b/examples/play-from-disk/main.go
index 08615942bfe..386ab88e2b6 100644
--- a/examples/play-from-disk/main.go
+++ b/examples/play-from-disk/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -17,8 +18,9 @@ import (
)
const (
- audioFileName = "output.ogg"
- videoFileName = "output.ivf"
+ audioFileName = "output.ogg"
+ videoFileName = "output.ivf"
+ oggPageDuration = time.Millisecond * 20
)
func main() {
@@ -44,11 +46,17 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
+
iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background())
if haveVideoFile {
// Create a video track
- videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
if videoTrackErr != nil {
panic(videoTrackErr)
}
@@ -87,8 +95,12 @@ func main() {
// Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as.
// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
- sleepTime := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000)
- for {
+ //
+ // It is important to use a time.Ticker instead of time.Sleep because
+ // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
+ // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
+ ticker := time.NewTicker(time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000))
+ for ; true; <-ticker.C {
frame, _, ivfErr := ivf.ParseNextFrame()
if ivfErr == io.EOF {
fmt.Printf("All video frames parsed and sent")
@@ -99,7 +111,6 @@ func main() {
panic(ivfErr)
}
- time.Sleep(sleepTime)
if ivfErr = videoTrack.WriteSample(media.Sample{Data: frame, Duration: time.Second}); ivfErr != nil {
panic(ivfErr)
}
@@ -109,7 +120,7 @@ func main() {
if haveAudioFile {
// Create a audio track
- audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion")
+ audioTrack, audioTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "pion")
if audioTrackErr != nil {
panic(audioTrackErr)
}
@@ -132,7 +143,7 @@ func main() {
}()
go func() {
- // Open a IVF file and start reading using our IVFReader
+ // Open a OGG file and start reading using our OGGReader
file, oggErr := os.Open(audioFileName)
if oggErr != nil {
panic(oggErr)
@@ -149,7 +160,12 @@ func main() {
// Keep track of last granule, the difference is the amount of samples in the buffer
var lastGranule uint64
- for {
+
+ // It is important to use a time.Ticker instead of time.Sleep because
+ // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
+ // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
+ ticker := time.NewTicker(oggPageDuration)
+ for ; true; <-ticker.C {
pageData, pageHeader, oggErr := ogg.ParseNextPage()
if oggErr == io.EOF {
fmt.Printf("All audio pages parsed and sent")
@@ -168,8 +184,6 @@ func main() {
if oggErr = audioTrack.WriteSample(media.Sample{Data: pageData, Duration: sampleDuration}); oggErr != nil {
panic(oggErr)
}
-
- time.Sleep(sampleDuration)
}
}()
}
@@ -183,6 +197,20 @@ func main() {
}
})
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
// Wait for the offer to be pasted
offer := webrtc.SessionDescription{}
signal.Decode(signal.MustReadStdin(), &offer)
diff --git a/examples/reflect/README.md b/examples/reflect/README.md
index 77abe8e0bff..96e25a5aa9b 100644
--- a/examples/reflect/README.md
+++ b/examples/reflect/README.md
@@ -9,10 +9,12 @@ go get github.com/pion/webrtc/v3/examples/reflect
```
### Open reflect example page
-[jsfiddle.net](https://jsfiddle.net/9jgukzt1/) you should see two text-areas and a 'Start Session' button.
+[jsfiddle.net](https://jsfiddle.net/ogs7muqh/1/) you should see two text-areas and a 'Start Session' button.
### Run reflect, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser, copy that and:
+In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
+We will use this value in the next step.
+
#### Linux/macOS
Run `echo $BROWSER_SDP | reflect`
#### Windows
diff --git a/examples/reflect/jsfiddle/demo.html b/examples/reflect/jsfiddle/demo.html
index 7c1ba183a37..e4f07824082 100644
--- a/examples/reflect/jsfiddle/demo.html
+++ b/examples/reflect/jsfiddle/demo.html
@@ -1,5 +1,10 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/reflect/jsfiddle/demo.js b/examples/reflect/jsfiddle/demo.js
index 5b1779db142..7c1b68580a9 100644
--- a/examples/reflect/jsfiddle/demo.js
+++ b/examples/reflect/jsfiddle/demo.js
@@ -1,19 +1,19 @@
/* eslint-env browser */
-let pc = new RTCPeerConnection({
+const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
-var log = msg => {
+const log = msg => {
document.getElementById('logs').innerHTML += msg + ' '
}
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
- stream.getTracks().forEach(track => pc.addTrack(track, stream));
+ stream.getTracks().forEach(track => pc.addTrack(track, stream))
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
}).catch(log)
@@ -24,7 +24,7 @@ pc.onicecandidate = event => {
}
}
pc.ontrack = function (event) {
- var el = document.createElement(event.track.kind)
+ const el = document.createElement(event.track.kind)
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
@@ -33,7 +33,7 @@ pc.ontrack = function (event) {
}
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -44,3 +44,18 @@ window.startSession = () => {
alert(e)
}
}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SDP was ' + msg)
+ } catch (err) {
+ log('Unable to copy SDP ' + err)
+ }
+}
diff --git a/examples/reflect/main.go b/examples/reflect/main.go
index 4d75e6fda39..466fb80b1c9 100644
--- a/examples/reflect/main.go
+++ b/examples/reflect/main.go
@@ -1,9 +1,11 @@
+//go:build !js
// +build !js
package main
import (
"fmt"
+ "os"
"time"
"github.com/pion/interceptor"
@@ -21,7 +23,7 @@ func main() {
// Setup the codecs you want to use.
// We'll use a VP8 and Opus but you can also define your own
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
- RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
@@ -54,9 +56,14 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// Create Track that we send video back to browser on
- outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
if err != nil {
panic(err)
}
@@ -117,10 +124,19 @@ func main() {
}
}
})
- // Set the handler for ICE connection state
+
+ // Set the handler for Peer connection state
// This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("Connection State has changed %s \n", connectionState.String())
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Create an answer
diff --git a/examples/rtcp-processing/README.md b/examples/rtcp-processing/README.md
new file mode 100644
index 00000000000..775bcc41b8f
--- /dev/null
+++ b/examples/rtcp-processing/README.md
@@ -0,0 +1,38 @@
+# rtcp-processing
+rtcp-processing demonstrates the Public API for processing RTCP packets in Pion WebRTC.
+
+This example is only processing messages for a RTPReceiver. A RTPReceiver is used for accepting
+media from a remote peer. These APIs also exist on the RTPSender when sending media to a remote peer.
+
+RTCP is used for statistics and control information for media in WebRTC. Using these messages
+you can get information about the quality of the media, round trip time and packet loss. You can
+also craft messages to influence the media quality.
+
+## Instructions
+### Download rtcp-processing
+```
+export GO111MODULE=on
+go get github.com/pion/webrtc/v3/examples/rtcp-processing
+```
+
+### Open rtcp-processing example page
+[jsfiddle.net](https://jsfiddle.net/Le3zg7sd/) you should see two text-areas, 'Start Session' button and 'Copy browser SessionDescription to clipboard'
+
+### Run rtcp-processing with your browsers Session Description as stdin
+In the jsfiddle press 'Copy browser Session Description to clipboard' or copy the base64 string manually.
+
+Now use this value you just copied as the input to `rtcp-processing`
+
+#### Linux/macOS
+Run `echo $BROWSER_SDP | rtcp-processing`
+#### Windows
+1. Paste the SessionDescription into a file.
+1. Run `rtcp-processing < my_file`
+
+### Input rtcp-processing's Session Description into your browser
+Copy the text that `rtcp-processing` just emitted and copy into the second text area in the jsfiddle
+
+### Hit 'Start Session' in jsfiddle
+You will see console messages for each inbound RTCP message from the remote peer.
+
+Congrats, you have used Pion WebRTC! Now start building something cool
diff --git a/examples/data-channels-close/jsfiddle/demo.css b/examples/rtcp-processing/jsfiddle/demo.css
similarity index 100%
rename from examples/data-channels-close/jsfiddle/demo.css
rename to examples/rtcp-processing/jsfiddle/demo.css
diff --git a/examples/rtcp-processing/jsfiddle/demo.details b/examples/rtcp-processing/jsfiddle/demo.details
new file mode 100644
index 00000000000..173b142ae54
--- /dev/null
+++ b/examples/rtcp-processing/jsfiddle/demo.details
@@ -0,0 +1,5 @@
+---
+ name: rtcp-processing
+ description: play-from-disk demonstrates how to process RTCP messages from Pion WebRTC
+ authors:
+ - Sean DuBois
diff --git a/examples/rtcp-processing/jsfiddle/demo.html b/examples/rtcp-processing/jsfiddle/demo.html
new file mode 100644
index 00000000000..1bcc74fd98d
--- /dev/null
+++ b/examples/rtcp-processing/jsfiddle/demo.html
@@ -0,0 +1,25 @@
+Browser Session Description
+
+
+
+
+
+
+
+
+
+
+Remote Session Description
+
+
+
+
+
+
+
+Video
+
+
+Logs
+
+
diff --git a/examples/rtcp-processing/jsfiddle/demo.js b/examples/rtcp-processing/jsfiddle/demo.js
new file mode 100644
index 00000000000..41cd47c1434
--- /dev/null
+++ b/examples/rtcp-processing/jsfiddle/demo.js
@@ -0,0 +1,62 @@
+/* eslint-env browser */
+
+const pc = new RTCPeerConnection({
+ iceServers: [{
+ urls: 'stun:stun.l.google.com:19302'
+ }]
+})
+const log = msg => {
+ document.getElementById('div').innerHTML += msg + ' '
+}
+
+pc.ontrack = function (event) {
+ const el = document.createElement(event.track.kind)
+ el.srcObject = event.streams[0]
+ el.autoplay = true
+ el.controls = true
+
+ document.getElementById('remoteVideos').appendChild(el)
+}
+
+pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
+pc.onicecandidate = event => {
+ if (event.candidate === null) {
+ document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
+ }
+}
+
+navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => {
+ document.getElementById('video1').srcObject = stream
+ stream.getTracks().forEach(track => pc.addTrack(track, stream))
+
+ pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
+ }).catch(log)
+
+window.startSession = () => {
+ const sd = document.getElementById('remoteSessionDescription').value
+ if (sd === '') {
+ return alert('Session Description must not be empty')
+ }
+
+ try {
+ pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))))
+ } catch (e) {
+ alert(e)
+ }
+}
+
+window.copySessionDescription = () => {
+ const browserSessionDescription = document.getElementById('localSessionDescription')
+
+ browserSessionDescription.focus()
+ browserSessionDescription.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SessionDescription was ' + msg)
+ } catch (err) {
+ log('Oops, unable to copy SessionDescription ' + err)
+ }
+}
diff --git a/examples/data-channels-close/main.go b/examples/rtcp-processing/main.go
similarity index 56%
rename from examples/data-channels-close/main.go
rename to examples/rtcp-processing/main.go
index 238a2eb952f..1bd5ab3e273 100644
--- a/examples/data-channels-close/main.go
+++ b/examples/rtcp-processing/main.go
@@ -1,18 +1,16 @@
+//go:build !js
+// +build !js
+
package main
import (
- "flag"
"fmt"
- "time"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/examples/internal/signal"
)
func main() {
- closeAfter := flag.Int("close-after", 5, "Close data channel after sending X times.")
- flag.Parse()
-
// Everything below is the Pion WebRTC API! Thanks for using it ❤️.
// Prepare the configuration
@@ -30,54 +28,30 @@ func main() {
panic(err)
}
- // Set the handler for ICE connection state
- // This will notify you when the peer has connected/disconnected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
- })
-
- // Register data channel creation handling
- peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
- fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
-
- // Register channel opening handling
- d.OnOpen(func() {
- fmt.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds\n", d.Label(), d.ID())
-
- ticker := time.NewTicker(5 * time.Second)
+ // Set a handler for when a new remote track starts
+ peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
+ fmt.Printf("Track has started streamId(%s) id(%s) rid(%s) \n", track.StreamID(), track.ID(), track.RID())
- d.OnClose(func() {
- fmt.Printf("Data channel '%s'-'%d' closed.\n", d.Label(), d.ID())
- ticker.Stop()
- })
-
- cnt := *closeAfter
- for range ticker.C {
- message := signal.RandSeq(15)
- fmt.Printf("Sending '%s'\n", message)
-
- // Send the message as text
- sendErr := d.SendText(message)
- if sendErr != nil {
- panic(sendErr)
- }
+ for {
+ // Read the RTCP packets as they become available for our new remote track
+ rtcpPackets, _, rtcpErr := receiver.ReadRTCP()
+ if rtcpErr != nil {
+ panic(rtcpErr)
+ }
- cnt--
- if cnt < 0 {
- fmt.Printf("Sent %d times. Closing data channel '%s'-'%d'.\n", *closeAfter, d.Label(), d.ID())
- ticker.Stop()
- err = d.Close()
- if err != nil {
- panic(err)
- }
+ for _, r := range rtcpPackets {
+ // Print a string description of the packets
+ if stringer, canString := r.(fmt.Stringer); canString {
+ fmt.Printf("Received RTCP Packet: %v", stringer.String())
}
}
- })
+ }
+ })
- // Register message handling
- d.OnMessage(func(msg webrtc.DataChannelMessage) {
- fmt.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
- })
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("Connection State has changed %s \n", connectionState.String())
})
// Wait for the offer to be pasted
diff --git a/examples/rtp-forwarder/README.md b/examples/rtp-forwarder/README.md
index 100d9a368b3..27f59613e06 100644
--- a/examples/rtp-forwarder/README.md
+++ b/examples/rtp-forwarder/README.md
@@ -9,10 +9,12 @@ go get github.com/pion/webrtc/v3/examples/rtp-forwarder
```
### Open rtp-forwarder example page
-[jsfiddle.net](https://jsfiddle.net/1qva2zd8/) you should see your Webcam, two text-areas and a 'Start Session' button
+[jsfiddle.net](https://jsfiddle.net/xjcve6d3/) you should see your Webcam, two text-areas and `Copy browser SDP to clipboard`, `Start Session` buttons
### Run rtp-forwarder, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser, copy that and:
+In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
+We will use this value in the next step.
+
#### Linux/macOS
Run `echo $BROWSER_SDP | rtp-forwarder`
#### Windows
@@ -33,7 +35,7 @@ Run `ffprobe -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to get more
Run `ffplay -i rtp-forwarder.sdp -protocol_whitelist file,udp,rtp` to play your streams
-You can add `-fflags nobuffer` to lower the latency. You will have worse playback in networks with jitter.
+You can add `-fflags nobuffer -flags low_delay -framedrop` to lower the latency. You will have worse playback in networks with jitter. Read about minimizing the delay on [Stackoverflow](https://stackoverflow.com/a/49273163/5472819).
#### Twitch/RTMP
`ffmpeg -protocol_whitelist file,udp,rtp -i rtp-forwarder.sdp -c:v libx264 -preset veryfast -b:v 3000k -maxrate 3000k -bufsize 6000k -pix_fmt yuv420p -g 50 -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv rtmp://live.twitch.tv/app/$STREAM_KEY` Make sure to replace `$STREAM_KEY` at the end of the URL first.
diff --git a/examples/rtp-forwarder/jsfiddle/demo.html b/examples/rtp-forwarder/jsfiddle/demo.html
index cba0be079df..363b3fb2d2a 100644
--- a/examples/rtp-forwarder/jsfiddle/demo.html
+++ b/examples/rtp-forwarder/jsfiddle/demo.html
@@ -1,5 +1,10 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/rtp-forwarder/jsfiddle/demo.js b/examples/rtp-forwarder/jsfiddle/demo.js
index 362e726e423..1eba38b99d3 100644
--- a/examples/rtp-forwarder/jsfiddle/demo.js
+++ b/examples/rtp-forwarder/jsfiddle/demo.js
@@ -1,13 +1,13 @@
/* eslint-env browser */
-let pc = new RTCPeerConnection({
+const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
-var log = msg => {
+const log = msg => {
document.getElementById('logs').innerHTML += msg + ' '
}
@@ -26,7 +26,7 @@ pc.onicecandidate = event => {
}
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -37,3 +37,18 @@ window.startSession = () => {
alert(e)
}
}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SDP was ' + msg)
+ } catch (err) {
+ log('Unable to copy SDP ' + err)
+ }
+}
diff --git a/examples/rtp-forwarder/main.go b/examples/rtp-forwarder/main.go
index a1b6c639878..41ce6036713 100644
--- a/examples/rtp-forwarder/main.go
+++ b/examples/rtp-forwarder/main.go
@@ -1,11 +1,12 @@
+//go:build !js
// +build !js
package main
import (
- "context"
"fmt"
"net"
+ "os"
"time"
"github.com/pion/interceptor"
@@ -30,12 +31,12 @@ func main() {
// Setup the codecs you want to use.
// We'll use a VP8 and Opus but you can also define your own
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
- RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/VP8", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
}, webrtc.RTPCodecTypeVideo); err != nil {
panic(err)
}
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
- RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/opus", ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
+ RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
}, webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}
@@ -68,6 +69,11 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// Allow us to receive 1 audio track, and 1 video track
if _, err = peerConnection.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil {
@@ -163,9 +169,6 @@ func main() {
}
})
- // Create context
- ctx, cancel := context.WithCancel(context.Background())
-
// Set the handler for ICE connection state
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
@@ -173,10 +176,20 @@ func main() {
if connectionState == webrtc.ICEConnectionStateConnected {
fmt.Println("Ctrl+C the remote client to stop the demo")
- } else if connectionState == webrtc.ICEConnectionStateFailed ||
- connectionState == webrtc.ICEConnectionStateDisconnected {
+ }
+ })
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
fmt.Println("Done forwarding")
- cancel()
+ os.Exit(0)
}
})
@@ -211,6 +224,6 @@ func main() {
// Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
- // Wait for context to be done
- <-ctx.Done()
+ // Block forever
+ select {}
}
diff --git a/examples/rtp-to-webrtc/README.md b/examples/rtp-to-webrtc/README.md
index 065d2db5fb3..fd467472317 100644
--- a/examples/rtp-to-webrtc/README.md
+++ b/examples/rtp-to-webrtc/README.md
@@ -35,13 +35,19 @@ gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480,format=I420 ! vp8
#### ffmpeg
```
-ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec libvpx -cpu-used 5 -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -f rtp rtp://127.0.0.1:5004?pkt_size=1200
+ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec libvpx -cpu-used 5 -deadline 1 -g 10 -error-resilient 1 -auto-alt-ref 1 -f rtp 'rtp://127.0.0.1:5004?pkt_size=1200'
```
-If you wish to send audio replace both occurrences of `vp8` in `main.go` then run
+If you wish to send audio replace all occurrences of `vp8` with Opus in `main.go` then run
```
-ffmpeg -f lavfi -i "sine=frequency=1000" -c:a libopus -b:a 48000 -sample_fmt s16p -ssrc 1 -payload_type 111 -f rtp -max_delay 0 -application lowdelay rtp:/127.0.0.1:5004?pkt_size=1200
+ffmpeg -f lavfi -i 'sine=frequency=1000' -c:a libopus -b:a 48000 -sample_fmt s16p -ssrc 1 -payload_type 111 -f rtp -max_delay 0 -application lowdelay 'rtp://127.0.0.1:5004?pkt_size=1200'
+```
+
+If you wish to send H264 instead of VP8 replace all occurrences of `vp8` with H264 in `main.go` then run
+
+```
+ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -pix_fmt yuv420p -c:v libx264 -g 10 -preset ultrafast -tune zerolatency -f rtp 'rtp://127.0.0.1:5004?pkt_size=1200'
```
### Input rtp-to-webrtc's SessionDescription into your browser
diff --git a/examples/rtp-to-webrtc/main.go b/examples/rtp-to-webrtc/main.go
index 04dded41e0b..46cafebd475 100644
--- a/examples/rtp-to-webrtc/main.go
+++ b/examples/rtp-to-webrtc/main.go
@@ -1,9 +1,12 @@
+//go:build !js
// +build !js
package main
import (
+ "errors"
"fmt"
+ "io"
"net"
"github.com/pion/webrtc/v3"
@@ -59,6 +62,12 @@ func main() {
// This will notify you when the peer has connected/disconnected
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
+
+ if connectionState == webrtc.ICEConnectionStateFailed {
+ if closeErr := peerConnection.Close(); closeErr != nil {
+ panic(closeErr)
+ }
+ }
})
// Wait for the offer to be pasted
@@ -101,6 +110,11 @@ func main() {
}
if _, err = videoTrack.Write(inboundRTPPacket[:n]); err != nil {
+ if errors.Is(err, io.ErrClosedPipe) {
+ // The peerConnection has been closed.
+ return
+ }
+
panic(err)
}
}
diff --git a/examples/save-to-disk/README.md b/examples/save-to-disk/README.md
index 61c16ab7e08..cf316e0053f 100644
--- a/examples/save-to-disk/README.md
+++ b/examples/save-to-disk/README.md
@@ -1,7 +1,7 @@
# save-to-disk
save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk.
-If you wish to save H264 to disk checkout out [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/master/save-to-webm)
+If you wish to save VP8/Opus inside the same file see [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/master/save-to-webm)
## Instructions
### Download save-to-disk
@@ -11,10 +11,12 @@ go get github.com/pion/webrtc/v3/examples/save-to-disk
```
### Open save-to-disk example page
-[jsfiddle.net](https://jsfiddle.net/vfmcg8rk/1/) you should see your Webcam, two text-areas and a 'Start Session' button
+[jsfiddle.net](https://jsfiddle.net/xjcve6d3/) you should see your Webcam, two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`.
### Run save-to-disk, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser, copy that and:
+In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
+We will use this value in the next step.
+
#### Linux/macOS
Run `echo $BROWSER_SDP | save-to-disk`
#### Windows
diff --git a/examples/save-to-disk/jsfiddle/demo.html b/examples/save-to-disk/jsfiddle/demo.html
index cba0be079df..363b3fb2d2a 100644
--- a/examples/save-to-disk/jsfiddle/demo.html
+++ b/examples/save-to-disk/jsfiddle/demo.html
@@ -1,5 +1,10 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/save-to-disk/jsfiddle/demo.js b/examples/save-to-disk/jsfiddle/demo.js
index 6863027ca82..d3ee36d9f7d 100644
--- a/examples/save-to-disk/jsfiddle/demo.js
+++ b/examples/save-to-disk/jsfiddle/demo.js
@@ -1,21 +1,20 @@
/* eslint-env browser */
-let pc = new RTCPeerConnection({
+const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
-var log = msg => {
+const log = msg => {
document.getElementById('logs').innerHTML += msg + ' '
}
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
-
document.getElementById('video1').srcObject = stream
- stream.getTracks().forEach(track => pc.addTrack(track, stream));
+ stream.getTracks().forEach(track => pc.addTrack(track, stream))
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
}).catch(log)
@@ -28,7 +27,7 @@ pc.onicecandidate = event => {
}
window.startSession = () => {
- let sd = document.getElementById('remoteSessionDescription').value
+ const sd = document.getElementById('remoteSessionDescription').value
if (sd === '') {
return alert('Session Description must not be empty')
}
@@ -39,3 +38,18 @@ window.startSession = () => {
alert(e)
}
}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ log('Copying SDP was ' + msg)
+ } catch (err) {
+ log('Unable to copy SDP ' + err)
+ }
+}
diff --git a/examples/save-to-disk/main.go b/examples/save-to-disk/main.go
index c2b68c83250..dfab19b4f92 100644
--- a/examples/save-to-disk/main.go
+++ b/examples/save-to-disk/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -133,19 +134,22 @@ func main() {
if connectionState == webrtc.ICEConnectionStateConnected {
fmt.Println("Ctrl+C the remote client to stop the demo")
- } else if connectionState == webrtc.ICEConnectionStateFailed ||
- connectionState == webrtc.ICEConnectionStateDisconnected {
- closeErr := oggFile.Close()
- if closeErr != nil {
+ } else if connectionState == webrtc.ICEConnectionStateFailed {
+ if closeErr := oggFile.Close(); closeErr != nil {
panic(closeErr)
}
- closeErr = ivfFile.Close()
- if closeErr != nil {
+ if closeErr := ivfFile.Close(); closeErr != nil {
panic(closeErr)
}
fmt.Println("Done writing media files")
+
+ // Gracefully shutdown the peer connection
+ if closeErr := peerConnection.Close(); closeErr != nil {
+ panic(closeErr)
+ }
+
os.Exit(0)
}
})
diff --git a/examples/simulcast/README.md b/examples/simulcast/README.md
index d48bf11b869..7d7ed4b41d4 100644
--- a/examples/simulcast/README.md
+++ b/examples/simulcast/README.md
@@ -13,10 +13,12 @@ go get github.com/pion/webrtc/v3/examples/simulcast
```
### Open simulcast example page
-[jsfiddle.net](https://jsfiddle.net/rxk4bftc) you should see two text-areas and a 'Start Session' button.
+[jsfiddle.net](https://jsfiddle.net/zLebmv41/1/) you should see two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`.
### Run simulcast, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser, copy that and:
+In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
+We will use this value in the next step.
+
#### Linux/macOS
Run `echo $BROWSER_SDP | simulcast`
#### Windows
diff --git a/examples/simulcast/jsfiddle/demo.html b/examples/simulcast/jsfiddle/demo.html
index 1377fcd2a37..4d3a9a3f4d4 100644
--- a/examples/simulcast/jsfiddle/demo.html
+++ b/examples/simulcast/jsfiddle/demo.html
@@ -1,6 +1,11 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/simulcast/jsfiddle/demo.js b/examples/simulcast/jsfiddle/demo.js
index e8ecc0457d9..cc0ebf3de89 100644
--- a/examples/simulcast/jsfiddle/demo.js
+++ b/examples/simulcast/jsfiddle/demo.js
@@ -1,92 +1,107 @@
+/* eslint-env browser */
+
// Create peer conn
const pc = new RTCPeerConnection({
- iceServers: [
- {
- urls: "stun:stun.l.google.com:19302",
- },
- ],
-});
+ iceServers: [{
+ urls: 'stun:stun.l.google.com:19302'
+ }]
+})
pc.oniceconnectionstatechange = (e) => {
- console.log("connection state change", pc.iceConnectionState);
-};
+ console.log('connection state change', pc.iceConnectionState)
+}
pc.onicecandidate = (event) => {
if (event.candidate === null) {
- document.getElementById("localSessionDescription").value = btoa(
+ document.getElementById('localSessionDescription').value = btoa(
JSON.stringify(pc.localDescription)
- );
+ )
}
-};
+}
pc.onnegotiationneeded = (e) =>
pc
.createOffer()
.then((d) => pc.setLocalDescription(d))
- .catch(console.error);
+ .catch(console.error)
pc.ontrack = (event) => {
- console.log("Got track event", event);
- let video = document.createElement("video");
- video.srcObject = event.streams[0];
- video.autoplay = true;
- video.width = "500";
- let label = document.createElement("div");
- label.textContent = event.streams[0].id;
- document.getElementById("serverVideos").appendChild(label);
- document.getElementById("serverVideos").appendChild(video);
-};
+ console.log('Got track event', event)
+ const video = document.createElement('video')
+ video.srcObject = event.streams[0]
+ video.autoplay = true
+ video.width = '500'
+ const label = document.createElement('div')
+ label.textContent = event.streams[0].id
+ document.getElementById('serverVideos').appendChild(label)
+ document.getElementById('serverVideos').appendChild(video)
+}
navigator.mediaDevices
.getUserMedia({
video: {
width: {
- ideal: 4096,
+ ideal: 4096
},
height: {
- ideal: 2160,
+ ideal: 2160
},
frameRate: {
ideal: 60,
- min: 10,
- },
+ min: 10
+ }
},
- audio: false,
+ audio: false
})
.then((stream) => {
- document.getElementById("browserVideo").srcObject = stream;
+ document.getElementById('browserVideo').srcObject = stream
pc.addTransceiver(stream.getVideoTracks()[0], {
- direction: "sendonly",
+ direction: 'sendonly',
streams: [stream],
sendEncodings: [
// for firefox order matters... first high resolution, then scaled resolutions...
{
- rid: "f",
+ rid: 'f'
},
{
- rid: "h",
- scaleResolutionDownBy: 2.0,
+ rid: 'h',
+ scaleResolutionDownBy: 2.0
},
{
- rid: "q",
- scaleResolutionDownBy: 4.0,
- },
- ],
- });
- pc.addTransceiver("video");
- pc.addTransceiver("video");
- pc.addTransceiver("video");
- });
+ rid: 'q',
+ scaleResolutionDownBy: 4.0
+ }
+ ]
+ })
+ pc.addTransceiver('video')
+ pc.addTransceiver('video')
+ pc.addTransceiver('video')
+ })
window.startSession = () => {
- const sd = document.getElementById("remoteSessionDescription").value;
- if (sd === "") {
- return alert("Session Description must not be empty");
+ const sd = document.getElementById('remoteSessionDescription').value
+ if (sd === '') {
+ return alert('Session Description must not be empty')
}
try {
- console.log("answer", JSON.parse(atob(sd)));
- pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
+ console.log('answer', JSON.parse(atob(sd)))
+ pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))))
} catch (e) {
- alert(e);
+ alert(e)
+ }
+}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ console.log('Copying SDP was ' + msg)
+ } catch (err) {
+ console.log('Unable to copy SDP ' + err)
}
-};
+}
diff --git a/examples/simulcast/main.go b/examples/simulcast/main.go
index e035907c3a0..807ce135782 100644
--- a/examples/simulcast/main.go
+++ b/examples/simulcast/main.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package main
@@ -6,6 +7,7 @@ import (
"errors"
"fmt"
"io"
+ "os"
"time"
"github.com/pion/interceptor"
@@ -57,23 +59,28 @@ func main() {
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
outputTracks := map[string]*webrtc.TrackLocalStaticRTP{}
// Create Track that we send video back to browser on
- outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_q", "pion_q")
+ outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_q", "pion_q")
if err != nil {
panic(err)
}
outputTracks["q"] = outputTrack
- outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_h", "pion_h")
+ outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_h", "pion_h")
if err != nil {
panic(err)
}
outputTracks["h"] = outputTrack
- outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video_f", "pion_f")
+ outputTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video_f", "pion_f")
if err != nil {
panic(err)
}
@@ -140,9 +147,19 @@ func main() {
}
}
})
- // Set the handler for ICE connection state and update chan if connected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("Connection State has changed %s \n", connectionState.String())
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
})
// Create an answer
diff --git a/examples/swap-tracks/README.md b/examples/swap-tracks/README.md
index d8a8627522b..c278f8a7fb9 100644
--- a/examples/swap-tracks/README.md
+++ b/examples/swap-tracks/README.md
@@ -9,10 +9,12 @@ go get github.com/pion/webrtc/v3/examples/swap-tracks
```
### Open swap-tracks example page
-[jsfiddle.net](https://jsfiddle.net/dzc17fga/) you should see two text-areas and a 'Start Session' button.
+[jsfiddle.net](https://jsfiddle.net/39w24tr6/1/) you should see two text-areas and two buttons: `Copy browser SDP to clipboard`, `Start Session`.
### Run swap-tracks, with your browsers SessionDescription as stdin
-In the jsfiddle the top textarea is your browser, copy that and:
+In the jsfiddle the top textarea is your browser's Session Description. Press `Copy browser SDP to clipboard` or copy the base64 string manually.
+We will use this value in the next step.
+
#### Linux/macOS
Run `echo $BROWSER_SDP | swap-tracks`
#### Windows
diff --git a/examples/swap-tracks/jsfiddle/demo.html b/examples/swap-tracks/jsfiddle/demo.html
index 73b7d821824..6fea3193bc0 100644
--- a/examples/swap-tracks/jsfiddle/demo.html
+++ b/examples/swap-tracks/jsfiddle/demo.html
@@ -1,5 +1,10 @@
Browser base64 Session Description
+
+
+
Golang base64 Session Description
diff --git a/examples/swap-tracks/jsfiddle/demo.js b/examples/swap-tracks/jsfiddle/demo.js
index e34744726d8..1aed709c583 100644
--- a/examples/swap-tracks/jsfiddle/demo.js
+++ b/examples/swap-tracks/jsfiddle/demo.js
@@ -1,3 +1,4 @@
+/* eslint-env browser */
// Create peer conn
const pc = new RTCPeerConnection({
@@ -38,7 +39,7 @@ requestAnimationFrame(() => drawCircle(document.getElementById('canvasOne').getC
requestAnimationFrame(() => drawCircle(document.getElementById('canvasTwo').getContext('2d'), '#cf635f', 0))
requestAnimationFrame(() => drawCircle(document.getElementById('canvasThree').getContext('2d'), '#46c240', 0))
-function drawCircle(ctx, color, angle) {
+function drawCircle (ctx, color, angle) {
// Background
ctx.clearRect(0, 0, 200, 200)
ctx.fillStyle = '#eeeeee'
@@ -65,4 +66,19 @@ window.startSession = () => {
} catch (e) {
alert(e)
}
-}
\ No newline at end of file
+}
+
+window.copySDP = () => {
+ const browserSDP = document.getElementById('localSessionDescription')
+
+ browserSDP.focus()
+ browserSDP.select()
+
+ try {
+ const successful = document.execCommand('copy')
+ const msg = successful ? 'successful' : 'unsuccessful'
+ console.log('Copying SDP was ' + msg)
+ } catch (err) {
+ console.log('Unable to copy SDP ' + err)
+ }
+}
diff --git a/examples/swap-tracks/main.go b/examples/swap-tracks/main.go
index df14458cf95..635ca3f1f30 100644
--- a/examples/swap-tracks/main.go
+++ b/examples/swap-tracks/main.go
@@ -1,8 +1,10 @@
+//go:build !js
// +build !js
package main
import (
+ "context"
"errors"
"fmt"
"io"
@@ -30,9 +32,14 @@ func main() { // nolint:gocognit
if err != nil {
panic(err)
}
+ defer func() {
+ if cErr := peerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close peerConnection: %v\n", cErr)
+ }
+ }()
// Create Track that we send video back to browser on
- outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ outputTrack, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion")
if err != nil {
panic(err)
}
@@ -114,9 +121,20 @@ func main() { // nolint:gocognit
}
}
})
- // Set the handler for ICE connection state and update chan if connected
- peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
- fmt.Printf("Connection State has changed %s \n", connectionState.String())
+
+ ctx, done := context.WithCancel(context.Background())
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ done()
+ }
})
// Create an answer
@@ -153,7 +171,12 @@ func main() { // nolint:gocognit
// Keep an increasing sequence number
packet.SequenceNumber = i
// Write out the packet, ignoring closed pipe if nobody is listening
- if err := outputTrack.WriteRTP(packet); err != nil && !errors.Is(err, io.ErrClosedPipe) {
+ if err := outputTrack.WriteRTP(packet); err != nil {
+ if errors.Is(err, io.ErrClosedPipe) {
+ // The peerConnection has been closed.
+ return
+ }
+
panic(err)
}
}
@@ -162,6 +185,12 @@ func main() { // nolint:gocognit
// Wait for connection, then rotate the track every 5s
fmt.Printf("Waiting for connection\n")
for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+
// We haven't gotten any tracks yet
if trackCount == 0 {
continue
diff --git a/examples/trickle-ice/README.md b/examples/trickle-ice/README.md
new file mode 100644
index 00000000000..ff4342370ab
--- /dev/null
+++ b/examples/trickle-ice/README.md
@@ -0,0 +1,29 @@
+# trickle-ice
+trickle-ice demonstrates Pion WebRTC's Trickle ICE APIs. ICE is the subsystem WebRTC uses to establish connectivity.
+
+Trickle ICE is the process of sharing addresses as soon as they are gathered. This parallelizes
+establishing a connection with a remote peer and starting sessions with TURN servers. Using Trickle ICE
+can dramatically reduce the amount of time it takes to establish a WebRTC connection.
+
+Trickle ICE isn't mandatory to use, but highly recommended.
+
+## Instructions
+
+### Download trickle-ice
+This example requires you to clone the repo since it is serving static HTML.
+
+```
+mkdir -p $GOPATH/src/github.com/pion
+cd $GOPATH/src/github.com/pion
+git clone https://github.com/pion/webrtc.git
+cd webrtc/examples/trickle-ice
+```
+
+### Run trickle-ice
+Execute `go run *.go`
+
+### Open the Web UI
+Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection.
+
+## Note
+Congrats, you have used Pion WebRTC! Now start building something cool
diff --git a/examples/trickle-ice/index.html b/examples/trickle-ice/index.html
new file mode 100644
index 00000000000..6c3dfb944b3
--- /dev/null
+++ b/examples/trickle-ice/index.html
@@ -0,0 +1,65 @@
+
+
+ trickle-ice
+
+
+
+
ICE Connection States
+
+
+
Inbound DataChannel Messages
+
+
+
+
+
diff --git a/examples/trickle-ice/main.go b/examples/trickle-ice/main.go
new file mode 100644
index 00000000000..98a540f3e68
--- /dev/null
+++ b/examples/trickle-ice/main.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/pion/webrtc/v3"
+ "golang.org/x/net/websocket"
+)
+
+// websocketServer is called for every new inbound WebSocket
+func websocketServer(ws *websocket.Conn) { // nolint:gocognit
+ // Create a new RTCPeerConnection
+ peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{})
+ if err != nil {
+ panic(err)
+ }
+
+ // When Pion gathers a new ICE Candidate send it to the client. This is how
+ // ice trickle is implemented. Everytime we have a new candidate available we send
+ // it as soon as it is ready. We don't wait to emit a Offer/Answer until they are
+ // all available
+ peerConnection.OnICECandidate(func(c *webrtc.ICECandidate) {
+ if c == nil {
+ return
+ }
+
+ outbound, marshalErr := json.Marshal(c.ToJSON())
+ if marshalErr != nil {
+ panic(marshalErr)
+ }
+
+ if _, err = ws.Write(outbound); err != nil {
+ panic(err)
+ }
+ })
+
+ // Set the handler for ICE connection state
+ // This will notify you when the peer has connected/disconnected
+ peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
+ fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
+ })
+
+ // Send the current time via a DataChannel to the remote peer every 3 seconds
+ peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
+ d.OnOpen(func() {
+ for range time.Tick(time.Second * 3) {
+ if err = d.SendText(time.Now().String()); err != nil {
+ panic(err)
+ }
+ }
+ })
+ })
+
+ buf := make([]byte, 1500)
+ for {
+ // Read each inbound WebSocket Message
+ n, err := ws.Read(buf)
+ if err != nil {
+ panic(err)
+ }
+
+ // Unmarshal each inbound WebSocket message
+ var (
+ candidate webrtc.ICECandidateInit
+ offer webrtc.SessionDescription
+ )
+
+ switch {
+ // Attempt to unmarshal as a SessionDescription. If the SDP field is empty
+ // assume it is not one.
+ case json.Unmarshal(buf[:n], &offer) == nil && offer.SDP != "":
+ if err = peerConnection.SetRemoteDescription(offer); err != nil {
+ panic(err)
+ }
+
+ answer, answerErr := peerConnection.CreateAnswer(nil)
+ if answerErr != nil {
+ panic(answerErr)
+ }
+
+ if err = peerConnection.SetLocalDescription(answer); err != nil {
+ panic(err)
+ }
+
+ outbound, marshalErr := json.Marshal(answer)
+ if marshalErr != nil {
+ panic(marshalErr)
+ }
+
+ if _, err = ws.Write(outbound); err != nil {
+ panic(err)
+ }
+ // Attempt to unmarshal as a ICECandidateInit. If the candidate field is empty
+ // assume it is not one.
+ case json.Unmarshal(buf[:n], &candidate) == nil && candidate.Candidate != "":
+ if err = peerConnection.AddICECandidate(candidate); err != nil {
+ panic(err)
+ }
+ default:
+ panic("Unknown message")
+ }
+ }
+}
+
+func main() {
+ http.Handle("/", http.FileServer(http.Dir(".")))
+ http.Handle("/websocket", websocket.Handler(websocketServer))
+
+ fmt.Println("Open http://localhost:8080 to access this demo")
+ panic(http.ListenAndServe(":8080", nil))
+}
diff --git a/examples/vnet/show-network-usage/main.go b/examples/vnet/show-network-usage/main.go
index 91efd5524f8..b3430d5c5c9 100644
--- a/examples/vnet/show-network-usage/main.go
+++ b/examples/vnet/show-network-usage/main.go
@@ -1,10 +1,13 @@
+//go:build !js
// +build !js
package main
import (
+ "fmt"
"log"
"net"
+ "os"
"sync/atomic"
"time"
@@ -106,9 +109,47 @@ func main() {
offerPeerConnection, err := offerAPI.NewPeerConnection(webrtc.Configuration{})
panicIfError(err)
+ defer func() {
+ if cErr := offerPeerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close offerPeerConnection: %v\n", cErr)
+ }
+ }()
answerPeerConnection, err := answerAPI.NewPeerConnection(webrtc.Configuration{})
panicIfError(err)
+ defer func() {
+ if cErr := answerPeerConnection.Close(); cErr != nil {
+ fmt.Printf("cannot close answerPeerConnection: %v\n", cErr)
+ }
+ }()
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ offerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (offerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
+
+ // Set the handler for Peer connection state
+ // This will notify you when the peer has connected/disconnected
+ answerPeerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
+ fmt.Printf("Peer Connection State has changed: %s (answerer)\n", s.String())
+
+ if s == webrtc.PeerConnectionStateFailed {
+ // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart.
+ // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout.
+ // Note that the PeerConnection may come back from PeerConnectionStateDisconnected.
+ fmt.Println("Peer Connection has gone to failed exiting")
+ os.Exit(0)
+ }
+ })
// Set ICE Candidate handler. As soon as a PeerConnection has gathered a candidate
// send it to the other peer
@@ -158,6 +199,7 @@ func main() {
panicIfError(answerPeerConnection.SetLocalDescription(answer))
panicIfError(offerPeerConnection.SetRemoteDescription(answer))
+ // Block forever
select {}
}
diff --git a/fmtp.go b/fmtp.go
deleted file mode 100644
index 9d4be5da645..00000000000
--- a/fmtp.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package webrtc
-
-import (
- "strings"
-)
-
-type fmtp map[string]string
-
-// parseFmtp parses fmtp string.
-func parseFmtp(line string) fmtp {
- f := fmtp{}
- for _, p := range strings.Split(line, ";") {
- pp := strings.SplitN(strings.TrimSpace(p), "=", 2)
- key := strings.ToLower(pp[0])
- var value string
- if len(pp) > 1 {
- value = pp[1]
- }
- f[key] = value
- }
- return f
-}
-
-// fmtpConsist checks that two FMTP parameters are not inconsistent.
-func fmtpConsist(a, b fmtp) bool {
- for k, v := range a {
- if vb, ok := b[k]; ok && !strings.EqualFold(vb, v) {
- return false
- }
- }
- for k, v := range b {
- if va, ok := a[k]; ok && !strings.EqualFold(va, v) {
- return false
- }
- }
- return true
-}
diff --git a/go.mod b/go.mod
index 979edd56e01..054639fc0f5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,21 +3,21 @@ module github.com/pion/webrtc/v3
go 1.13
require (
- github.com/onsi/ginkgo v1.16.1 // indirect
- github.com/onsi/gomega v1.11.0 // indirect
- github.com/pion/datachannel v1.4.21
- github.com/pion/dtls/v2 v2.0.9
- github.com/pion/ice/v2 v2.1.7
- github.com/pion/interceptor v0.0.12
+ github.com/onsi/ginkgo v1.16.5 // indirect
+ github.com/onsi/gomega v1.17.0 // indirect
+ github.com/pion/datachannel v1.5.2
+ github.com/pion/dtls/v2 v2.1.3
+ github.com/pion/ice/v2 v2.2.2
+ github.com/pion/interceptor v0.1.8
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.5
- github.com/pion/sctp v1.7.12
+ github.com/pion/rtcp v1.2.9
+ github.com/pion/rtp v1.7.4
+ github.com/pion/sctp v1.8.2
github.com/pion/sdp/v3 v3.0.4
- github.com/pion/srtp/v2 v2.0.2
- github.com/pion/transport v0.12.3
+ github.com/pion/srtp/v2 v2.0.6-0.20220304062923-d55e443f8e15
+ github.com/pion/transport v0.13.0
github.com/sclevine/agouti v3.0.0+incompatible
github.com/stretchr/testify v1.7.0
- golang.org/x/net v0.0.0-20210420210106-798c2154c571
+ golang.org/x/net v0.0.0-20220225172249-27dd8689420f
)
diff --git a/go.sum b/go.sum
index d1645f84041..2b9596c7fb8 100644
--- a/go.sum
+++ b/go.sum
@@ -12,14 +12,16 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/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=
@@ -31,50 +33,48 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
-github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
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.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
-github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
-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.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8=
-github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho=
-github.com/pion/ice/v2 v2.1.7 h1:FjgDfUNrVYTxQabJrkBX6ld12tvYbgzHenqPh3PJF6E=
-github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0=
-github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE=
-github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4=
+github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
+github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
+github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
+github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
+github.com/pion/ice/v2 v2.2.2 h1:UfmAslxZ0u0itVjA4x7aw7WeQIv22FdF8VjW9cM+74g=
+github.com/pion/ice/v2 v2.2.2/go.mod h1:vLI7dFqxw8zMSb9J+ca74XU7JjLhddgfQB9+BbTydCo=
+github.com/pion/interceptor v0.1.8 h1:5K27KMw8enTB1jVDFrjadK8sZjI5JbPJ91OVfiih5fE=
+github.com/pion/interceptor v0.1.8/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
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.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
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/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/rtp v1.6.5 h1:o2cZf8OascA5HF/b0PAbTxRKvOWxTQxWYt7SlToxFGI=
-github.com/pion/rtp v1.6.5/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.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
-github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
+github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
+github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
+github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
+github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
+github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
+github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
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.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU=
-github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
+github.com/pion/srtp/v2 v2.0.6-0.20220304062923-d55e443f8e15 h1:qFdF9b185eGmlBr1OyizpP4a8RnS8tCT8MztCKofuQ8=
+github.com/pion/srtp/v2 v2.0.6-0.20220304062923-d55e443f8e15/go.mod h1:Kp632EOcOX2wtB6njSY+oRamReUfEYINuaGmKIMHVlA=
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.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
-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/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
+github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
+github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
+github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-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=
@@ -88,22 +88,22 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20190620200207-3b0461eec859/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg=
-golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -117,14 +117,17 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ=
-golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -138,8 +141,10 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
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=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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=
diff --git a/ice_go.go b/ice_go.go
index 897ed0da37e..992cd9cb45d 100644
--- a/ice_go.go
+++ b/ice_go.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/icegatherer.go b/icegatherer.go
index e071491d04d..c3a05408067 100644
--- a/icegatherer.go
+++ b/icegatherer.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/icegatherer_test.go b/icegatherer_test.go
index 23b571ed8c7..9e37a59da6c 100644
--- a/icegatherer_test.go
+++ b/icegatherer_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/icemux.go b/icemux.go
index 1d5ba1a7969..8291a6c8b98 100644
--- a/icemux.go
+++ b/icemux.go
@@ -19,7 +19,7 @@ func NewICETCPMux(logger logging.LeveledLogger, listener net.Listener, readBuffe
// NewICEUDPMux creates a new instance of ice.UDPMuxDefault. It allows many PeerConnections to be served
// by a single UDP Port.
-func NewICEUDPMux(logger logging.LeveledLogger, udpConn *net.UDPConn) ice.UDPMux {
+func NewICEUDPMux(logger logging.LeveledLogger, udpConn net.PacketConn) ice.UDPMux {
return ice.NewUDPMuxDefault(ice.UDPMuxParams{
UDPConn: udpConn,
Logger: logger,
diff --git a/iceserver.go b/iceserver.go
index 76146ece273..b83a9e8b380 100644
--- a/iceserver.go
+++ b/iceserver.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/iceserver_js.go b/iceserver_js.go
index 07b2dcba626..3f4f9c3a94c 100644
--- a/iceserver_js.go
+++ b/iceserver_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/iceserver_test.go b/iceserver_test.go
index c2e5deb3684..dafe1561288 100644
--- a/iceserver_test.go
+++ b/iceserver_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/icetransport.go b/icetransport.go
index d10c81b9202..31cc4c9a5c6 100644
--- a/icetransport.go
+++ b/icetransport.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -155,7 +156,7 @@ func (t *ICETransport) Start(gatherer *ICEGatherer, params ICEParameters, role *
config := mux.Config{
Conn: t.conn,
- BufferSize: receiveMTU,
+ BufferSize: int(t.gatherer.api.settingEngine.getReceiveMTU()),
LoggerFactory: t.loggerFactory,
}
t.mux = mux.NewMux(config)
diff --git a/icetransport_js.go b/icetransport_js.go
new file mode 100644
index 00000000000..095f354bb3d
--- /dev/null
+++ b/icetransport_js.go
@@ -0,0 +1,27 @@
+//go:build js && wasm
+// +build js,wasm
+
+package webrtc
+
+import "syscall/js"
+
+// ICETransport allows an application access to information about the ICE
+// transport over which packets are sent and received.
+type ICETransport struct {
+ // Pointer to the underlying JavaScript ICETransport object.
+ underlying js.Value
+}
+
+// GetSelectedCandidatePair returns the selected candidate pair on which packets are sent
+// if there is no selected pair nil is returned
+func (t *ICETransport) GetSelectedCandidatePair() (*ICECandidatePair, error) {
+ val := t.underlying.Call("getSelectedCandidatePair")
+ if val.IsNull() || val.IsUndefined() {
+ return nil, nil
+ }
+
+ return NewICECandidatePair(
+ valueToICECandidate(val.Get("local")),
+ valueToICECandidate(val.Get("remote")),
+ ), nil
+}
diff --git a/icetransport_test.go b/icetransport_test.go
index afd7c7e81ee..98c4eb35661 100644
--- a/icetransport_test.go
+++ b/icetransport_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/interceptor.go b/interceptor.go
index eff94962d84..e93fc7666ae 100644
--- a/interceptor.go
+++ b/interceptor.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -8,7 +9,9 @@ import (
"github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/nack"
"github.com/pion/interceptor/pkg/report"
+ "github.com/pion/interceptor/pkg/twcc"
"github.com/pion/rtp"
+ "github.com/pion/sdp/v3"
)
// RegisterDefaultInterceptors will register some useful interceptors.
@@ -23,6 +26,10 @@ func RegisterDefaultInterceptors(mediaEngine *MediaEngine, interceptorRegistry *
return err
}
+ if err := ConfigureTWCCSender(mediaEngine, interceptorRegistry); err != nil {
+ return err
+ }
+
return nil
}
@@ -62,6 +69,47 @@ func ConfigureNack(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Re
return nil
}
+// ConfigureTWCCHeaderExtensionSender will setup everything necessary for adding
+// a TWCC header extension to outgoing RTP packets. This will allow the remote peer to generate TWCC reports.
+func ConfigureTWCCHeaderExtensionSender(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error {
+ if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeVideo); err != nil {
+ return err
+ }
+
+ if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeAudio); err != nil {
+ return err
+ }
+
+ i, err := twcc.NewHeaderExtensionInterceptor()
+ if err != nil {
+ return err
+ }
+
+ interceptorRegistry.Add(i)
+ return nil
+}
+
+// ConfigureTWCCSender will setup everything necessary for generating TWCC reports.
+func ConfigureTWCCSender(mediaEngine *MediaEngine, interceptorRegistry *interceptor.Registry) error {
+ mediaEngine.RegisterFeedback(RTCPFeedback{Type: TypeRTCPFBTransportCC}, RTPCodecTypeVideo)
+ if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeVideo); err != nil {
+ return err
+ }
+
+ mediaEngine.RegisterFeedback(RTCPFeedback{Type: TypeRTCPFBTransportCC}, RTPCodecTypeAudio)
+ if err := mediaEngine.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, RTPCodecTypeAudio); err != nil {
+ return err
+ }
+
+ generator, err := twcc.NewSenderInterceptor()
+ if err != nil {
+ return err
+ }
+
+ interceptorRegistry.Add(generator)
+ return nil
+}
+
type interceptorToTrackLocalWriter struct{ interceptor atomic.Value } // interceptor.RTPWriter }
func (i *interceptorToTrackLocalWriter) WriteRTP(header *rtp.Header, payload []byte) (int, error) {
@@ -81,7 +129,7 @@ func (i *interceptorToTrackLocalWriter) Write(b []byte) (int, error) {
return i.WriteRTP(&packet.Header, packet.Payload)
}
-func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCodecCapability, webrtcHeaderExtensions []RTPHeaderExtensionParameter) interceptor.StreamInfo {
+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})
@@ -92,7 +140,7 @@ func createStreamInfo(id string, ssrc SSRC, payloadType PayloadType, codec RTPCo
feedbacks = append(feedbacks, interceptor.RTCPFeedback{Type: f.Type, Parameter: f.Parameter})
}
- return interceptor.StreamInfo{
+ return &interceptor.StreamInfo{
ID: id,
Attributes: interceptor.Attributes{},
SSRC: uint32(ssrc),
diff --git a/interceptor_test.go b/interceptor_test.go
index f7b9ce69ab4..bce700e2552 100644
--- a/interceptor_test.go
+++ b/interceptor_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -33,26 +34,30 @@ func TestPeerConnection_Interceptor(t *testing.T) {
assert.NoError(t, m.RegisterDefaultCodecs())
ir := &interceptor.Registry{}
- ir.Add(&mock_interceptor.Interceptor{
- BindLocalStreamFn: func(_ *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter {
- return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) {
- // set extension on outgoing packet
- header.Extension = true
- header.ExtensionProfile = 0xBEDE
- assert.NoError(t, header.SetExtension(2, []byte("foo")))
-
- return writer.Write(header, payload, attributes)
- })
- },
- BindRemoteStreamFn: func(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader {
- return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
- if a == nil {
- a = interceptor.Attributes{}
- }
-
- a.Set("attribute", "value")
- return reader.Read(b, a)
- })
+ ir.Add(&mock_interceptor.Factory{
+ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) {
+ return &mock_interceptor.Interceptor{
+ BindLocalStreamFn: func(_ *interceptor.StreamInfo, writer interceptor.RTPWriter) interceptor.RTPWriter {
+ return interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) {
+ // set extension on outgoing packet
+ header.Extension = true
+ header.ExtensionProfile = 0xBEDE
+ assert.NoError(t, header.SetExtension(2, []byte("foo")))
+
+ return writer.Write(header, payload, attributes)
+ })
+ },
+ BindRemoteStreamFn: func(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader {
+ return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
+ if a == nil {
+ a = interceptor.Attributes{}
+ }
+
+ a.Set("attribute", "value")
+ return reader.Read(b, a)
+ })
+ },
+ }, nil
},
})
@@ -65,7 +70,7 @@ func TestPeerConnection_Interceptor(t *testing.T) {
offerer := createPC()
answerer := createPC()
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = offerer.AddTrack(track)
@@ -148,12 +153,14 @@ func Test_Interceptor_BindUnbind(t *testing.T) {
},
}
ir := &interceptor.Registry{}
- ir.Add(mockInterceptor)
+ ir.Add(&mock_interceptor.Factory{
+ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { return mockInterceptor, nil },
+ })
sender, receiver, err := NewAPI(WithMediaEngine(m), WithInterceptorRegistry(ir)).newPair(Configuration{})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = sender.AddTrack(track)
@@ -209,3 +216,24 @@ func Test_Interceptor_BindUnbind(t *testing.T) {
t.Errorf("CloseFn is expected to be called twice, but called %d times", cnt)
}
}
+
+func Test_InterceptorRegistry_Build(t *testing.T) {
+ registryBuildCount := 0
+
+ ir := &interceptor.Registry{}
+ ir.Add(&mock_interceptor.Factory{
+ NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) {
+ registryBuildCount++
+ return &interceptor.NoOp{}, nil
+ },
+ })
+
+ peerConnectionA, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ peerConnectionB, err := NewAPI(WithInterceptorRegistry(ir)).NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ assert.Equal(t, 2, registryBuildCount)
+ closePairNow(t, peerConnectionA, peerConnectionB)
+}
diff --git a/internal/fmtp/fmtp.go b/internal/fmtp/fmtp.go
new file mode 100644
index 00000000000..86057594acf
--- /dev/null
+++ b/internal/fmtp/fmtp.go
@@ -0,0 +1,92 @@
+// Package fmtp implements per codec parsing of fmtp lines
+package fmtp
+
+import (
+ "strings"
+)
+
+// FMTP interface for implementing custom
+// FMTP parsers based on MimeType
+type FMTP interface {
+ // MimeType returns the MimeType associated with
+ // the fmtp
+ MimeType() string
+ // Match compares two fmtp descriptions for
+ // compatibility based on the MimeType
+ Match(f FMTP) bool
+ // Parameter returns a value for the associated key
+ // if contained in the parsed fmtp string
+ Parameter(key string) (string, bool)
+}
+
+// Parse parses an fmtp string based on the MimeType
+func Parse(mimetype, line string) FMTP {
+ var f FMTP
+
+ parameters := make(map[string]string)
+
+ for _, p := range strings.Split(line, ";") {
+ pp := strings.SplitN(strings.TrimSpace(p), "=", 2)
+ key := strings.ToLower(pp[0])
+ var value string
+ if len(pp) > 1 {
+ value = pp[1]
+ }
+ parameters[key] = value
+ }
+
+ switch {
+ case strings.EqualFold(mimetype, "video/h264"):
+ f = &h264FMTP{
+ parameters: parameters,
+ }
+ default:
+ f = &genericFMTP{
+ mimeType: mimetype,
+ parameters: parameters,
+ }
+ }
+
+ return f
+}
+
+type genericFMTP struct {
+ mimeType string
+ parameters map[string]string
+}
+
+func (g *genericFMTP) MimeType() string {
+ return g.mimeType
+}
+
+// Match returns true if g and b are compatible fmtp descriptions
+// The generic implementation is used for MimeTypes that are not defined
+func (g *genericFMTP) Match(b FMTP) bool {
+ c, ok := b.(*genericFMTP)
+ if !ok {
+ return false
+ }
+
+ if g.mimeType != c.MimeType() {
+ return false
+ }
+
+ for k, v := range g.parameters {
+ if vb, ok := c.parameters[k]; ok && !strings.EqualFold(vb, v) {
+ return false
+ }
+ }
+
+ for k, v := range c.parameters {
+ if va, ok := g.parameters[k]; ok && !strings.EqualFold(va, v) {
+ return false
+ }
+ }
+
+ return true
+}
+
+func (g *genericFMTP) Parameter(key string) (string, bool) {
+ v, ok := g.parameters[key]
+ return v, ok
+}
diff --git a/fmtp_test.go b/internal/fmtp/fmtp_test.go
similarity index 65%
rename from fmtp_test.go
rename to internal/fmtp/fmtp_test.go
index 3f0a498e78b..0ec8b567346 100644
--- a/fmtp_test.go
+++ b/internal/fmtp/fmtp_test.go
@@ -1,54 +1,70 @@
-package webrtc
+package fmtp
import (
"reflect"
"testing"
)
-func TestParseFmtp(t *testing.T) {
+func TestGenericParseFmtp(t *testing.T) {
testCases := map[string]struct {
input string
- expected fmtp
+ expected FMTP
}{
"OneParam": {
input: "key-name=value",
- expected: fmtp{
- "key-name": "value",
+ expected: &genericFMTP{
+ mimeType: "generic",
+ parameters: map[string]string{
+ "key-name": "value",
+ },
},
},
"OneParamWithWhiteSpeces": {
input: "\tkey-name=value ",
- expected: fmtp{
- "key-name": "value",
+ expected: &genericFMTP{
+ mimeType: "generic",
+ parameters: map[string]string{
+ "key-name": "value",
+ },
},
},
"TwoParams": {
input: "key-name=value;key2=value2",
- expected: fmtp{
- "key-name": "value",
- "key2": "value2",
+ expected: &genericFMTP{
+ mimeType: "generic",
+ parameters: map[string]string{
+ "key-name": "value",
+ "key2": "value2",
+ },
},
},
"TwoParamsWithWhiteSpeces": {
input: "key-name=value; \n\tkey2=value2 ",
- expected: fmtp{
- "key-name": "value",
- "key2": "value2",
+ expected: &genericFMTP{
+ mimeType: "generic",
+ parameters: map[string]string{
+ "key-name": "value",
+ "key2": "value2",
+ },
},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
- f := parseFmtp(testCase.input)
+ f := Parse("generic", testCase.input)
if !reflect.DeepEqual(testCase.expected, f) {
t.Errorf("Expected Fmtp params: %v, got: %v", testCase.expected, f)
}
+
+ if f.MimeType() != "generic" {
+ t.Errorf("Expected MimeType of generic, got: %s", f.MimeType())
+ }
})
}
}
-func TestFmtpConsist(t *testing.T) {
+func TestGenericFmtpCompare(t *testing.T) {
consistString := map[bool]string{true: "consist", false: "inconsist"}
testCases := map[string]struct {
@@ -89,7 +105,18 @@ func TestFmtpConsist(t *testing.T) {
for name, testCase := range testCases {
testCase := testCase
check := func(t *testing.T, a, b string) {
- c := fmtpConsist(parseFmtp(a), parseFmtp(b))
+ aa := Parse("", a)
+ bb := Parse("", b)
+ c := aa.Match(bb)
+ if c != testCase.consist {
+ t.Errorf(
+ "'%s' and '%s' are expected to be %s, but treated as %s",
+ a, b, consistString[testCase.consist], consistString[c],
+ )
+ }
+
+ // test reverse case here
+ c = bb.Match(aa)
if c != testCase.consist {
t.Errorf(
"'%s' and '%s' are expected to be %s, but treated as %s",
@@ -100,8 +127,5 @@ func TestFmtpConsist(t *testing.T) {
t.Run(name, func(t *testing.T) {
check(t, testCase.a, testCase.b)
})
- t.Run(name+"_Reversed", func(t *testing.T) {
- check(t, testCase.b, testCase.a)
- })
}
}
diff --git a/internal/fmtp/h264.go b/internal/fmtp/h264.go
new file mode 100644
index 00000000000..5a79b9e64a4
--- /dev/null
+++ b/internal/fmtp/h264.go
@@ -0,0 +1,80 @@
+package fmtp
+
+import (
+ "encoding/hex"
+)
+
+func profileLevelIDMatches(a, b string) bool {
+ aa, err := hex.DecodeString(a)
+ if err != nil || len(aa) < 2 {
+ return false
+ }
+ bb, err := hex.DecodeString(b)
+ if err != nil || len(bb) < 2 {
+ return false
+ }
+ return aa[0] == bb[0] && aa[1] == bb[1]
+}
+
+type h264FMTP struct {
+ parameters map[string]string
+}
+
+func (h *h264FMTP) MimeType() string {
+ return "video/h264"
+}
+
+// Match returns true if h and b are compatible fmtp descriptions
+// Based on RFC6184 Section 8.2.2:
+// The parameters identifying a media format configuration for H.264
+// are profile-level-id and packetization-mode. These media format
+// configuration parameters (except for the level part of profile-
+// level-id) MUST be used symmetrically; that is, the answerer MUST
+// either maintain all configuration parameters or remove the media
+// format (payload type) completely if one or more of the parameter
+// values are not supported.
+// Informative note: The requirement for symmetric use does not
+// apply for the level part of profile-level-id and does not apply
+// for the other stream properties and capability parameters.
+func (h *h264FMTP) Match(b FMTP) bool {
+ c, ok := b.(*h264FMTP)
+ if !ok {
+ return false
+ }
+
+ // test packetization-mode
+ hpmode, hok := h.parameters["packetization-mode"]
+ if !hok {
+ return false
+ }
+ cpmode, cok := c.parameters["packetization-mode"]
+ if !cok {
+ return false
+ }
+
+ if hpmode != cpmode {
+ return false
+ }
+
+ // test profile-level-id
+ hplid, hok := h.parameters["profile-level-id"]
+ if !hok {
+ return false
+ }
+
+ cplid, cok := c.parameters["profile-level-id"]
+ if !cok {
+ return false
+ }
+
+ if !profileLevelIDMatches(hplid, cplid) {
+ return false
+ }
+
+ return true
+}
+
+func (h *h264FMTP) Parameter(key string) (string, bool) {
+ v, ok := h.parameters[key]
+ return v, ok
+}
diff --git a/internal/fmtp/h264_test.go b/internal/fmtp/h264_test.go
new file mode 100644
index 00000000000..1e8fc97d441
--- /dev/null
+++ b/internal/fmtp/h264_test.go
@@ -0,0 +1,142 @@
+package fmtp
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestH264FMTPParse(t *testing.T) {
+ testCases := map[string]struct {
+ input string
+ expected FMTP
+ }{
+ "OneParam": {
+ input: "key-name=value",
+ expected: &h264FMTP{
+ parameters: map[string]string{
+ "key-name": "value",
+ },
+ },
+ },
+ "OneParamWithWhiteSpeces": {
+ input: "\tkey-name=value ",
+ expected: &h264FMTP{
+ parameters: map[string]string{
+ "key-name": "value",
+ },
+ },
+ },
+ "TwoParams": {
+ input: "key-name=value;key2=value2",
+ expected: &h264FMTP{
+ parameters: map[string]string{
+ "key-name": "value",
+ "key2": "value2",
+ },
+ },
+ },
+ "TwoParamsWithWhiteSpeces": {
+ input: "key-name=value; \n\tkey2=value2 ",
+ expected: &h264FMTP{
+ parameters: map[string]string{
+ "key-name": "value",
+ "key2": "value2",
+ },
+ },
+ },
+ }
+ for name, testCase := range testCases {
+ testCase := testCase
+ t.Run(name, func(t *testing.T) {
+ f := Parse("video/h264", testCase.input)
+ if !reflect.DeepEqual(testCase.expected, f) {
+ t.Errorf("Expected Fmtp params: %v, got: %v", testCase.expected, f)
+ }
+
+ if f.MimeType() != "video/h264" {
+ t.Errorf("Expected MimeType of video/h264, got: %s", f.MimeType())
+ }
+ })
+ }
+}
+
+func TestH264FMTPCompare(t *testing.T) {
+ consistString := map[bool]string{true: "consist", false: "inconsist"}
+
+ testCases := map[string]struct {
+ a, b string
+ consist bool
+ }{
+ "Equal": {
+ a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ b: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ consist: true,
+ },
+ "EqualWithWhitespaceVariants": {
+ a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ b: " level-asymmetry-allowed=1; \npacketization-mode=1;\t\nprofile-level-id=42e01f",
+ consist: true,
+ },
+ "EqualWithCase": {
+ a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ b: "level-asymmetry-allowed=1;packetization-mode=1;PROFILE-LEVEL-ID=42e01f",
+ consist: true,
+ },
+ "OneHasExtraParam": {
+ a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ b: "packetization-mode=1;profile-level-id=42e01f",
+ consist: true,
+ },
+ "DifferentProfileLevelIDVersions": {
+ a: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
+ b: "packetization-mode=1;profile-level-id=42e029",
+ consist: true,
+ },
+ "Inconsistent": {
+ a: "packetization-mode=1;profile-level-id=42e029",
+ b: "packetization-mode=0;profile-level-id=42e029",
+ consist: false,
+ },
+ "Inconsistent_MissingPacketizationMode": {
+ a: "packetization-mode=1;profile-level-id=42e029",
+ b: "profile-level-id=42e029",
+ consist: false,
+ },
+ "Inconsistent_MissingProfileLevelID": {
+ a: "packetization-mode=1;profile-level-id=42e029",
+ b: "packetization-mode=1",
+ consist: false,
+ },
+ "Inconsistent_InvalidProfileLevelID": {
+ a: "packetization-mode=1;profile-level-id=42e029",
+ b: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=41e029",
+ consist: false,
+ },
+ }
+ for name, testCase := range testCases {
+ testCase := testCase
+ check := func(t *testing.T, a, b string) {
+ aa := Parse("video/h264", a)
+ bb := Parse("video/h264", b)
+ c := aa.Match(bb)
+ if c != testCase.consist {
+ t.Errorf(
+ "'%s' and '%s' are expected to be %s, but treated as %s",
+ a, b, consistString[testCase.consist], consistString[c],
+ )
+ }
+
+ // test reverse case here
+ c = bb.Match(aa)
+ if c != testCase.consist {
+ t.Errorf(
+ "'%s' and '%s' are expected to be %s, but treated as %s",
+ a, b, consistString[testCase.consist], consistString[c],
+ )
+ }
+ }
+ t.Run(name, func(t *testing.T) {
+ check(t, testCase.a, testCase.b)
+ })
+ }
+}
diff --git a/internal/mux/mux_test.go b/internal/mux/mux_test.go
index e8baaa09e85..6b6a23dadee 100644
--- a/internal/mux/mux_test.go
+++ b/internal/mux/mux_test.go
@@ -106,6 +106,10 @@ func (m *muxErrorConn) Read(b []byte) (n int, err error) {
pion/webrtc#1720
*/
func TestNonFatalRead(t *testing.T) {
+ // Limit runtime in case of deadlocks
+ lim := test.TimeOut(time.Second * 20)
+ defer lim.Stop()
+
expectedData := []byte("expectedData")
// In memory pipe
diff --git a/js_compare.go b/js_compare.go
deleted file mode 100644
index 31e39802b4d..00000000000
--- a/js_compare.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// +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()
-}
diff --git a/js_compare_legacy.go b/js_compare_legacy.go
deleted file mode 100644
index e2e247a0e12..00000000000
--- a/js_compare_legacy.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// +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()
-}
diff --git a/js_utils.go b/js_utils.go
index 25e89396b55..7e40da9a64b 100644
--- a/js_utils.go
+++ b/js_utils.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
@@ -22,7 +23,6 @@ func awaitPromise(promise js.Value) (js.Value, error) {
return js.Undefined()
})
defer thenFunc.Release()
- promise.Call("then", thenFunc)
catchFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
@@ -31,7 +31,8 @@ func awaitPromise(promise js.Value) (js.Value, error) {
return js.Undefined()
})
defer catchFunc.Release()
- promise.Call("catch", catchFunc)
+
+ promise.Call("then", thenFunc).Call("catch", catchFunc)
select {
case result := <-resultsChan:
@@ -42,7 +43,7 @@ func awaitPromise(promise js.Value) (js.Value, error) {
}
func valueToUint16Pointer(val js.Value) *uint16 {
- if jsValueIsNull(val) || jsValueIsUndefined(val) {
+ if val.IsNull() || val.IsUndefined() {
return nil
}
convertedVal := uint16(val.Int())
@@ -50,7 +51,7 @@ func valueToUint16Pointer(val js.Value) *uint16 {
}
func valueToStringPointer(val js.Value) *string {
- if jsValueIsNull(val) || jsValueIsUndefined(val) {
+ if val.IsNull() || val.IsUndefined() {
return nil
}
stringVal := val.String()
@@ -79,28 +80,28 @@ func interfaceToValueOrUndefined(val interface{}) js.Value {
}
func valueToStringOrZero(val js.Value) string {
- if jsValueIsUndefined(val) || jsValueIsNull(val) {
+ if val.IsUndefined() || val.IsNull() {
return ""
}
return val.String()
}
func valueToUint8OrZero(val js.Value) uint8 {
- if jsValueIsUndefined(val) || jsValueIsNull(val) {
+ if val.IsUndefined() || val.IsNull() {
return 0
}
return uint8(val.Int())
}
func valueToUint16OrZero(val js.Value) uint16 {
- if jsValueIsNull(val) || jsValueIsUndefined(val) {
+ if val.IsNull() || val.IsUndefined() {
return 0
}
return uint16(val.Int())
}
func valueToUint32OrZero(val js.Value) uint32 {
- if jsValueIsNull(val) || jsValueIsUndefined(val) {
+ if val.IsNull() || val.IsUndefined() {
return 0
}
return uint32(val.Int())
diff --git a/mediaengine.go b/mediaengine.go
index c7384fe4f9c..bdab6b9fb60 100644
--- a/mediaengine.go
+++ b/mediaengine.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -6,17 +7,22 @@ import (
"fmt"
"strconv"
"strings"
+ "sync"
"time"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
"github.com/pion/sdp/v3"
+ "github.com/pion/webrtc/v3/internal/fmtp"
)
const (
// MimeTypeH264 H264 MIME type.
// Note: Matching should be case insensitive.
MimeTypeH264 = "video/H264"
+ // MimeTypeH265 H265 MIME type
+ // Note: Matching should be case insensitive.
+ MimeTypeH265 = "video/H265"
// MimeTypeOpus Opus MIME type
// Note: Matching should be case insensitive.
MimeTypeOpus = "audio/opus"
@@ -26,6 +32,9 @@ const (
// MimeTypeVP9 VP9 MIME type
// Note: Matching should be case insensitive.
MimeTypeVP9 = "video/VP9"
+ // MimeTypeAV1 AV1 MIME type
+ // Note: Matching should be case insensitive.
+ MimeTypeAV1 = "video/AV1"
// MimeTypeG722 G722 MIME type
// Note: Matching should be case insensitive.
MimeTypeG722 = "audio/G722"
@@ -57,6 +66,8 @@ type MediaEngine struct {
headerExtensions []mediaEngineHeaderExtension
negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension
+
+ mu sync.RWMutex
}
// RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
@@ -196,6 +207,9 @@ func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParame
// 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 {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
switch typ {
case RTPCodecTypeAudio:
@@ -211,6 +225,9 @@ func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType)
// 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 {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
if m.negotiatedHeaderExtensions == nil {
m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
}
@@ -251,6 +268,9 @@ func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapabi
// RegisterFeedback adds feedback mechanism to already registered codecs.
func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
switch typ {
case RTPCodecTypeVideo:
for i, v := range m.videoCodecs {
@@ -268,6 +288,9 @@ func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType)
// 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) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
if m.negotiatedHeaderExtensions == nil {
return 0, false, false
}
@@ -284,6 +307,8 @@ func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapabilit
// copy copies any user modifiable state of the MediaEngine
// all internal state is reset
func (m *MediaEngine) copy() *MediaEngine {
+ m.mu.Lock()
+ defer m.mu.Unlock()
cloned := &MediaEngine{
videoCodecs: append([]RTPCodecParameters{}, m.videoCodecs...),
audioCodecs: append([]RTPCodecParameters{}, m.audioCodecs...),
@@ -295,15 +320,39 @@ func (m *MediaEngine) copy() *MediaEngine {
return cloned
}
-func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) {
- for _, codec := range m.negotiatedVideoCodecs {
+func findCodecByPayload(codecs []RTPCodecParameters, payloadType PayloadType) *RTPCodecParameters {
+ for _, codec := range codecs {
if codec.PayloadType == payloadType {
- return codec, RTPCodecTypeVideo, nil
+ return &codec
}
}
- for _, codec := range m.negotiatedAudioCodecs {
- if codec.PayloadType == payloadType {
- return codec, RTPCodecTypeAudio, nil
+ return nil
+}
+
+func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
+ // if we've negotiated audio or video, check the negotiated types before our
+ // built-in payload types, to ensure we pick the codec the other side wants.
+ if m.negotiatedVideo {
+ if codec := findCodecByPayload(m.negotiatedVideoCodecs, payloadType); codec != nil {
+ return *codec, RTPCodecTypeVideo, nil
+ }
+ }
+ if m.negotiatedAudio {
+ if codec := findCodecByPayload(m.negotiatedAudioCodecs, payloadType); codec != nil {
+ return *codec, RTPCodecTypeAudio, nil
+ }
+ }
+ if !m.negotiatedVideo {
+ if codec := findCodecByPayload(m.videoCodecs, payloadType); codec != nil {
+ return *codec, RTPCodecTypeVideo, nil
+ }
+ }
+ if !m.negotiatedAudio {
+ if codec := findCodecByPayload(m.audioCodecs, payloadType); codec != nil {
+ return *codec, RTPCodecTypeAudio, nil
}
}
@@ -311,6 +360,9 @@ func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParame
}
func (m *MediaEngine) collectStats(collector *statsReportCollector) {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
statsLoop := func(codecs []RTPCodecParameters) {
for _, codec := range codecs {
collector.Collecting()
@@ -340,8 +392,8 @@ func (m *MediaEngine) matchRemoteCodec(remoteCodec RTPCodecParameters, typ RTPCo
codecs = m.audioCodecs
}
- remoteFmtp := parseFmtp(remoteCodec.RTPCodecCapability.SDPFmtpLine)
- if apt, hasApt := remoteFmtp["apt"]; hasApt {
+ remoteFmtp := fmtp.Parse(remoteCodec.RTPCodecCapability.MimeType, remoteCodec.RTPCodecCapability.SDPFmtpLine)
+ if apt, hasApt := remoteFmtp.Parameter("apt"); hasApt {
payloadType, err := strconv.Atoi(apt)
if err != nil {
return codecMatchNone, err
@@ -418,6 +470,9 @@ func (m *MediaEngine) pushCodecs(codecs []RTPCodecParameters, typ RTPCodecType)
// Update the MediaEngine from a remote description
func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
for _, media := range desc.MediaDescriptions {
var typ RTPCodecType
switch {
@@ -478,6 +533,9 @@ func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) e
}
func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
+ m.mu.RLock()
+ defer m.mu.RUnlock()
+
if typ == RTPCodecTypeVideo {
if m.negotiatedVideo {
return m.negotiatedVideoCodecs
@@ -498,6 +556,11 @@ func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters {
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
+ // perform before locking to prevent recursive RLocks
+ foundCodecs := m.getCodecsByKind(typ)
+
+ m.mu.RLock()
+ defer m.mu.RUnlock()
if m.negotiatedVideo && typ == RTPCodecTypeVideo ||
m.negotiatedAudio && typ == RTPCodecTypeAudio {
for id, e := range m.negotiatedHeaderExtensions {
@@ -515,7 +578,7 @@ func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPT
return RTPParameters{
HeaderExtensions: headerExtensions,
- Codecs: m.getCodecsByKind(typ),
+ Codecs: foundCodecs,
}
}
@@ -525,6 +588,8 @@ func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RT
return RTPParameters{}, err
}
+ m.mu.RLock()
+ defer m.mu.RUnlock()
headerExtensions := make([]RTPHeaderExtensionParameter, 0)
for id, e := range m.negotiatedHeaderExtensions {
if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
@@ -545,7 +610,9 @@ func payloaderForCodec(codec RTPCodecCapability) (rtp.Payloader, error) {
case strings.ToLower(MimeTypeOpus):
return &codecs.OpusPayloader{}, nil
case strings.ToLower(MimeTypeVP8):
- return &codecs.VP8Payloader{}, nil
+ return &codecs.VP8Payloader{
+ EnablePictureID: true,
+ }, nil
case strings.ToLower(MimeTypeVP9):
return &codecs.VP9Payloader{}, nil
case strings.ToLower(MimeTypeG722):
diff --git a/mediaengine_test.go b/mediaengine_test.go
index 61c87d916af..1acf0247abc 100644
--- a/mediaengine_test.go
+++ b/mediaengine_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -111,6 +112,28 @@ a=fmtp:112 minptime=10; useinbandfec=1
assert.Equal(t, opusCodec.MimeType, MimeTypeOpus)
})
+ t.Run("Ambiguous Payload Type", func(t *testing.T) {
+ const opusSamePayload = `v=0
+o=- 4596489990601351948 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=audio 9 UDP/TLS/RTP/SAVPF 96
+a=rtpmap:96 opus/48000/2
+a=fmtp:96 minptime=10; useinbandfec=1
+`
+
+ m := MediaEngine{}
+ assert.NoError(t, m.RegisterDefaultCodecs())
+ assert.NoError(t, m.updateFromRemoteDescription(mustParse(opusSamePayload)))
+
+ assert.False(t, m.negotiatedVideo)
+ assert.True(t, m.negotiatedAudio)
+
+ opusCodec, _, err := m.getCodecByPayload(96)
+ assert.NoError(t, err)
+ assert.Equal(t, opusCodec.MimeType, MimeTypeOpus)
+ })
+
t.Run("Case Insensitive", func(t *testing.T) {
const opusUpcase = `v=0
o=- 4596489990601351948 2 IN IP4 127.0.0.1
@@ -167,13 +190,7 @@ a=rtpmap:111 opus/48000/2
m := MediaEngine{}
assert.NoError(t, m.RegisterDefaultCodecs())
- for _, extension := range []string{
- "urn:ietf:params:rtp-hdrext:sdes:mid",
- "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
- } {
- assert.NoError(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: extension}, RTPCodecTypeAudio))
- }
-
+ registerSimulcastHeaderExtensions(&m, RTPCodecTypeAudio)
assert.NoError(t, m.updateFromRemoteDescription(mustParse(headerExtensions)))
assert.False(t, m.negotiatedVideo)
@@ -394,9 +411,9 @@ func TestMediaEngineHeaderExtensionDirection(t *testing.T) {
m := &MediaEngine{}
registerCodec(m)
- assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv), ErrRegisterHeaderExtensionInvalidDirection)
- assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionInactive), ErrRegisterHeaderExtensionInvalidDirection)
- assert.Error(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirection(0)), ErrRegisterHeaderExtensionInvalidDirection)
+ assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionSendrecv), ErrRegisterHeaderExtensionInvalidDirection)
+ assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirectionInactive), ErrRegisterHeaderExtensionInvalidDirection)
+ assert.ErrorIs(t, m.RegisterHeaderExtension(RTPHeaderExtensionCapability{"pion-header-test"}, RTPCodecTypeAudio, RTPTransceiverDirection(0)), ErrRegisterHeaderExtensionInvalidDirection)
})
}
diff --git a/operations.go b/operations.go
index 82bc832244a..1eb85345c35 100644
--- a/operations.go
+++ b/operations.go
@@ -1,6 +1,7 @@
package webrtc
import (
+ "container/list"
"sync"
)
@@ -11,11 +12,13 @@ type operation func()
type operations struct {
mu sync.Mutex
busy bool
- ops []operation
+ ops *list.List
}
func newOperations() *operations {
- return &operations{}
+ return &operations{
+ ops: list.New(),
+ }
}
// Enqueue adds a new action to be executed. If there are no actions scheduled,
@@ -27,7 +30,7 @@ func (o *operations) Enqueue(op operation) {
o.mu.Lock()
running := o.busy
- o.ops = append(o.ops, op)
+ o.ops.PushBack(op)
o.busy = true
o.mu.Unlock()
@@ -40,7 +43,7 @@ func (o *operations) Enqueue(op operation) {
func (o *operations) IsEmpty() bool {
o.mu.Lock()
defer o.mu.Unlock()
- return len(o.ops) == 0
+ return o.ops.Len() == 0
}
// Done blocks until all currently enqueued operations are finished executing.
@@ -57,20 +60,20 @@ func (o *operations) Done() {
func (o *operations) pop() func() {
o.mu.Lock()
defer o.mu.Unlock()
- if len(o.ops) == 0 {
+ if o.ops.Len() == 0 {
return nil
}
- fn := o.ops[0]
- o.ops = o.ops[1:]
- return fn
+ e := o.ops.Front()
+ o.ops.Remove(e)
+ return e.Value.(operation)
}
func (o *operations) start() {
defer func() {
o.mu.Lock()
defer o.mu.Unlock()
- if len(o.ops) == 0 {
+ if o.ops.Len() == 0 {
o.busy = false
return
}
diff --git a/ortc_datachannel_test.go b/ortc_datachannel_test.go
new file mode 100644
index 00000000000..cc64d2cb448
--- /dev/null
+++ b/ortc_datachannel_test.go
@@ -0,0 +1,65 @@
+//go:build !js
+// +build !js
+
+package webrtc
+
+import (
+ "io"
+ "testing"
+ "time"
+
+ "github.com/pion/transport/test"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDataChannel_ORTCE2E(t *testing.T) {
+ lim := test.TimeOut(time.Second * 20)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ stackA, stackB, err := newORTCPair()
+ assert.NoError(t, err)
+
+ awaitSetup := make(chan struct{})
+ awaitString := make(chan struct{})
+ awaitBinary := make(chan struct{})
+ stackB.sctp.OnDataChannel(func(d *DataChannel) {
+ close(awaitSetup)
+
+ d.OnMessage(func(msg DataChannelMessage) {
+ if msg.IsString {
+ close(awaitString)
+ } else {
+ close(awaitBinary)
+ }
+ })
+ })
+
+ assert.NoError(t, signalORTCPair(stackA, stackB))
+
+ var id uint16 = 1
+ dcParams := &DataChannelParameters{
+ Label: "Foo",
+ ID: &id,
+ }
+ channelA, err := stackA.api.NewDataChannel(stackA.sctp, dcParams)
+ assert.NoError(t, err)
+
+ <-awaitSetup
+
+ assert.NoError(t, channelA.SendText("ABC"))
+ assert.NoError(t, channelA.Send([]byte("ABC")))
+
+ <-awaitString
+ <-awaitBinary
+
+ assert.NoError(t, stackA.close())
+ assert.NoError(t, stackB.close())
+
+ // attempt to send when channel is closed
+ assert.ErrorIs(t, channelA.Send([]byte("ABC")), io.ErrClosedPipe)
+ assert.ErrorIs(t, channelA.SendText("test"), io.ErrClosedPipe)
+ assert.ErrorIs(t, channelA.ensureOpen(), io.ErrClosedPipe)
+}
diff --git a/ortc_media_test.go b/ortc_media_test.go
new file mode 100644
index 00000000000..5d247337136
--- /dev/null
+++ b/ortc_media_test.go
@@ -0,0 +1,69 @@
+//go:build !js
+// +build !js
+
+package webrtc
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/pion/transport/test"
+ "github.com/pion/webrtc/v3/pkg/media"
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_ORTC_Media(t *testing.T) {
+ lim := test.TimeOut(time.Second * 20)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ stackA, stackB, err := newORTCPair()
+ assert.NoError(t, err)
+
+ assert.NoError(t, stackA.api.mediaEngine.RegisterDefaultCodecs())
+ assert.NoError(t, stackB.api.mediaEngine.RegisterDefaultCodecs())
+
+ assert.NoError(t, signalORTCPair(stackA, stackB))
+
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ rtpSender, err := stackA.api.NewRTPSender(track, stackA.dtls)
+ assert.NoError(t, err)
+ assert.NoError(t, rtpSender.Send(rtpSender.GetParameters()))
+
+ rtpReceiver, err := stackB.api.NewRTPReceiver(RTPCodecTypeVideo, stackB.dtls)
+ assert.NoError(t, err)
+ assert.NoError(t, rtpReceiver.Receive(RTPReceiveParameters{Encodings: []RTPDecodingParameters{
+ {RTPCodingParameters: rtpSender.GetParameters().Encodings[0].RTPCodingParameters},
+ }}))
+
+ seenPacket, seenPacketCancel := context.WithCancel(context.Background())
+ go func() {
+ track := rtpReceiver.Track()
+ _, _, err := track.ReadRTP()
+ assert.NoError(t, err)
+
+ seenPacketCancel()
+ }()
+
+ func() {
+ for range time.Tick(time.Millisecond * 20) {
+ select {
+ case <-seenPacket.Done():
+ return
+ default:
+ assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
+ }
+ }
+ }()
+
+ assert.NoError(t, rtpSender.Stop())
+ assert.NoError(t, rtpReceiver.Stop())
+
+ assert.NoError(t, stackA.close())
+ assert.NoError(t, stackB.close())
+}
diff --git a/datachannel_ortc_test.go b/ortc_test.go
similarity index 63%
rename from datachannel_ortc_test.go
rename to ortc_test.go
index 1f769c09384..0ebfec7ab06 100644
--- a/datachannel_ortc_test.go
+++ b/ortc_test.go
@@ -1,97 +1,12 @@
+//go:build !js
// +build !js
package webrtc
import (
- "io"
- "testing"
- "time"
-
- "github.com/pion/transport/test"
"github.com/pion/webrtc/v3/internal/util"
- "github.com/stretchr/testify/assert"
)
-func TestDataChannel_ORTCE2E(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- stackA, stackB, err := newORTCPair()
- if err != nil {
- t.Fatal(err)
- }
-
- awaitSetup := make(chan struct{})
- awaitString := make(chan struct{})
- awaitBinary := make(chan struct{})
- stackB.sctp.OnDataChannel(func(d *DataChannel) {
- close(awaitSetup)
-
- d.OnMessage(func(msg DataChannelMessage) {
- if msg.IsString {
- close(awaitString)
- } else {
- close(awaitBinary)
- }
- })
- })
-
- err = signalORTCPair(stackA, stackB)
- if err != nil {
- t.Fatal(err)
- }
-
- var id uint16 = 1
- dcParams := &DataChannelParameters{
- Label: "Foo",
- ID: &id,
- }
- channelA, err := stackA.api.NewDataChannel(stackA.sctp, dcParams)
- if err != nil {
- t.Fatal(err)
- }
-
- <-awaitSetup
-
- err = channelA.SendText("ABC")
- if err != nil {
- t.Fatal(err)
- }
- err = channelA.Send([]byte("ABC"))
- if err != nil {
- t.Fatal(err)
- }
- <-awaitString
- <-awaitBinary
-
- err = stackA.close()
- if err != nil {
- t.Fatal(err)
- }
-
- err = stackB.close()
- if err != nil {
- t.Fatal(err)
- }
-
- // attempt to send when channel is closed
- err = channelA.Send([]byte("ABC"))
- assert.Error(t, err)
- assert.Equal(t, io.ErrClosedPipe, err)
-
- err = channelA.SendText("test")
- assert.Error(t, err)
- assert.Equal(t, io.ErrClosedPipe, err)
-
- err = channelA.ensureOpen()
- assert.Error(t, err)
- assert.Equal(t, io.ErrClosedPipe, err)
-}
-
type testORTCStack struct {
api *API
gatherer *ICEGatherer
@@ -185,10 +100,10 @@ func (s *testORTCStack) close() error {
}
type testORTCSignal struct {
- ICECandidates []ICECandidate `json:"iceCandidates"`
- ICEParameters ICEParameters `json:"iceParameters"`
- DTLSParameters DTLSParameters `json:"dtlsParameters"`
- SCTPCapabilities SCTPCapabilities `json:"sctpCapabilities"`
+ ICECandidates []ICECandidate
+ ICEParameters ICEParameters
+ DTLSParameters DTLSParameters
+ SCTPCapabilities SCTPCapabilities
}
func newORTCPair() (stackA *testORTCStack, stackB *testORTCStack, err error) {
diff --git a/peerconnection.go b/peerconnection.go
index b3e5c6642ba..6b64c9eea79 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -135,15 +136,22 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection,
pc.iceConnectionState.Store(ICEConnectionStateNew)
pc.connectionState.Store(PeerConnectionStateNew)
- if !api.settingEngine.disableMediaEngineCopy {
- pc.api = &API{
- settingEngine: api.settingEngine,
- mediaEngine: api.mediaEngine.copy(),
- interceptor: api.interceptor,
- }
+ i, err := api.interceptorRegistry.Build("")
+ if err != nil {
+ return nil, err
+ }
+
+ pc.api = &API{
+ settingEngine: api.settingEngine,
+ interceptor: i,
+ }
+
+ if api.settingEngine.disableMediaEngineCopy {
+ pc.api.mediaEngine = api.mediaEngine
+ } else {
+ pc.api.mediaEngine = api.mediaEngine.copy()
}
- var err error
if err = pc.initConfiguration(configuration); err != nil {
return nil, err
}
@@ -177,7 +185,7 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection,
}
})
- pc.interceptorRTCPWriter = api.interceptor.BindRTCPWriter(interceptor.RTCPWriterFunc(pc.writeRTCP))
+ pc.interceptorRTCPWriter = pc.api.interceptor.BindRTCPWriter(interceptor.RTCPWriterFunc(pc.writeRTCP))
return pc, nil
}
@@ -296,7 +304,7 @@ func (pc *PeerConnection) onNegotiationNeeded() {
func (pc *PeerConnection) negotiationNeededOp() {
// Don't run NegotiatedNeeded checks if OnNegotiationNeeded is not set
- if handler := pc.onNegotiationNeededHandler.Load(); handler == nil {
+ if handler, ok := pc.onNegotiationNeededHandler.Load().(func()); !ok || handler == nil {
return
}
@@ -464,8 +472,8 @@ func (pc *PeerConnection) OnICEConnectionStateChange(f func(ICEConnectionState))
func (pc *PeerConnection) onICEConnectionStateChange(cs ICEConnectionState) {
pc.iceConnectionState.Store(cs)
pc.log.Infof("ICE connection state changed: %s", cs)
- if handler := pc.onICEConnectionStateChangeHandler.Load(); handler != nil {
- handler.(func(ICEConnectionState))(cs)
+ if handler, ok := pc.onICEConnectionStateChangeHandler.Load().(func(ICEConnectionState)); ok && handler != nil {
+ handler(cs)
}
}
@@ -475,6 +483,14 @@ func (pc *PeerConnection) OnConnectionStateChange(f func(PeerConnectionState)) {
pc.onConnectionStateChangeHandler.Store(f)
}
+func (pc *PeerConnection) onConnectionStateChange(cs PeerConnectionState) {
+ pc.connectionState.Store(cs)
+ pc.log.Infof("peer connection state changed: %s", cs)
+ if handler, ok := pc.onConnectionStateChangeHandler.Load().(func(PeerConnectionState)); ok && handler != nil {
+ go handler(cs)
+ }
+}
+
// SetConfiguration updates the configuration of this PeerConnection object.
func (pc *PeerConnection) SetConfiguration(configuration Configuration) error { //nolint:gocognit
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-setconfiguration (step #2)
@@ -578,8 +594,6 @@ func (pc *PeerConnection) hasLocalDescriptionChanged(desc *SessionDescription) b
return false
}
-var errExcessiveRetries = errors.New("excessive retries in CreateOffer")
-
// CreateOffer starts the PeerConnection and generates the localDescription
// https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-createoffer
func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription, error) { //nolint:gocognit
@@ -647,7 +661,7 @@ func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription
continue
}
pc.greaterMid++
- err = t.setMid(strconv.Itoa(pc.greaterMid))
+ err = t.SetMid(strconv.Itoa(pc.greaterMid))
if err != nil {
return SessionDescription{}, err
}
@@ -736,11 +750,7 @@ func (pc *PeerConnection) updateConnectionState(iceConnectionState ICEConnection
return
}
- pc.log.Infof("peer connection state changed: %s", connectionState)
- pc.connectionState.Store(connectionState)
- if handler := pc.onConnectionStateChangeHandler.Load(); handler != nil {
- go handler.(func(PeerConnectionState))(connectionState)
- }
+ pc.onConnectionStateChange(connectionState)
}
func (pc *PeerConnection) createICETransport() *ICETransport {
@@ -776,8 +786,9 @@ func (pc *PeerConnection) createICETransport() *ICETransport {
// CreateAnswer starts the PeerConnection and generates the localDescription
func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescription, error) {
useIdentity := pc.idpLoginURL != nil
+ remoteDesc := pc.RemoteDescription()
switch {
- case pc.RemoteDescription() == nil:
+ case remoteDesc == nil:
return SessionDescription{}, &rtcerr.InvalidStateError{Err: ErrNoRemoteDescription}
case useIdentity:
return SessionDescription{}, errIdentityProviderNotImplemented
@@ -790,6 +801,13 @@ func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescripti
connectionRole := connectionRoleFromDtlsRole(pc.api.settingEngine.answeringDTLSRole)
if connectionRole == sdp.ConnectionRole(0) {
connectionRole = connectionRoleFromDtlsRole(defaultDtlsRoleAnswer)
+
+ // If one of the agents is lite and the other one is not, the lite agent must be the controlling agent.
+ // If both or neither agents are lite the offering agent is controlling.
+ // RFC 8445 S6.1.1
+ if isIceLiteSet(remoteDesc.parsed) && !pc.api.settingEngine.candidates.ICELite {
+ connectionRole = connectionRoleFromDtlsRole(DTLSRoleServer)
+ }
}
pc.mu.Lock()
defer pc.mu.Unlock()
@@ -967,6 +985,7 @@ func (pc *PeerConnection) SetLocalDescription(desc SessionDescription) error {
if err := pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
+ pc.configureRTPReceivers(haveLocalDescription, remoteDesc, currentTransceivers)
pc.ops.Enqueue(func() {
pc.startRTP(haveLocalDescription, remoteDesc, currentTransceivers)
})
@@ -1050,12 +1069,28 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
localDirection := RTPTransceiverDirectionRecvonly
if direction == RTPTransceiverDirectionRecvonly {
localDirection = RTPTransceiverDirectionSendonly
+ } else if direction == RTPTransceiverDirectionInactive {
+ localDirection = RTPTransceiverDirectionInactive
}
- t = newRTPTransceiver(receiver, nil, localDirection, kind)
+ t = newRTPTransceiver(receiver, nil, localDirection, kind, pc.api)
pc.mu.Lock()
pc.addRTPTransceiver(t)
pc.mu.Unlock()
+
+ // if transceiver is create by remote sdp, set prefer codec same as remote peer
+ if codecs, err := codecsFromMediaDescription(media); err == nil {
+ filteredCodecs := []RTPCodecParameters{}
+ for _, codec := range codecs {
+ if c, matchType := codecParametersFuzzySearch(codec, pc.api.mediaEngine.getCodecsByKind(kind)); matchType == codecMatchExact {
+ // if codec match exact, use payloadtype register to mediaengine
+ codec.PayloadType = c.PayloadType
+ filteredCodecs = append(filteredCodecs, codec)
+ }
+ }
+ _ = t.SetCodecPreferences(filteredCodecs)
+ }
+
case direction == RTPTransceiverDirectionRecvonly:
if t.Direction() == RTPTransceiverDirectionSendrecv {
t.setDirection(RTPTransceiverDirectionSendonly)
@@ -1067,14 +1102,14 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
}
if t.Mid() == "" {
- if err := t.setMid(midValue); err != nil {
+ if err := t.SetMid(midValue); err != nil {
return err
}
}
}
}
- remoteUfrag, remotePwd, candidates, err := extractICEDetails(desc.parsed)
+ remoteUfrag, remotePwd, candidates, err := extractICEDetails(desc.parsed, pc.log)
if err != nil {
return err
}
@@ -1105,6 +1140,7 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
if err = pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
+ pc.configureRTPReceivers(true, &desc, currentTransceivers)
pc.ops.Enqueue(func() {
pc.startRTP(true, &desc, currentTransceivers)
})
@@ -1164,12 +1200,7 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
return nil
}
- remoteIsLite := false
- for _, a := range desc.parsed.Attributes {
- if strings.TrimSpace(a.Key) == sdp.AttrKeyICELite {
- remoteIsLite = true
- }
- }
+ remoteIsLite := isIceLiteSet(desc.parsed)
fingerprint, fingerprintHash, err := extractFingerprint(desc.parsed)
if err != nil {
@@ -1190,6 +1221,8 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
if err := pc.startRTPSenders(currentTransceivers); err != nil {
return err
}
+
+ pc.configureRTPReceivers(false, &desc, currentTransceivers)
}
pc.ops.Enqueue(func() {
@@ -1201,19 +1234,8 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
return nil
}
-func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPReceiver) {
- encodings := []RTPDecodingParameters{}
- if incoming.ssrc != 0 {
- encodings = append(encodings, RTPDecodingParameters{RTPCodingParameters{SSRC: incoming.ssrc}})
- }
- for _, rid := range incoming.rids {
- encodings = append(encodings, RTPDecodingParameters{RTPCodingParameters{RID: rid}})
- }
-
- if err := receiver.Receive(RTPReceiveParameters{Encodings: encodings}); err != nil {
- pc.log.Warnf("RTPReceiver Receive failed %s", err)
- return
- }
+func (pc *PeerConnection) configureReceiver(incoming trackDetails, receiver *RTPReceiver) {
+ receiver.configureReceive(trackDetailsToRTPReceiveParameters(&incoming))
// set track id and label early so they can be set as new track information
// is received from the SDP.
@@ -1223,102 +1245,182 @@ func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPRece
receiver.tracks[i].track.streamID = incoming.streamID
receiver.tracks[i].track.mu.Unlock()
}
+}
- // We can't block and wait for a single SSRC
- if incoming.ssrc == 0 {
+func (pc *PeerConnection) startReceiver(incoming trackDetails, receiver *RTPReceiver) {
+ if err := receiver.startReceive(trackDetailsToRTPReceiveParameters(&incoming)); err != nil {
+ pc.log.Warnf("RTPReceiver Receive failed %s", err)
return
}
- go func() {
- if err := receiver.Track().determinePayloadType(); err != nil {
- pc.log.Warnf("Could not determine PayloadType for SSRC %d", receiver.Track().SSRC())
+ for _, t := range receiver.Tracks() {
+ if t.SSRC() == 0 || t.RID() != "" {
return
}
- params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(receiver.Track().PayloadType())
- if err != nil {
- pc.log.Warnf("no codec could be found for payloadType %d", receiver.Track().PayloadType())
- return
- }
+ go func(track *TrackRemote) {
+ b := make([]byte, pc.api.settingEngine.getReceiveMTU())
+ n, _, err := track.peek(b)
+ if err != nil {
+ pc.log.Warnf("Could not determine PayloadType for SSRC %d (%s)", track.SSRC(), err)
+ return
+ }
- receiver.Track().mu.Lock()
- receiver.Track().kind = receiver.kind
- receiver.Track().codec = params.Codecs[0]
- receiver.Track().params = params
- receiver.Track().mu.Unlock()
+ if err = track.checkAndUpdateTrack(b[:n]); err != nil {
+ pc.log.Warnf("Failed to set codec settings for track SSRC %d (%s)", track.SSRC(), err)
+ return
+ }
- pc.onTrack(receiver.Track(), receiver)
- }()
+ pc.onTrack(track, receiver)
+ }(t)
+ }
}
-// startRTPReceivers opens knows inbound SRTP streams from the RemoteDescription
-func (pc *PeerConnection) startRTPReceivers(incomingTracks []trackDetails, currentTransceivers []*RTPTransceiver) { //nolint:gocognit
- localTransceivers := append([]*RTPTransceiver{}, currentTransceivers...)
+func runIfNewReceiver(
+ incomingTrack trackDetails,
+ transceivers []*RTPTransceiver,
+ f func(incomingTrack trackDetails, receiver *RTPReceiver),
+) bool {
+ for _, t := range transceivers {
+ if t.Mid() != incomingTrack.mid {
+ continue
+ }
- remoteIsPlanB := false
- switch pc.configuration.SDPSemantics {
- case SDPSemanticsPlanB:
- remoteIsPlanB = true
- case SDPSemanticsUnifiedPlanWithFallback:
- remoteIsPlanB = descriptionIsPlanB(pc.RemoteDescription())
- default:
- // none
+ receiver := t.Receiver()
+ if (incomingTrack.kind != t.Kind()) ||
+ (t.Direction() != RTPTransceiverDirectionRecvonly && t.Direction() != RTPTransceiverDirectionSendrecv) ||
+ receiver == nil ||
+ (receiver.haveReceived()) {
+ continue
+ }
+
+ f(incomingTrack, receiver)
+ return true
}
- // Ensure we haven't already started a transceiver for this ssrc
- for i := range incomingTracks {
- if len(incomingTracks) <= i {
- break
- }
- incomingTrack := incomingTracks[i]
+ return false
+}
- for _, t := range localTransceivers {
- if (t.Receiver()) == nil || t.Receiver().Track() == nil || t.Receiver().Track().ssrc != incomingTrack.ssrc {
+// configurepRTPReceivers opens knows inbound SRTP streams from the RemoteDescription
+func (pc *PeerConnection) configureRTPReceivers(isRenegotiation bool, remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) { //nolint:gocognit
+ incomingTracks := trackDetailsFromSDP(pc.log, remoteDesc.parsed)
+
+ if isRenegotiation {
+ for _, t := range currentTransceivers {
+ receiver := t.Receiver()
+ if receiver == nil {
continue
}
- incomingTracks = filterTrackWithSSRC(incomingTracks, incomingTrack.ssrc)
- }
- }
+ tracks := t.Receiver().Tracks()
+ if len(tracks) == 0 {
+ continue
+ }
- unhandledTracks := incomingTracks[:0]
- for i := range incomingTracks {
- trackHandled := false
- for j := range localTransceivers {
- t := localTransceivers[j]
- incomingTrack := incomingTracks[i]
+ receiverNeedsStopped := false
+ func() {
+ for _, t := range tracks {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if t.rid != "" {
+ if details := trackDetailsForRID(incomingTracks, t.rid); details != nil {
+ t.id = details.id
+ t.streamID = details.streamID
+ continue
+ }
+ } else if t.ssrc != 0 {
+ if details := trackDetailsForSSRC(incomingTracks, t.ssrc); details != nil {
+ t.id = details.id
+ t.streamID = details.streamID
+ continue
+ }
+ }
+
+ receiverNeedsStopped = true
+ }
+ }()
- if t.Mid() != incomingTrack.mid {
+ if !receiverNeedsStopped {
continue
}
- if (incomingTrack.kind != t.kind) ||
- (t.Direction() != RTPTransceiverDirectionRecvonly && t.Direction() != RTPTransceiverDirectionSendrecv) ||
- (t.Receiver()) == nil ||
- (t.Receiver().haveReceived()) {
+ if err := receiver.Stop(); err != nil {
+ pc.log.Warnf("Failed to stop RtpReceiver: %s", err)
continue
}
- pc.startReceiver(incomingTrack, t.Receiver())
- trackHandled = true
- break
+ receiver, err := pc.api.NewRTPReceiver(receiver.kind, pc.dtlsTransport)
+ if err != nil {
+ pc.log.Warnf("Failed to create new RtpReceiver: %s", err)
+ continue
+ }
+ t.setReceiver(receiver)
}
+ }
+
+ localTransceivers := append([]*RTPTransceiver{}, currentTransceivers...)
+ // Ensure we haven't already started a transceiver for this ssrc
+ filteredTracks := append([]trackDetails{}, incomingTracks...)
+ for _, incomingTrack := range incomingTracks {
+ // If we already have a TrackRemote for a given SSRC don't handle it again
+ for _, t := range localTransceivers {
+ if receiver := t.Receiver(); receiver != nil {
+ for _, track := range receiver.Tracks() {
+ for _, ssrc := range incomingTrack.ssrcs {
+ if ssrc == track.SSRC() {
+ filteredTracks = filterTrackWithSSRC(filteredTracks, track.SSRC())
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for _, incomingTrack := range filteredTracks {
+ _ = runIfNewReceiver(incomingTrack, localTransceivers, pc.configureReceiver)
+ }
+}
+
+// startRTPReceivers opens knows inbound SRTP streams from the RemoteDescription
+func (pc *PeerConnection) startRTPReceivers(remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) {
+ incomingTracks := trackDetailsFromSDP(pc.log, remoteDesc.parsed)
+ if len(incomingTracks) == 0 {
+ return
+ }
+
+ localTransceivers := append([]*RTPTransceiver{}, currentTransceivers...)
+
+ unhandledTracks := incomingTracks[:0]
+ for _, incomingTrack := range incomingTracks {
+ trackHandled := runIfNewReceiver(incomingTrack, localTransceivers, pc.startReceiver)
if !trackHandled {
- unhandledTracks = append(unhandledTracks, incomingTracks[i])
+ unhandledTracks = append(unhandledTracks, incomingTrack)
}
}
+ remoteIsPlanB := false
+ switch pc.configuration.SDPSemantics {
+ case SDPSemanticsPlanB:
+ remoteIsPlanB = true
+ case SDPSemanticsUnifiedPlanWithFallback:
+ remoteIsPlanB = descriptionIsPlanB(pc.RemoteDescription())
+ default:
+ // none
+ }
+
if remoteIsPlanB {
- for _, incoming := range unhandledTracks {
- t, err := pc.AddTransceiverFromKind(incoming.kind, RTPTransceiverInit{
+ for _, incomingTrack := range unhandledTracks {
+ t, err := pc.AddTransceiverFromKind(incomingTrack.kind, RTPTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
})
if err != nil {
- pc.log.Warnf("Could not add transceiver for remote SSRC %d: %s", incoming.ssrc, err)
+ pc.log.Warnf("Could not add transceiver for remote SSRC %d: %s", incomingTrack.ssrcs[0], err)
continue
}
- pc.startReceiver(incoming, t.Receiver())
+ pc.configureReceiver(incomingTrack, t.Receiver())
+ pc.startReceiver(incomingTrack, t.Receiver())
}
}
}
@@ -1326,17 +1428,8 @@ func (pc *PeerConnection) startRTPReceivers(incomingTracks []trackDetails, curre
// startRTPSenders starts all outbound RTP streams
func (pc *PeerConnection) startRTPSenders(currentTransceivers []*RTPTransceiver) error {
for _, transceiver := range currentTransceivers {
- if transceiver.Sender() != nil && transceiver.Sender().isNegotiated() && !transceiver.Sender().hasSent() {
- err := transceiver.Sender().Send(RTPSendParameters{
- Encodings: []RTPEncodingParameters{
- {
- RTPCodingParameters{
- SSRC: transceiver.Sender().ssrc,
- PayloadType: transceiver.Sender().payloadType,
- },
- },
- },
- })
+ if sender := transceiver.Sender(); sender != nil && sender.isNegotiated() && !sender.hasSent() {
+ err := sender.Send(sender.GetParameters())
if err != nil {
return err
}
@@ -1359,61 +1452,74 @@ func (pc *PeerConnection) startSCTP() {
return
}
+}
- // DataChannels that need to be opened now that SCTP is available
- // make a copy we may have incoming DataChannels mutating this while we open
- pc.sctpTransport.lock.RLock()
- dataChannels := append([]*DataChannel{}, pc.sctpTransport.dataChannels...)
- pc.sctpTransport.lock.RUnlock()
+func (pc *PeerConnection) handleUndeclaredSSRC(ssrc SSRC, remoteDescription *SessionDescription) (handled bool, err error) {
+ if len(remoteDescription.parsed.MediaDescriptions) != 1 {
+ return false, nil
+ }
- var openedDCCount uint32
- for _, d := range dataChannels {
- if d.ReadyState() == DataChannelStateConnecting {
- err := d.open(pc.sctpTransport, false)
- if err != nil {
- pc.log.Warnf("failed to open data channel: %s", err)
- continue
+ onlyMediaSection := remoteDescription.parsed.MediaDescriptions[0]
+ streamID := ""
+ id := ""
+
+ for _, a := range onlyMediaSection.Attributes {
+ switch a.Key {
+ case sdp.AttrKeyMsid:
+ if split := strings.Split(a.Value, " "); len(split) == 2 {
+ streamID = split[0]
+ id = split[1]
}
- openedDCCount++
+ case sdp.AttrKeySSRC:
+ return false, errPeerConnSingleMediaSectionHasExplicitSSRC
+ case sdpAttributeRid:
+ return false, nil
}
}
- pc.sctpTransport.lock.Lock()
- pc.sctpTransport.dataChannelsOpened += openedDCCount
- pc.sctpTransport.lock.Unlock()
+ incoming := trackDetails{
+ ssrcs: []SSRC{ssrc},
+ kind: RTPCodecTypeVideo,
+ streamID: streamID,
+ id: id,
+ }
+ if onlyMediaSection.MediaName.Media == RTPCodecTypeAudio.String() {
+ incoming.kind = RTPCodecTypeAudio
+ }
+
+ t, err := pc.AddTransceiverFromKind(incoming.kind, RTPTransceiverInit{
+ Direction: RTPTransceiverDirectionSendrecv,
+ })
+ if err != nil {
+ return false, fmt.Errorf("%w: %d: %s", errPeerConnRemoteSSRCAddTransceiver, ssrc, err)
+ }
+
+ pc.configureReceiver(incoming, t.Receiver())
+ pc.startReceiver(incoming, t.Receiver())
+ return true, nil
}
-func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocognit
+func (pc *PeerConnection) handleIncomingSSRC(rtpStream io.Reader, ssrc SSRC) error { //nolint:gocognit
remoteDescription := pc.RemoteDescription()
if remoteDescription == nil {
return errPeerConnRemoteDescriptionNil
}
- // If the remote SDP was only one media section the ssrc doesn't have to be explicitly declared
- if len(remoteDescription.parsed.MediaDescriptions) == 1 {
- onlyMediaSection := remoteDescription.parsed.MediaDescriptions[0]
- for _, a := range onlyMediaSection.Attributes {
- if a.Key == ssrcStr {
- return errPeerConnSingleMediaSectionHasExplicitSSRC
- }
- }
-
- incoming := trackDetails{
- ssrc: ssrc,
- kind: RTPCodecTypeVideo,
+ // If a SSRC already exists in the RemoteDescription don't perform heuristics upon it
+ for _, track := range trackDetailsFromSDP(pc.log, remoteDescription.parsed) {
+ if track.repairSsrc != nil && ssrc == *track.repairSsrc {
+ return nil
}
- if onlyMediaSection.MediaName.Media == RTPCodecTypeAudio.String() {
- incoming.kind = RTPCodecTypeAudio
+ for _, trackSsrc := range track.ssrcs {
+ if ssrc == trackSsrc {
+ return nil
+ }
}
+ }
- t, err := pc.AddTransceiverFromKind(incoming.kind, RTPTransceiverInit{
- Direction: RTPTransceiverDirectionSendrecv,
- })
- if err != nil {
- return fmt.Errorf("%w: %d: %s", errPeerConnRemoteSSRCAddTransceiver, ssrc, err)
- }
- pc.startReceiver(incoming, t.Receiver())
- return nil
+ // If the remote SDP was only one media section the ssrc doesn't have to be explicitly declared
+ if handled, err := pc.handleUndeclaredSSRC(ssrc, remoteDescription); handled || err != nil {
+ return err
}
midExtensionID, audioSupported, videoSupported := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdp.SDESMidURI})
@@ -1426,49 +1532,68 @@ func (pc *PeerConnection) handleUndeclaredSSRC(rtpStream io.Reader, ssrc SSRC) e
return errPeerConnSimulcastStreamIDRTPExtensionRequired
}
- b := make([]byte, receiveMTU)
- var mid, rid string
- for readCount := 0; readCount <= simulcastProbeCount; readCount++ {
- i, err := rtpStream.Read(b)
- if err != nil {
- return err
- }
+ repairStreamIDExtensionID, _, _ := pc.api.mediaEngine.getHeaderExtensionID(RTPHeaderExtensionCapability{sdesRepairRTPStreamIDURI})
- maybeMid, maybeRid, payloadType, err := handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID))
- if err != nil {
- return err
- }
+ b := make([]byte, pc.api.settingEngine.getReceiveMTU())
- if maybeMid != "" {
- mid = maybeMid
- }
- if maybeRid != "" {
- rid = maybeRid
- }
+ i, err := rtpStream.Read(b)
+ if err != nil {
+ return err
+ }
- if mid == "" || rid == "" {
- continue
- }
+ var mid, rid, rsid string
+ payloadType, err := handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID), uint8(repairStreamIDExtensionID), &mid, &rid, &rsid)
+ if err != nil {
+ return err
+ }
- params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(payloadType)
- if err != nil {
- return err
+ params, err := pc.api.mediaEngine.getRTPParametersByPayloadType(payloadType)
+ if err != nil {
+ return err
+ }
+
+ streamInfo := createStreamInfo("", ssrc, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions)
+ readStream, interceptor, rtcpReadStream, rtcpInterceptor, err := pc.dtlsTransport.streamsForSSRC(ssrc, *streamInfo)
+ if err != nil {
+ return err
+ }
+
+ for readCount := 0; readCount <= simulcastProbeCount; readCount++ {
+ if mid == "" || (rid == "" && rsid == "") {
+ i, _, err := interceptor.Read(b, nil)
+ if err != nil {
+ return err
+ }
+
+ if _, err = handleUnknownRTPPacket(b[:i], uint8(midExtensionID), uint8(streamIDExtensionID), uint8(repairStreamIDExtensionID), &mid, &rid, &rsid); err != nil {
+ return err
+ }
+
+ continue
}
for _, t := range pc.GetTransceivers() {
- if t.Mid() != mid || t.Receiver() == nil {
+ receiver := t.Receiver()
+ if t.Mid() != mid || receiver == nil {
continue
}
- track, err := t.Receiver().receiveForRid(rid, params, ssrc)
+ if rsid != "" {
+ receiver.mu.Lock()
+ defer receiver.mu.Unlock()
+ return receiver.receiveForRtx(SSRC(0), rsid, streamInfo, readStream, interceptor, rtcpReadStream, rtcpInterceptor)
+ }
+
+ track, err := receiver.receiveForRid(rid, params, streamInfo, readStream, interceptor, rtcpReadStream, rtcpInterceptor)
if err != nil {
return err
}
- pc.onTrack(track, t.Receiver())
+ pc.onTrack(track, receiver)
return nil
}
}
+ pc.api.interceptor.UnbindRemoteStream(streamInfo)
return errPeerConnSimulcastIncomingSSRCFailed
}
@@ -1505,8 +1630,8 @@ func (pc *PeerConnection) undeclaredMediaProcessor() {
go func(rtpStream io.Reader, ssrc SSRC) {
pc.dtlsTransport.storeSimulcastStream(stream)
- if err := pc.handleUndeclaredSSRC(rtpStream, ssrc); err != nil {
- pc.log.Errorf("Incoming unhandled RTP ssrc(%d), OnTrack will not be fired. %v", ssrc, err)
+ if err := pc.handleIncomingSSRC(rtpStream, ssrc); err != nil {
+ pc.log.Errorf(incomingUnhandledRTPSsrc, ssrc, err)
}
atomic.AddUint64(&simulcastRoutineCount, ^uint64(0))
}(stream, SSRC(ssrc))
@@ -1558,6 +1683,10 @@ func (pc *PeerConnection) AddICECandidate(candidate ICECandidateInit) error {
if candidateValue != "" {
candidate, err := ice.UnmarshalCandidate(candidateValue)
if err != nil {
+ if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) {
+ pc.log.Warnf("Discarding remote candidate: %s", err)
+ return nil
+ }
return err
}
@@ -1583,8 +1712,8 @@ func (pc *PeerConnection) GetSenders() (result []*RTPSender) {
defer pc.mu.Unlock()
for _, transceiver := range pc.rtpTransceivers {
- if transceiver.Sender() != nil {
- result = append(result, transceiver.Sender())
+ if sender := transceiver.Sender(); sender != nil {
+ result = append(result, sender)
}
}
return result
@@ -1596,8 +1725,8 @@ func (pc *PeerConnection) GetReceivers() (receivers []*RTPReceiver) {
defer pc.mu.Unlock()
for _, transceiver := range pc.rtpTransceivers {
- if transceiver.Receiver() != nil {
- receivers = append(receivers, transceiver.Receiver())
+ if receiver := transceiver.Receiver(); receiver != nil {
+ receivers = append(receivers, receiver)
}
}
return
@@ -1691,7 +1820,7 @@ func (pc *PeerConnection) newTransceiverFromTrack(direction RTPTransceiverDirect
if err != nil {
return
}
- return newRTPTransceiver(r, s, direction, track.Kind()), nil
+ return newRTPTransceiver(r, s, direction, track.Kind(), pc.api), nil
}
// AddTransceiverFromKind Create a new RtpTransceiver and adds it to the set of transceivers.
@@ -1725,7 +1854,7 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPT
if err != nil {
return nil, err
}
- t = newRTPTransceiver(receiver, nil, RTPTransceiverDirectionRecvonly, kind)
+ t = newRTPTransceiver(receiver, nil, RTPTransceiverDirectionRecvonly, kind, pc.api)
default:
return nil, errPeerConnAddTransceiverFromKindSupport
}
@@ -1858,13 +1987,11 @@ func (pc *PeerConnection) writeRTCP(pkts []rtcp.Packet, _ interceptor.Attributes
// Close ends the PeerConnection
func (pc *PeerConnection) Close() error {
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #1)
- if pc.isClosed.get() {
+ // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #2)
+ if pc.isClosed.swap(true) {
return nil
}
- // https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #2)
- pc.isClosed.set(true)
-
// https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-close (step #3)
pc.signalingState.Set(SignalingStateClosed)
@@ -2089,48 +2216,16 @@ func (pc *PeerConnection) startTransports(iceRole ICERole, dtlsRole DTLSRole, re
}
}
+// nolint: gocognit
func (pc *PeerConnection) startRTP(isRenegotiation bool, remoteDesc *SessionDescription, currentTransceivers []*RTPTransceiver) {
- trackDetails := trackDetailsFromSDP(pc.log, remoteDesc.parsed)
- if isRenegotiation {
- for _, t := range currentTransceivers {
- if t.Receiver() == nil || t.Receiver().Track() == nil {
- continue
- }
-
- t.Receiver().Track().mu.Lock()
- ssrc := t.Receiver().Track().ssrc
-
- if details := trackDetailsForSSRC(trackDetails, ssrc); details != nil {
- t.Receiver().Track().id = details.id
- t.Receiver().Track().streamID = details.streamID
- t.Receiver().Track().mu.Unlock()
- continue
- }
-
- t.Receiver().Track().mu.Unlock()
-
- if err := t.Receiver().Stop(); err != nil {
- pc.log.Warnf("Failed to stop RtpReceiver: %s", err)
- continue
- }
-
- receiver, err := pc.api.NewRTPReceiver(t.Receiver().kind, pc.dtlsTransport)
- if err != nil {
- pc.log.Warnf("Failed to create new RtpReceiver: %s", err)
- continue
- }
- t.setReceiver(receiver)
- }
+ if !isRenegotiation {
+ pc.undeclaredMediaProcessor()
}
- pc.startRTPReceivers(trackDetails, currentTransceivers)
+ pc.startRTPReceivers(remoteDesc, currentTransceivers)
if haveApplicationMediaSection(remoteDesc.parsed) {
pc.startSCTP()
}
-
- if !isRenegotiation {
- pc.undeclaredMediaProcessor()
- }
}
// generateUnmatchedSDP generates an SDP that doesn't take remote state into account
@@ -2168,8 +2263,8 @@ func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, u
} else if t.kind == RTPCodecTypeAudio {
audio = append(audio, t)
}
- if t.Sender() != nil {
- t.Sender().setNegotiated()
+ if sender := t.Sender(); sender != nil {
+ sender.setNegotiated()
}
}
@@ -2185,8 +2280,8 @@ func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, u
}
} else {
for _, t := range transceivers {
- if t.Sender() != nil {
- t.Sender().setNegotiated()
+ if sender := t.Sender(); sender != nil {
+ sender.setNegotiated()
}
mediaSections = append(mediaSections, mediaSection{id: t.Mid(), transceivers: []*RTPTransceiver{t}})
}
@@ -2255,7 +2350,7 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
switch {
case sdpSemantics == SDPSemanticsPlanB || sdpSemantics == SDPSemanticsUnifiedPlanWithFallback && detectedPlanB:
if !detectedPlanB {
- return nil, &rtcerr.TypeError{Err: ErrIncorrectSDPSemantics}
+ return nil, &rtcerr.TypeError{Err: fmt.Errorf("%w: Expected PlanB, but RemoteDescription is UnifiedPlan", ErrIncorrectSDPSemantics)}
}
// If we're responding to a plan-b offer, then we should try to fill up this
// media entry with all matching local transceivers
@@ -2265,28 +2360,28 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
t, localTransceivers = satisfyTypeAndDirection(kind, direction, localTransceivers)
if t == nil {
if len(mediaTransceivers) == 0 {
- t = &RTPTransceiver{kind: kind}
+ t = &RTPTransceiver{kind: kind, api: pc.api, codecs: pc.api.mediaEngine.getCodecsByKind(kind)}
t.setDirection(RTPTransceiverDirectionInactive)
mediaTransceivers = append(mediaTransceivers, t)
}
break
}
- if t.Sender() != nil {
- t.Sender().setNegotiated()
+ if sender := t.Sender(); sender != nil {
+ sender.setNegotiated()
}
mediaTransceivers = append(mediaTransceivers, t)
}
mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers})
case sdpSemantics == SDPSemanticsUnifiedPlan || sdpSemantics == SDPSemanticsUnifiedPlanWithFallback:
if detectedPlanB {
- return nil, &rtcerr.TypeError{Err: ErrIncorrectSDPSemantics}
+ return nil, &rtcerr.TypeError{Err: fmt.Errorf("%w: Expected UnifiedPlan, but RemoteDescription is PlanB", ErrIncorrectSDPSemantics)}
}
t, localTransceivers = findByMid(midValue, localTransceivers)
if t == nil {
return nil, fmt.Errorf("%w: %q", errPeerConnTranscieverMidNil, midValue)
}
- if t.Sender() != nil {
- t.Sender().setNegotiated()
+ if sender := t.Sender(); sender != nil {
+ sender.setNegotiated()
}
mediaTransceivers := []*RTPTransceiver{t}
mediaSections = append(mediaSections, mediaSection{id: midValue, transceivers: mediaTransceivers, ridMap: getRids(media)})
@@ -2297,8 +2392,8 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
if includeUnmatched {
if !detectedPlanB {
for _, t := range localTransceivers {
- if t.Sender() != nil {
- t.Sender().setNegotiated()
+ if sender := t.Sender(); sender != nil {
+ sender.setNegotiated()
}
mediaSections = append(mediaSections, mediaSection{id: t.Mid(), transceivers: []*RTPTransceiver{t}})
}
diff --git a/peerconnection_close_test.go b/peerconnection_close_test.go
index c07e9abbdfd..4a8d2916d89 100644
--- a/peerconnection_close_test.go
+++ b/peerconnection_close_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -91,7 +92,7 @@ func TestPeerConnection_Close_PreICE(t *testing.T) {
if pcAnswer.iceTransport.State() == ICETransportStateChecking {
break
}
- time.Sleep(time.Second)
+ time.Sleep(time.Second / 4)
}
assert.NoError(t, pcAnswer.Close())
@@ -99,11 +100,9 @@ func TestPeerConnection_Close_PreICE(t *testing.T) {
// Assert that ICETransport is shutdown, test timeout will prevent deadlock
for {
if pcAnswer.iceTransport.State() == ICETransportStateClosed {
- time.Sleep(time.Second * 3)
return
}
-
- time.Sleep(time.Second)
+ time.Sleep(time.Second / 4)
}
}
diff --git a/peerconnection_go_test.go b/peerconnection_go_test.go
index d1cd63c0fbb..f5832a3e45a 100644
--- a/peerconnection_go_test.go
+++ b/peerconnection_go_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -442,7 +443,7 @@ func TestPeerConnection_AnswerWithClosedConnection(t *testing.T) {
assert.NoError(t, answerPeerConn.Close())
_, err = answerPeerConn.CreateAnswer(nil)
- assert.Error(t, err, &rtcerr.InvalidStateError{Err: ErrConnectionClosed})
+ assert.Equal(t, err, &rtcerr.InvalidStateError{Err: ErrConnectionClosed})
}
func TestPeerConnection_satisfyTypeAndDirection(t *testing.T) {
@@ -562,80 +563,52 @@ func TestOneAttrKeyConnectionSetupPerMediaDescriptionInSDP(t *testing.T) {
assert.NoError(t, pc.Close())
}
-func TestPeerConnection_OfferingLite(t *testing.T) {
+func TestPeerConnection_IceLite(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
- s := SettingEngine{}
- s.SetLite(true)
- offerPC, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{})
- if err != nil {
- t.Fatal(err)
- }
-
- answerPC, err := NewAPI().NewPeerConnection(Configuration{})
- if err != nil {
- t.Fatal(err)
- }
-
- if err = signalPair(offerPC, answerPC); err != nil {
- t.Fatal(err)
- }
-
- iceComplete := make(chan interface{})
- answerPC.OnICEConnectionStateChange(func(iceState ICEConnectionState) {
- if iceState == ICEConnectionStateConnected {
- select {
- case <-iceComplete:
- default:
- close(iceComplete)
- }
+ connectTwoAgents := func(offerIsLite, answerisLite bool) {
+ offerSettingEngine := SettingEngine{}
+ offerSettingEngine.SetLite(offerIsLite)
+ offerPC, err := NewAPI(WithSettingEngine(offerSettingEngine)).NewPeerConnection(Configuration{})
+ if err != nil {
+ t.Fatal(err)
}
- })
-
- <-iceComplete
- closePairNow(t, offerPC, answerPC)
-}
-func TestPeerConnection_AnsweringLite(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
+ answerSettingEngine := SettingEngine{}
+ answerSettingEngine.SetLite(answerisLite)
+ answerPC, err := NewAPI(WithSettingEngine(answerSettingEngine)).NewPeerConnection(Configuration{})
+ if err != nil {
+ t.Fatal(err)
+ }
- lim := test.TimeOut(time.Second * 10)
- defer lim.Stop()
+ if err = signalPair(offerPC, answerPC); err != nil {
+ t.Fatal(err)
+ }
- offerPC, err := NewAPI().NewPeerConnection(Configuration{})
- if err != nil {
- t.Fatal(err)
- }
+ dataChannelOpen := make(chan interface{})
+ answerPC.OnDataChannel(func(_ *DataChannel) {
+ close(dataChannelOpen)
+ })
- s := SettingEngine{}
- s.SetLite(true)
- answerPC, err := NewAPI(WithSettingEngine(s)).NewPeerConnection(Configuration{})
- if err != nil {
- t.Fatal(err)
+ <-dataChannelOpen
+ closePairNow(t, offerPC, answerPC)
}
- if err = signalPair(offerPC, answerPC); err != nil {
- t.Fatal(err)
- }
+ t.Run("Offerer", func(t *testing.T) {
+ connectTwoAgents(true, false)
+ })
- iceComplete := make(chan interface{})
- answerPC.OnICEConnectionStateChange(func(iceState ICEConnectionState) {
- if iceState == ICEConnectionStateConnected {
- select {
- case <-iceComplete:
- default:
- close(iceComplete)
- }
- }
+ t.Run("Answerer", func(t *testing.T) {
+ connectTwoAgents(false, true)
})
- <-iceComplete
- closePairNow(t, offerPC, answerPC)
+ t.Run("Both", func(t *testing.T) {
+ connectTwoAgents(true, true)
+ })
}
func TestOnICEGatheringStateChange(t *testing.T) {
@@ -1048,7 +1021,7 @@ type trackRecords struct {
func (r *trackRecords) newTrack() (*TrackLocalStaticRTP, error) {
trackID := fmt.Sprintf("pion-track-%d", len(r.trackIDs))
- track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, trackID, "pion")
+ track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, trackID, "pion")
r.trackIDs[trackID] = struct{}{}
return track, err
}
@@ -1091,7 +1064,6 @@ func TestPeerConnection_MassiveTracks(t *testing.T) {
Extension: false,
ExtensionProfile: 1,
Version: 2,
- PayloadOffset: 20,
SequenceNumber: 27023,
Timestamp: 3653407706,
CSRC: []uint32{},
@@ -1223,7 +1195,7 @@ func TestICELite(t *testing.T) {
assert.NoError(t, peerConnection.SetLocalDescription(SDPAnswer))
- assert.Equal(t, ICERoleControlling, peerConnection.iceTransport.role,
+ assert.Equal(t, ICERoleControlling, peerConnection.iceTransport.Role(),
"pion did not set state to ICE-CONTROLLED against ice-light offer")
assert.NoError(t, peerConnection.Close())
@@ -1397,3 +1369,218 @@ func TestPeerConnection_SessionID(t *testing.T) {
}
closePairNow(t, pcOffer, pcAnswer)
}
+
+func TestPeerConnectionNilCallback(t *testing.T) {
+ pc, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ pc.onSignalingStateChange(SignalingStateStable)
+ pc.OnSignalingStateChange(func(ss SignalingState) {
+ t.Error("OnSignalingStateChange called")
+ })
+ pc.OnSignalingStateChange(nil)
+ pc.onSignalingStateChange(SignalingStateStable)
+
+ pc.onConnectionStateChange(PeerConnectionStateNew)
+ pc.OnConnectionStateChange(func(pcs PeerConnectionState) {
+ t.Error("OnConnectionStateChange called")
+ })
+ pc.OnConnectionStateChange(nil)
+ pc.onConnectionStateChange(PeerConnectionStateNew)
+
+ pc.onICEConnectionStateChange(ICEConnectionStateNew)
+ pc.OnICEConnectionStateChange(func(ics ICEConnectionState) {
+ t.Error("OnConnectionStateChange called")
+ })
+ pc.OnICEConnectionStateChange(nil)
+ pc.onICEConnectionStateChange(ICEConnectionStateNew)
+
+ pc.onNegotiationNeeded()
+ pc.negotiationNeededOp()
+ pc.OnNegotiationNeeded(func() {
+ t.Error("OnNegotiationNeeded called")
+ })
+ pc.OnNegotiationNeeded(nil)
+ pc.onNegotiationNeeded()
+ pc.negotiationNeededOp()
+
+ assert.NoError(t, pc.Close())
+}
+
+func TestTransceiverCreatedByRemoteSdpHasSameCodecOrderAsRemote(t *testing.T) {
+ t.Run("Codec MatchExact", func(t *testing.T) { //nolint:dupl
+ const remoteSdp = `v=0
+o=- 4596489990601351948 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=video 60323 UDP/TLS/RTP/SAVPF 98 94 106
+a=ice-ufrag:1/MvHwjAyVf27aLu
+a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
+a=ice-options:google-ice
+a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
+a=mid:0
+a=rtpmap:98 H264/90000
+a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
+a=rtpmap:94 VP8/90000
+a=rtpmap:106 H264/90000
+a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
+a=sendonly
+m=video 60323 UDP/TLS/RTP/SAVPF 108 98 125
+a=ice-ufrag:1/MvHwjAyVf27aLu
+a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
+a=ice-options:google-ice
+a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
+a=mid:1
+a=rtpmap:98 H264/90000
+a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
+a=rtpmap:108 VP8/90000
+a=sendonly
+a=rtpmap:125 H264/90000
+a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
+`
+ m := MediaEngine{}
+ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 94,
+ }, RTPCodecTypeVideo))
+ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil},
+ PayloadType: 98,
+ }, RTPCodecTypeVideo))
+
+ api := NewAPI(WithMediaEngine(&m))
+ pc, err := api.NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+ assert.NoError(t, pc.SetRemoteDescription(SessionDescription{
+ Type: SDPTypeOffer,
+ SDP: remoteSdp,
+ }))
+ ans, _ := pc.CreateAnswer(nil)
+ assert.NoError(t, pc.SetLocalDescription(ans))
+ codecOfTr1 := pc.GetTransceivers()[0].getCodecs()[0]
+ codecs := pc.api.mediaEngine.getCodecsByKind(RTPCodecTypeVideo)
+ _, matchType := codecParametersFuzzySearch(codecOfTr1, codecs)
+ assert.Equal(t, codecMatchExact, matchType)
+ codecOfTr2 := pc.GetTransceivers()[1].getCodecs()[0]
+ _, matchType = codecParametersFuzzySearch(codecOfTr2, codecs)
+ assert.Equal(t, codecMatchExact, matchType)
+ assert.EqualValues(t, 94, codecOfTr2.PayloadType)
+ assert.NoError(t, pc.Close())
+ })
+
+ t.Run("Codec PartialExact Only", func(t *testing.T) { //nolint:dupl
+ const remoteSdp = `v=0
+o=- 4596489990601351948 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+m=video 60323 UDP/TLS/RTP/SAVPF 98 106
+a=ice-ufrag:1/MvHwjAyVf27aLu
+a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
+a=ice-options:google-ice
+a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
+a=mid:0
+a=rtpmap:98 H264/90000
+a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
+a=rtpmap:106 H264/90000
+a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
+a=sendonly
+m=video 60323 UDP/TLS/RTP/SAVPF 125 98
+a=ice-ufrag:1/MvHwjAyVf27aLu
+a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
+a=ice-options:google-ice
+a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
+a=mid:1
+a=rtpmap:125 H264/90000
+a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
+a=rtpmap:98 H264/90000
+a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
+a=sendonly
+`
+ m := MediaEngine{}
+ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 94,
+ }, RTPCodecTypeVideo))
+ assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", nil},
+ PayloadType: 98,
+ }, RTPCodecTypeVideo))
+
+ api := NewAPI(WithMediaEngine(&m))
+ pc, err := api.NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+ assert.NoError(t, pc.SetRemoteDescription(SessionDescription{
+ Type: SDPTypeOffer,
+ SDP: remoteSdp,
+ }))
+ ans, _ := pc.CreateAnswer(nil)
+ assert.NoError(t, pc.SetLocalDescription(ans))
+ codecOfTr1 := pc.GetTransceivers()[0].getCodecs()[0]
+ codecs := pc.api.mediaEngine.getCodecsByKind(RTPCodecTypeVideo)
+ _, matchType := codecParametersFuzzySearch(codecOfTr1, codecs)
+ assert.Equal(t, codecMatchExact, matchType)
+ codecOfTr2 := pc.GetTransceivers()[1].getCodecs()[0]
+ _, matchType = codecParametersFuzzySearch(codecOfTr2, codecs)
+ assert.Equal(t, codecMatchExact, matchType)
+ // h.264/profile-id=640032 should be remap to 106 as same as transceiver 1
+ assert.EqualValues(t, 106, codecOfTr2.PayloadType)
+ assert.NoError(t, pc.Close())
+ })
+}
+
+// Assert that remote candidates with an unknown transport are ignored and logged.
+// This allows us to accept SessionDescriptions with proprietary candidates
+// like `ssltcp`.
+func TestInvalidCandidateTransport(t *testing.T) {
+ const (
+ sslTCPCandidate = `candidate:1 1 ssltcp 1 127.0.0.1 443 typ host generation 0`
+ sslTCPOffer = `v=0
+o=- 0 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=msid-semantic: WMS
+m=application 9 DTLS/SCTP 5000
+c=IN IP4 0.0.0.0
+a=ice-ufrag:1/MvHwjAyVf27aLu
+a=ice-pwd:3dBU7cFOBl120v33cynDvN1E
+a=fingerprint:sha-256 75:74:5A:A6:A4:E5:52:F4:A7:67:4C:01:C7:EE:91:3F:21:3D:A2:E3:53:7B:6F:30:86:F2:30:AA:65:FB:04:24
+a=mid:0
+a=` + sslTCPCandidate + "\n"
+ )
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: sslTCPOffer}))
+ assert.NoError(t, peerConnection.AddICECandidate(ICECandidateInit{Candidate: sslTCPCandidate}))
+
+ assert.NoError(t, peerConnection.Close())
+}
+
+func TestOfferWithInactiveDirection(t *testing.T) {
+ const remoteSDP = `v=0
+o=- 4596489990601351948 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=fingerprint:sha-256 F7:BF:B4:42:5B:44:C0:B9:49:70:6D:26:D7:3E:E6:08:B1:5B:25:2E:32:88:50:B6:3C:BE:4E:18:A7:2C:85:7C
+a=group:BUNDLE 0 1
+a=msid-semantic:WMS *
+m=video 9 UDP/TLS/RTP/SAVPF 97
+c=IN IP4 0.0.0.0
+a=inactive
+a=ice-pwd:05d682b2902af03db90d9a9a5f2f8d7f
+a=ice-ufrag:93cc7e4d
+a=mid:0
+a=rtpmap:97 H264/90000
+a=setup:actpass
+a=ssrc:1455629982 cname:{61fd3093-0326-4b12-8258-86bdc1fe677a}
+`
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: remoteSDP}))
+ assert.Equal(t, RTPTransceiverDirectionInactive, peerConnection.rtpTransceivers[0].direction.Load().(RTPTransceiverDirection))
+
+ assert.NoError(t, peerConnection.Close())
+}
diff --git a/peerconnection_js.go b/peerconnection_js.go
index db3c1af59a7..8e555778334 100644
--- a/peerconnection_js.go
+++ b/peerconnection_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
// Package webrtc implements the WebRTC 1.0 as defined in W3C WebRTC specification document.
@@ -55,6 +56,7 @@ func (api *API) NewPeerConnection(configuration Configuration) (_ *PeerConnectio
}, nil
}
+// JSValue returns the underlying PeerConnection
func (pc *PeerConnection) JSValue() js.Value {
return pc.underlying
}
@@ -500,7 +502,6 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPT
return &RTPTransceiver{
underlying: pc.underlying.Call("addTransceiver", kind.String(), rtpTransceiverInitInitToValue(init[0])),
}, err
-
}
return &RTPTransceiver{
@@ -522,6 +523,21 @@ func (pc *PeerConnection) GetTransceivers() (transceivers []*RTPTransceiver) {
return
}
+// SCTP returns the SCTPTransport for this PeerConnection
+//
+// The SCTP transport over which SCTP data is sent and received. If SCTP has not been negotiated, the value is nil.
+// https://www.w3.org/TR/webrtc/#attributes-15
+func (pc *PeerConnection) SCTP() *SCTPTransport {
+ underlying := pc.underlying.Get("sctp")
+ if underlying.IsNull() || underlying.IsUndefined() {
+ return nil
+ }
+
+ return &SCTPTransport{
+ underlying: underlying,
+ }
+}
+
// 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.
@@ -561,7 +577,7 @@ func iceServerToValue(server ICEServer) js.Value {
}
func valueToConfiguration(configValue js.Value) Configuration {
- if jsValueIsNull(configValue) || jsValueIsUndefined(configValue) {
+ if configValue.IsNull() || configValue.IsUndefined() {
return Configuration{}
}
return Configuration{
@@ -578,7 +594,7 @@ func valueToConfiguration(configValue js.Value) Configuration {
}
func valueToICEServers(iceServersValue js.Value) []ICEServer {
- if jsValueIsNull(iceServersValue) || jsValueIsUndefined(iceServersValue) {
+ if iceServersValue.IsNull() || iceServersValue.IsUndefined() {
return nil
}
iceServers := make([]ICEServer, iceServersValue.Length())
@@ -599,10 +615,10 @@ func valueToICEServer(iceServerValue js.Value) ICEServer {
}
func valueToICECandidate(val js.Value) *ICECandidate {
- if jsValueIsNull(val) || jsValueIsUndefined(val) {
+ if val.IsNull() || val.IsUndefined() {
return nil
}
- if jsValueIsUndefined(val.Get("protocol")) && !jsValueIsUndefined(val.Get("candidate")) {
+ if val.Get("protocol").IsUndefined() && !val.Get("candidate").IsUndefined() {
// Missing some fields, assume it's Firefox and parse SDP candidate.
c, err := ice.UnmarshalCandidate(val.Get("candidate").String())
if err != nil {
@@ -653,7 +669,7 @@ func sessionDescriptionToValue(desc *SessionDescription) js.Value {
}
func valueToSessionDescription(descValue js.Value) *SessionDescription {
- if jsValueIsNull(descValue) || jsValueIsUndefined(descValue) {
+ if descValue.IsNull() || descValue.IsUndefined() {
return nil
}
return &SessionDescription{
diff --git a/peerconnection_media_test.go b/peerconnection_media_test.go
index 1dd3b048079..1af132c95ab 100644
--- a/peerconnection_media_test.go
+++ b/peerconnection_media_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -14,9 +15,11 @@ import (
"testing"
"time"
+ "github.com/pion/logging"
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/rtp"
+ "github.com/pion/sdp/v3"
"github.com/pion/transport/test"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/stretchr/testify/assert"
@@ -29,6 +32,18 @@ var (
errNoTransceiverwithMid = errors.New("no transceiver with mid")
)
+func registerSimulcastHeaderExtensions(m *MediaEngine, codecType RTPCodecType) {
+ for _, extension := range []string{
+ sdp.SDESMidURI,
+ sdp.SDESRTPStreamIDURI,
+ sdesRepairRTPStreamIDURI,
+ } {
+ if err := m.RegisterHeaderExtension(RTPHeaderExtensionCapability{URI: extension}, codecType); err != nil {
+ panic(err)
+ }
+ }
+}
+
/*
Integration test for bi-directional peers
@@ -116,7 +131,7 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
}
})
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, expectedTrackID, expectedStreamID)
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, expectedTrackID, expectedStreamID)
if err != nil {
t.Fatal(err)
}
@@ -128,6 +143,9 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
go func() {
for {
time.Sleep(time.Millisecond * 100)
+ if pcOffer.ICEConnectionState() != ICEConnectionStateConnected {
+ continue
+ }
if routineErr := vp8Track.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}); routineErr != nil {
fmt.Println(routineErr)
}
@@ -144,7 +162,7 @@ func TestPeerConnection_Media_Sample(t *testing.T) {
go func() {
for {
time.Sleep(time.Millisecond * 100)
- if routineErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{SenderSSRC: uint32(sender.ssrc), MediaSSRC: uint32(sender.ssrc)}}); routineErr != nil {
+ if routineErr := pcOffer.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{SenderSSRC: uint32(sender.trackEncodings[0].ssrc), MediaSSRC: uint32(sender.trackEncodings[0].ssrc)}}); routineErr != nil {
awaitRTCPSenderSend <- routineErr
}
@@ -220,12 +238,12 @@ func TestPeerConnection_Media_Shutdown(t *testing.T) {
t.Fatal(err)
}
- opusTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "audio", "pion1")
+ opusTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio", "pion1")
if err != nil {
t.Fatal(err)
}
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -303,14 +321,17 @@ func TestPeerConnection_Media_Disconnected(t *testing.T) {
defer report()
s := SettingEngine{}
- s.SetICETimeouts(1*time.Second, 5*time.Second, 250*time.Millisecond)
+ s.SetICETimeouts(time.Second/2, time.Second/2, time.Second/8)
- pcOffer, pcAnswer, err := newPair()
+ m := &MediaEngine{}
+ assert.NoError(t, m.RegisterDefaultCodecs())
+
+ pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(s), WithMediaEngine(m)).newPair(Configuration{})
if err != nil {
t.Fatal(err)
}
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
if err != nil {
t.Fatal(err)
}
@@ -362,6 +383,43 @@ func TestPeerConnection_Media_Disconnected(t *testing.T) {
assert.NoError(t, pcOffer.Close())
}
+type undeclaredSsrcLogger struct{ unhandledSimulcastError chan struct{} }
+
+func (u *undeclaredSsrcLogger) Trace(msg string) {}
+func (u *undeclaredSsrcLogger) Tracef(format string, args ...interface{}) {}
+func (u *undeclaredSsrcLogger) Debug(msg string) {}
+func (u *undeclaredSsrcLogger) Debugf(format string, args ...interface{}) {}
+func (u *undeclaredSsrcLogger) Info(msg string) {}
+func (u *undeclaredSsrcLogger) Infof(format string, args ...interface{}) {}
+func (u *undeclaredSsrcLogger) Warn(msg string) {}
+func (u *undeclaredSsrcLogger) Warnf(format string, args ...interface{}) {}
+func (u *undeclaredSsrcLogger) Error(msg string) {}
+func (u *undeclaredSsrcLogger) Errorf(format string, args ...interface{}) {
+ if format == incomingUnhandledRTPSsrc {
+ close(u.unhandledSimulcastError)
+ }
+}
+
+type undeclaredSsrcLoggerFactory struct{ unhandledSimulcastError chan struct{} }
+
+func (u *undeclaredSsrcLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
+ return &undeclaredSsrcLogger{u.unhandledSimulcastError}
+}
+
+// Filter SSRC lines
+func filterSsrc(offer string) (filteredSDP string) {
+ scanner := bufio.NewScanner(strings.NewReader(offer))
+ for scanner.Scan() {
+ l := scanner.Text()
+ if strings.HasPrefix(l, "a=ssrc") {
+ continue
+ }
+
+ filteredSDP += l + "\n"
+ }
+ return
+}
+
// If a SessionDescription has a single media section and no SSRC
// assume that it is meant to handle all RTP packets
func TestUndeclaredSSRC(t *testing.T) {
@@ -371,83 +429,86 @@ func TestUndeclaredSSRC(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
- pcOffer, pcAnswer, err := newPair()
- assert.NoError(t, err)
+ t.Run("No SSRC", func(t *testing.T) {
+ pcOffer, pcAnswer, err := newPair()
+ assert.NoError(t, err)
- _, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo)
- assert.NoError(t, err)
+ vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
+ assert.NoError(t, err)
- _, err = pcOffer.CreateDataChannel("test-channel", nil)
- assert.NoError(t, err)
+ _, err = pcOffer.AddTrack(vp8Writer)
+ assert.NoError(t, err)
- vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
- assert.NoError(t, err)
+ onTrackFired := make(chan struct{})
+ pcAnswer.OnTrack(func(trackRemote *TrackRemote, r *RTPReceiver) {
+ assert.Equal(t, trackRemote.StreamID(), vp8Writer.StreamID())
+ assert.Equal(t, trackRemote.ID(), vp8Writer.ID())
+ close(onTrackFired)
+ })
- _, err = pcOffer.AddTrack(vp8Writer)
- assert.NoError(t, err)
+ offer, err := pcOffer.CreateOffer(nil)
+ assert.NoError(t, err)
- onTrackFired := make(chan *TrackRemote)
- pcAnswer.OnTrack(func(t *TrackRemote, r *RTPReceiver) {
- close(onTrackFired)
- })
+ offerGatheringComplete := GatheringCompletePromise(pcOffer)
+ assert.NoError(t, pcOffer.SetLocalDescription(offer))
+ <-offerGatheringComplete
- offer, err := pcOffer.CreateOffer(nil)
- assert.NoError(t, err)
+ offer.SDP = filterSsrc(pcOffer.LocalDescription().SDP)
+ assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
- offerGatheringComplete := GatheringCompletePromise(pcOffer)
- assert.NoError(t, pcOffer.SetLocalDescription(offer))
+ answer, err := pcAnswer.CreateAnswer(nil)
+ assert.NoError(t, err)
- <-offerGatheringComplete
- offer = *pcOffer.LocalDescription()
+ answerGatheringComplete := GatheringCompletePromise(pcAnswer)
+ assert.NoError(t, pcAnswer.SetLocalDescription(answer))
+ <-answerGatheringComplete
- // Filter SSRC lines, and remove SCTP
- filteredSDP := ""
- scanner := bufio.NewScanner(strings.NewReader(offer.SDP))
- inApplicationMedia := false
- for scanner.Scan() {
- l := scanner.Text()
- if strings.HasPrefix(l, "m=application") {
- inApplicationMedia = !inApplicationMedia
- } else if strings.HasPrefix(l, "a=ssrc") {
- continue
- }
+ assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
- if inApplicationMedia {
- continue
- }
+ sendVideoUntilDone(onTrackFired, t, []*TrackLocalStaticSample{vp8Writer})
+ closePairNow(t, pcOffer, pcAnswer)
+ })
- filteredSDP += l + "\n"
- }
+ t.Run("Has RID", func(t *testing.T) {
+ unhandledSimulcastError := make(chan struct{})
- offer.SDP = filteredSDP
+ m := &MediaEngine{}
+ assert.NoError(t, m.RegisterDefaultCodecs())
- assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
+ pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(SettingEngine{
+ LoggerFactory: &undeclaredSsrcLoggerFactory{unhandledSimulcastError},
+ }), WithMediaEngine(m)).newPair(Configuration{})
+ assert.NoError(t, err)
- answer, err := pcAnswer.CreateAnswer(nil)
- assert.NoError(t, err)
+ vp8Writer, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
+ assert.NoError(t, err)
- answerGatheringComplete := GatheringCompletePromise(pcAnswer)
- assert.NoError(t, pcAnswer.SetLocalDescription(answer))
- <-answerGatheringComplete
+ _, err = pcOffer.AddTrack(vp8Writer)
+ assert.NoError(t, err)
- assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
+ offer, err := pcOffer.CreateOffer(nil)
+ assert.NoError(t, err)
- go func() {
- for {
- assert.NoError(t, vp8Writer.WriteSample(media.Sample{Data: []byte{0x00}, Duration: time.Second}))
- time.Sleep(time.Millisecond * 25)
+ offerGatheringComplete := GatheringCompletePromise(pcOffer)
+ assert.NoError(t, pcOffer.SetLocalDescription(offer))
+ <-offerGatheringComplete
- select {
- case <-onTrackFired:
- return
- default:
- continue
- }
- }
- }()
+ // Append RID to end of SessionDescription. Will not be considered unhandled anymore
+ offer.SDP = filterSsrc(pcOffer.LocalDescription().SDP) + "a=" + sdpAttributeRid + "\r\n"
+ assert.NoError(t, pcAnswer.SetRemoteDescription(offer))
- <-onTrackFired
- closePairNow(t, pcOffer, pcAnswer)
+ answer, err := pcAnswer.CreateAnswer(nil)
+ assert.NoError(t, err)
+
+ answerGatheringComplete := GatheringCompletePromise(pcAnswer)
+ assert.NoError(t, pcAnswer.SetLocalDescription(answer))
+ <-answerGatheringComplete
+
+ assert.NoError(t, pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription()))
+
+ sendVideoUntilDone(unhandledSimulcastError, t, []*TrackLocalStaticSample{vp8Writer})
+ closePairNow(t, pcOffer, pcAnswer)
+ })
}
func TestAddTransceiverFromTrackSendOnly(t *testing.T) {
@@ -570,7 +631,7 @@ func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
assert.Equal(t, []*RTPTransceiver{tr}, pc.GetTransceivers())
addTrack := func() (TrackLocal, *RTPSender) {
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
sender, err := pc.AddTrack(track)
@@ -582,12 +643,12 @@ func TestAddTransceiverAddTrack_Reuse(t *testing.T) {
track1, sender1 := addTrack()
assert.Equal(t, 1, len(pc.GetTransceivers()))
assert.Equal(t, sender1, tr.Sender())
- assert.Equal(t, track1, tr.Sender().track)
+ assert.Equal(t, track1, tr.Sender().Track())
require.NoError(t, pc.RemoveTrack(sender1))
track2, _ := addTrack()
assert.Equal(t, 1, len(pc.GetTransceivers()))
- assert.Equal(t, track2, tr.Sender().track)
+ assert.Equal(t, track2, tr.Sender().Track())
addTrack()
assert.Equal(t, 2, len(pc.GetTransceivers()))
@@ -608,7 +669,7 @@ func TestAddTransceiverAddTrack_NewRTPSender_Error(t *testing.T) {
dtlsTransport := pc.dtlsTransport
pc.dtlsTransport = nil
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
_, err = pc.AddTrack(track)
@@ -633,11 +694,11 @@ func TestRtpSenderReceiver_ReadClose_Error(t *testing.T) {
sender, receiver := tr.Sender(), tr.Receiver()
assert.NoError(t, sender.Stop())
_, _, err = sender.Read(make([]byte, 0, 1400))
- assert.Error(t, err, io.ErrClosedPipe)
+ assert.ErrorIs(t, err, io.ErrClosedPipe)
assert.NoError(t, receiver.Stop())
_, _, err = receiver.Read(make([]byte, 0, 1400))
- assert.Error(t, err, io.ErrClosedPipe)
+ assert.ErrorIs(t, err, io.ErrClosedPipe)
assert.NoError(t, pc.Close())
}
@@ -694,7 +755,7 @@ func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) {
}
track, err := NewTrackLocalStaticSample(
- RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"},
+ RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"},
"track-id",
"track-label",
)
@@ -717,7 +778,7 @@ func TestAddTransceiverFromTrackFailsRecvOnly(t *testing.T) {
func TestPlanBMediaExchange(t *testing.T) {
runTest := func(trackCount int, t *testing.T) {
addSingleTrack := func(p *PeerConnection) *TrackLocalStaticSample {
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()))
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()), fmt.Sprintf("video-%d", randutil.NewMathRandomGenerator().Uint32()))
assert.NoError(t, err)
_, err = p.AddTrack(track)
@@ -802,7 +863,7 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) {
assert.NoError(t, err)
defer func() { assert.NoError(t, pcAnswer.Close()) }()
- track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
@@ -823,7 +884,7 @@ func TestPeerConnection_Start_Only_Negotiated_Senders(t *testing.T) {
// Add a new track between providing the offer and applying the answer
- track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
+ track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
require.NoError(t, err)
sender2, err := pcOffer.AddTrack(track2)
@@ -866,7 +927,7 @@ func TestPeerConnection_Start_Right_Receiver(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
@@ -919,69 +980,165 @@ func TestPeerConnection_Start_Right_Receiver(t *testing.T) {
closePairNow(t, pcOffer, pcAnswer)
}
-// Assert that failed Simulcast probing doesn't cause
-// the handleUndeclaredSSRC to be leaked
func TestPeerConnection_Simulcast_Probe(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
+ lim := test.TimeOut(time.Second * 30) //nolint
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
- track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
- assert.NoError(t, err)
+ // Assert that failed Simulcast probing doesn't cause
+ // the handleUndeclaredSSRC to be leaked
+ t.Run("Leak", func(t *testing.T) {
+ track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
- offerer, answerer, err := newPair()
- assert.NoError(t, err)
+ offerer, answerer, err := newPair()
+ assert.NoError(t, err)
- _, err = offerer.AddTrack(track)
- assert.NoError(t, err)
+ _, err = offerer.AddTrack(track)
+ assert.NoError(t, err)
- ticker := time.NewTicker(time.Millisecond * 20)
- testFinished := make(chan struct{})
- seenFiveStreams, seenFiveStreamsCancel := context.WithCancel(context.Background())
+ ticker := time.NewTicker(time.Millisecond * 20)
+ testFinished := make(chan struct{})
+ seenFiveStreams, seenFiveStreamsCancel := context.WithCancel(context.Background())
- go func() {
- for {
- select {
- case <-testFinished:
- return
- case <-ticker.C:
- answerer.dtlsTransport.lock.Lock()
- if len(answerer.dtlsTransport.simulcastStreams) >= 5 {
- seenFiveStreamsCancel()
+ go func() {
+ for {
+ select {
+ case <-testFinished:
+ return
+ case <-ticker.C:
+ answerer.dtlsTransport.lock.Lock()
+ if len(answerer.dtlsTransport.simulcastStreams) >= 5 {
+ seenFiveStreamsCancel()
+ }
+ answerer.dtlsTransport.lock.Unlock()
+
+ track.mu.Lock()
+ if len(track.bindings) == 1 {
+ _, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{
+ Version: 2,
+ SSRC: randutil.NewMathRandomGenerator().Uint32(),
+ }, []byte{0, 1, 2, 3, 4, 5})
+ assert.NoError(t, err)
+ }
+ track.mu.Unlock()
}
- answerer.dtlsTransport.lock.Unlock()
-
- track.mu.Lock()
- if len(track.bindings) == 1 {
- _, err = track.bindings[0].writeStream.WriteRTP(&rtp.Header{
- Version: 2,
- SSRC: randutil.NewMathRandomGenerator().Uint32(),
- }, []byte{0, 1, 2, 3, 4, 5})
- assert.NoError(t, err)
+ }
+ }()
+
+ assert.NoError(t, signalPair(offerer, answerer))
+
+ peerConnectionConnected := untilConnectionState(PeerConnectionStateConnected, offerer, answerer)
+ peerConnectionConnected.Wait()
+
+ <-seenFiveStreams.Done()
+
+ closePairNow(t, offerer, answerer)
+ close(testFinished)
+ })
+
+ // Assert that NonSimulcast Traffic isn't incorrectly broken by the probe
+ t.Run("Break NonSimulcast", func(t *testing.T) {
+ unhandledSimulcastError := make(chan struct{})
+
+ m := &MediaEngine{}
+ if err := m.RegisterDefaultCodecs(); err != nil {
+ panic(err)
+ }
+ registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo)
+
+ pcOffer, pcAnswer, err := NewAPI(WithSettingEngine(SettingEngine{
+ LoggerFactory: &undeclaredSsrcLoggerFactory{unhandledSimulcastError},
+ }), WithMediaEngine(m)).newPair(Configuration{})
+ assert.NoError(t, err)
+
+ firstTrack, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "firstTrack", "firstTrack")
+ assert.NoError(t, err)
+
+ _, err = pcOffer.AddTrack(firstTrack)
+ assert.NoError(t, err)
+
+ secondTrack, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "secondTrack", "secondTrack")
+ assert.NoError(t, err)
+
+ _, err = pcOffer.AddTrack(secondTrack)
+ assert.NoError(t, err)
+
+ assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) (filtered string) {
+ shouldDiscard := false
+
+ scanner := bufio.NewScanner(strings.NewReader(sessionDescription))
+ for scanner.Scan() {
+ if strings.HasPrefix(scanner.Text(), "m=video") {
+ shouldDiscard = !shouldDiscard
+ }
+
+ if !shouldDiscard {
+ filtered += scanner.Text() + "\r\n"
}
- track.mu.Unlock()
}
+
+ return
+ }))
+
+ sequenceNumber := uint16(0)
+ sendRTPPacket := func() {
+ sequenceNumber++
+ assert.NoError(t, firstTrack.WriteRTP(&rtp.Packet{
+ Header: rtp.Header{
+ Version: 2,
+ SequenceNumber: sequenceNumber,
+ },
+ Payload: []byte{0x00},
+ }))
+ time.Sleep(20 * time.Millisecond)
}
- }()
- assert.NoError(t, signalPair(offerer, answerer))
+ for ; sequenceNumber <= 5; sequenceNumber++ {
+ sendRTPPacket()
+ }
+
+ assert.NoError(t, signalPair(pcOffer, pcAnswer))
+
+ trackRemoteChan := make(chan *TrackRemote, 1)
+ pcAnswer.OnTrack(func(trackRemote *TrackRemote, _ *RTPReceiver) {
+ trackRemoteChan <- trackRemote
+ })
+
+ trackRemote := func() *TrackRemote {
+ for {
+ select {
+ case t := <-trackRemoteChan:
+ return t
+ default:
+ sendRTPPacket()
+ }
+ }
+ }()
- peerConnectionConnected := untilConnectionState(PeerConnectionStateConnected, offerer, answerer)
- peerConnectionConnected.Wait()
+ func() {
+ for {
+ select {
+ case <-unhandledSimulcastError:
+ return
+ default:
+ sendRTPPacket()
+ }
+ }
+ }()
- <-seenFiveStreams.Done()
+ _, _, err = trackRemote.Read(make([]byte, 1500))
+ assert.NoError(t, err)
- closePairNow(t, offerer, answerer)
- close(testFinished)
+ closePairNow(t, pcOffer, pcAnswer)
+ })
}
-// Assert that CreateOffer can't enter infinite loop
-// We attempt to generate an offer multiple times in case a user
-// has edited the PeerConnection. We can assert this broken behavior with an
-// empty MediaEngine. See pion/webrtc#1656 for full behavior
-func TestPeerConnection_CreateOffer_InfiniteLoop(t *testing.T) {
+// Assert that CreateOffer returns an error for a RTPSender with no codecs
+// pion/webrtc#1702
+func TestPeerConnection_CreateOffer_NoCodecs(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
@@ -993,14 +1150,14 @@ func TestPeerConnection_CreateOffer_InfiniteLoop(t *testing.T) {
pc, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pc.AddTrack(track)
assert.NoError(t, err)
_, err = pc.CreateOffer(nil)
- assert.Error(t, err, errExcessiveRetries)
+ assert.Equal(t, err, ErrSenderWithNoCodecs)
assert.NoError(t, pc.Close())
}
@@ -1011,7 +1168,7 @@ func TestPeerConnection_RaceReplaceTrack(t *testing.T) {
assert.NoError(t, err)
addTrack := func() *TrackLocalStaticSample {
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
_, err = pc.AddTrack(track)
assert.NoError(t, err)
@@ -1053,6 +1210,133 @@ func TestPeerConnection_RaceReplaceTrack(t *testing.T) {
assert.NoError(t, pc.Close())
}
+func TestPeerConnection_Simulcast(t *testing.T) {
+ lim := test.TimeOut(time.Second * 30)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ rids := []string{"a", "b", "c"}
+ var ridMapLock sync.RWMutex
+ ridMap := map[string]int{}
+
+ // Enable Extension Headers needed for Simulcast
+ m := &MediaEngine{}
+ if err := m.RegisterDefaultCodecs(); err != nil {
+ panic(err)
+ }
+ registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo)
+
+ assertRidCorrect := func(t *testing.T) {
+ ridMapLock.Lock()
+ defer ridMapLock.Unlock()
+
+ for _, rid := range rids {
+ assert.Equal(t, ridMap[rid], 1)
+ }
+ assert.Equal(t, len(ridMap), 3)
+ }
+
+ ridsFullfilled := func() bool {
+ ridMapLock.Lock()
+ defer ridMapLock.Unlock()
+
+ ridCount := len(ridMap)
+ return ridCount == 3
+ }
+
+ onTrackHandler := func(trackRemote *TrackRemote, _ *RTPReceiver) {
+ ridMapLock.Lock()
+ defer ridMapLock.Unlock()
+ ridMap[trackRemote.RID()] = ridMap[trackRemote.RID()] + 1
+ }
+
+ t.Run("RTP Extension Based", func(t *testing.T) {
+ pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{})
+ assert.NoError(t, err)
+
+ vp8WriterA, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("a"))
+ assert.NoError(t, err)
+
+ sender, err := pcOffer.AddTrack(vp8WriterA)
+ assert.NoError(t, err)
+ assert.NotNil(t, sender)
+
+ vp8WriterB, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("b"))
+ assert.NoError(t, err)
+ err = sender.AddEncoding(vp8WriterB)
+ assert.NoError(t, err)
+
+ vp8WriterC, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2", WithRTPStreamID("c"))
+ assert.NoError(t, err)
+ err = sender.AddEncoding(vp8WriterC)
+ assert.NoError(t, err)
+
+ ridMap = map[string]int{}
+ pcAnswer.OnTrack(onTrackHandler)
+
+ parameters := sender.GetParameters()
+ assert.Equal(t, "a", parameters.Encodings[0].RID)
+ assert.Equal(t, "b", parameters.Encodings[1].RID)
+ assert.Equal(t, "c", parameters.Encodings[2].RID)
+
+ var midID, ridID, rsidID uint8
+ for _, extension := range parameters.HeaderExtensions {
+ switch extension.URI {
+ case sdp.SDESMidURI:
+ midID = uint8(extension.ID)
+ case sdp.SDESRTPStreamIDURI:
+ ridID = uint8(extension.ID)
+ case sdesRepairRTPStreamIDURI:
+ rsidID = uint8(extension.ID)
+ }
+ }
+ assert.NotZero(t, midID)
+ assert.NotZero(t, ridID)
+ assert.NotZero(t, rsidID)
+
+ assert.NoError(t, signalPair(pcOffer, pcAnswer))
+
+ for sequenceNumber := uint16(0); !ridsFullfilled(); sequenceNumber++ {
+ time.Sleep(20 * time.Millisecond)
+
+ for ssrc, rid := range rids {
+ header := &rtp.Header{
+ Version: 2,
+ SSRC: uint32(ssrc),
+ SequenceNumber: sequenceNumber,
+ PayloadType: 96,
+ }
+ assert.NoError(t, header.SetExtension(midID, []byte("0")))
+
+ // Send RSID for first 10 packets
+ if sequenceNumber >= 10 {
+ assert.NoError(t, header.SetExtension(ridID, []byte(rid)))
+ } else {
+ assert.NoError(t, header.SetExtension(rsidID, []byte(rid)))
+ header.SSRC += 10
+ }
+
+ var writer *TrackLocalStaticRTP
+ switch rid {
+ case "a":
+ writer = vp8WriterA
+ case "b":
+ writer = vp8WriterB
+ case "c":
+ writer = vp8WriterC
+ }
+ _, err = writer.bindings[0].writeStream.WriteRTP(header, []byte{0x00})
+ assert.NoError(t, err)
+ }
+ }
+
+ assertRidCorrect(t)
+ closePairNow(t, pcOffer, pcAnswer)
+ })
+}
+
// Issue #1636
func TestPeerConnection_DTLS_Restart_MediaAndDataChannel(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
diff --git a/peerconnection_renegotiation_test.go b/peerconnection_renegotiation_test.go
index 71f14e78478..e081c1c95ea 100644
--- a/peerconnection_renegotiation_test.go
+++ b/peerconnection_renegotiation_test.go
@@ -1,8 +1,10 @@
+//go:build !js
// +build !js
package webrtc
import (
+ "bufio"
"context"
"io"
"strconv"
@@ -12,6 +14,7 @@ import (
"testing"
"time"
+ "github.com/pion/rtp"
"github.com/pion/transport/test"
"github.com/pion/webrtc/v3/internal/util"
"github.com/pion/webrtc/v3/pkg/media"
@@ -171,7 +174,7 @@ func TestPeerConnection_Renegotiation_AddTrack(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
sender, err := pcOffer.AddTrack(vp8Track)
@@ -214,7 +217,7 @@ func TestPeerConnection_Renegotiation_AddTrack_Multiple(t *testing.T) {
_, err := pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, trackID, trackID)
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, trackID, trackID)
assert.NoError(t, err)
_, err = pcOffer.AddTrack(track)
@@ -291,7 +294,7 @@ func TestPeerConnection_Renegotiation_AddTrack_Rename(t *testing.T) {
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo1", "bar1")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo1", "bar1")
assert.NoError(t, err)
_, err = pcAnswer.AddTrack(vp8Track)
assert.NoError(t, err)
@@ -330,13 +333,13 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
require.NoError(t, err)
- track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion2")
+ track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
require.NoError(t, err)
sender2, err := pcOffer.AddTrack(track2)
@@ -368,7 +371,7 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
// Must have 3 media descriptions (2 video channels)
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
- assert.True(t, sdpMidHasSsrc(offer, "0", sender1.ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", sender1.ssrc, offer.SDP)
+ assert.True(t, sdpMidHasSsrc(offer, "0", sender1.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "0", sender1.trackEncodings[0].ssrc, offer.SDP)
// Remove first track, must keep same number of media
// descriptions and same track ssrc for mid 1 as previous
@@ -379,15 +382,15 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
- assert.True(t, sdpMidHasSsrc(offer, "1", sender2.ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", sender2.ssrc, offer.SDP)
+ assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.SDP: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP)
_, err = pcAnswer.CreateAnswer(nil)
- assert.Error(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState})
+ assert.Equal(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState})
pcOffer.ops.Done()
pcAnswer.ops.Done()
- track3, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion3")
+ track3, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion3")
require.NoError(t, err)
sender3, err := pcOffer.AddTrack(track3)
@@ -399,8 +402,8 @@ func TestPeerConnection_Transceiver_Mid(t *testing.T) {
// We reuse the existing non-sending transceiver
assert.Equal(t, len(offer.parsed.MediaDescriptions), 2)
- assert.True(t, sdpMidHasSsrc(offer, "0", sender3.ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", sender3.ssrc, offer.SDP)
- assert.True(t, sdpMidHasSsrc(offer, "1", sender2.ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", sender2.ssrc, offer.SDP)
+ assert.True(t, sdpMidHasSsrc(offer, "0", sender3.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "0", sender3.trackEncodings[0].ssrc, offer.SDP)
+ assert.True(t, sdpMidHasSsrc(offer, "1", sender2.trackEncodings[0].ssrc), "Expected mid %q with ssrc %d, offer.sdp: %s", "1", sender2.trackEncodings[0].ssrc, offer.SDP)
closePairNow(t, pcOffer, pcAnswer)
}
@@ -418,10 +421,10 @@ func TestPeerConnection_Renegotiation_CodecChange(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video1", "pion1")
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion1")
require.NoError(t, err)
- track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video2", "pion2")
+ track2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video2", "pion2")
require.NoError(t, err)
sender1, err := pcOffer.AddTrack(track1)
@@ -511,7 +514,7 @@ func TestPeerConnection_Renegotiation_RemoveTrack(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
sender, err := pcOffer.AddTrack(vp8Track)
@@ -565,7 +568,7 @@ func TestPeerConnection_RoleSwitch(t *testing.T) {
_, err = pcFirstOfferer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ vp8Track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
_, err = pcSecondOfferer.AddTrack(vp8Track)
@@ -672,7 +675,7 @@ func TestPeerConnection_Renegotiation_SetLocalDescription(t *testing.T) {
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
assert.NoError(t, err)
- localTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "foo", "bar")
+ localTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "foo", "bar")
assert.NoError(t, err)
sender, err := pcAnswer.AddTrack(localTrack)
@@ -785,7 +788,7 @@ func TestAddDataChannelDuringRenegotation(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pcOffer.AddTrack(track)
@@ -877,7 +880,7 @@ func TestNegotiationNeededRemoveTrack(t *testing.T) {
pcAnswer, err := NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
pcOffer.OnNegotiationNeeded(func() {
@@ -939,7 +942,7 @@ func TestNegotiationNeededStressOneSided(t *testing.T) {
})
for i := 0; i < expectedTrackCount; i++ {
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pcA.AddTrack(track)
@@ -993,3 +996,151 @@ func TestPeerConnection_Renegotiation_DisableTrack(t *testing.T) {
closePairNow(t, pcOffer, pcAnswer)
}
+
+func TestPeerConnection_Renegotiation_Simulcast(t *testing.T) {
+ lim := test.TimeOut(time.Second * 30)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ m := &MediaEngine{}
+ if err := m.RegisterDefaultCodecs(); err != nil {
+ panic(err)
+ }
+ registerSimulcastHeaderExtensions(m, RTPCodecTypeVideo)
+
+ originalRids := []string{"a", "b", "c"}
+ signalWithRids := func(sessionDescription string, rids []string) string {
+ sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0]
+ sessionDescription = filterSsrc(sessionDescription)
+ for _, rid := range rids {
+ sessionDescription += "a=" + sdpAttributeRid + ":" + rid + " send\r\n"
+ }
+ return sessionDescription + "a=simulcast:send " + strings.Join(rids, ";") + "\r\n"
+ }
+
+ var trackMapLock sync.RWMutex
+ trackMap := map[string]*TrackRemote{}
+
+ onTrackHandler := func(track *TrackRemote, _ *RTPReceiver) {
+ trackMapLock.Lock()
+ defer trackMapLock.Unlock()
+ trackMap[track.RID()] = track
+ }
+
+ sendUntilAllTracksFired := func(vp8Writer *TrackLocalStaticRTP, rids []string) {
+ allTracksFired := func() bool {
+ trackMapLock.Lock()
+ defer trackMapLock.Unlock()
+
+ return len(trackMap) == len(rids)
+ }
+
+ for sequenceNumber := uint16(0); !allTracksFired(); sequenceNumber++ {
+ time.Sleep(20 * time.Millisecond)
+
+ for ssrc, rid := range rids {
+ header := &rtp.Header{
+ Version: 2,
+ SSRC: uint32(ssrc),
+ SequenceNumber: sequenceNumber,
+ PayloadType: 96,
+ }
+ assert.NoError(t, header.SetExtension(1, []byte("0")))
+ assert.NoError(t, header.SetExtension(2, []byte(rid)))
+
+ _, err := vp8Writer.bindings[0].writeStream.WriteRTP(header, []byte{0x00})
+ assert.NoError(t, err)
+ }
+ }
+ }
+
+ assertTracksClosed := func(t *testing.T) {
+ trackMapLock.Lock()
+ defer trackMapLock.Unlock()
+
+ for _, track := range trackMap {
+ _, _, err := track.ReadRTP() // Ignore first Read, this is our peeked data
+ assert.Nil(t, err)
+
+ _, _, err = track.ReadRTP()
+ assert.Equal(t, err, io.EOF)
+ }
+ }
+
+ t.Run("Disable Transceiver", func(t *testing.T) {
+ trackMap = map[string]*TrackRemote{}
+ pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{})
+ assert.NoError(t, err)
+
+ vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
+ assert.NoError(t, err)
+
+ rtpTransceiver, err := pcOffer.AddTransceiverFromTrack(
+ vp8Writer,
+ RTPTransceiverInit{
+ Direction: RTPTransceiverDirectionSendonly,
+ },
+ )
+ assert.NoError(t, err)
+
+ assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
+ return signalWithRids(sessionDescription, originalRids)
+ }))
+
+ pcAnswer.OnTrack(onTrackHandler)
+ sendUntilAllTracksFired(vp8Writer, originalRids)
+
+ assert.NoError(t, pcOffer.RemoveTrack(rtpTransceiver.Sender()))
+ assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
+ sessionDescription = strings.SplitAfter(sessionDescription, "a=end-of-candidates\r\n")[0]
+ return sessionDescription
+ }))
+
+ assertTracksClosed(t)
+ closePairNow(t, pcOffer, pcAnswer)
+ })
+
+ t.Run("Change RID", func(t *testing.T) {
+ trackMap = map[string]*TrackRemote{}
+ pcOffer, pcAnswer, err := NewAPI(WithMediaEngine(m)).newPair(Configuration{})
+ assert.NoError(t, err)
+
+ vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion2")
+ assert.NoError(t, err)
+
+ _, err = pcOffer.AddTransceiverFromTrack(
+ vp8Writer,
+ RTPTransceiverInit{
+ Direction: RTPTransceiverDirectionSendonly,
+ },
+ )
+ assert.NoError(t, err)
+
+ assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
+ return signalWithRids(sessionDescription, originalRids)
+ }))
+
+ pcAnswer.OnTrack(onTrackHandler)
+ sendUntilAllTracksFired(vp8Writer, originalRids)
+
+ newRids := []string{"d", "e", "f"}
+ assert.NoError(t, signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string {
+ scanner := bufio.NewScanner(strings.NewReader(sessionDescription))
+ sessionDescription = ""
+ for scanner.Scan() {
+ l := scanner.Text()
+ if strings.HasPrefix(l, "a=rid") || strings.HasPrefix(l, "a=simulcast") {
+ continue
+ }
+
+ sessionDescription += l + "\n"
+ }
+ return signalWithRids(sessionDescription, newRids)
+ }))
+
+ assertTracksClosed(t)
+ closePairNow(t, pcOffer, pcAnswer)
+ })
+}
diff --git a/peerconnection_test.go b/peerconnection_test.go
index 4c36de7bbfe..0c598184c75 100644
--- a/peerconnection_test.go
+++ b/peerconnection_test.go
@@ -28,7 +28,7 @@ func newPair() (pcOffer *PeerConnection, pcAnswer *PeerConnection, err error) {
return pca, pcb, nil
}
-func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
+func signalPairWithModification(pcOffer *PeerConnection, pcAnswer *PeerConnection, modificationFunc func(string) string) error {
// Note(albrow): We need to create a data channel in order to trigger ICE
// candidate gathering in the background for the JavaScript/Wasm bindings. If
// we don't do this, the complete offer including ICE candidates will never be
@@ -46,7 +46,9 @@ func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
return err
}
<-offerGatheringComplete
- if err = pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()); err != nil {
+
+ offer.SDP = modificationFunc(pcOffer.LocalDescription().SDP)
+ if err = pcAnswer.SetRemoteDescription(offer); err != nil {
return err
}
@@ -62,6 +64,10 @@ func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
return pcOffer.SetRemoteDescription(*pcAnswer.LocalDescription())
}
+func signalPair(pcOffer *PeerConnection, pcAnswer *PeerConnection) error {
+ return signalPairWithModification(pcOffer, pcAnswer, func(sessionDescription string) string { return sessionDescription })
+}
+
func offerMediaHasDirection(offer SessionDescription, kind RTPCodecType, direction RTPTransceiverDirection) bool {
parsed := &sdp.SessionDescription{}
if err := parsed.Unmarshal([]byte(offer.SDP)); err != nil {
@@ -77,6 +83,24 @@ func offerMediaHasDirection(offer SessionDescription, kind RTPCodecType, directi
return false
}
+func untilConnectionState(state PeerConnectionState, peers ...*PeerConnection) *sync.WaitGroup {
+ var triggered sync.WaitGroup
+ triggered.Add(len(peers))
+
+ for _, p := range peers {
+ done := false
+ hdlr := func(p PeerConnectionState) {
+ if !done && p == state {
+ done = true
+ triggered.Done()
+ }
+ }
+
+ p.OnConnectionStateChange(hdlr)
+ }
+ return &triggered
+}
+
func TestNew(t *testing.T) {
pc, err := NewPeerConnection(Configuration{
ICEServers: []ICEServer{
@@ -321,7 +345,7 @@ func TestCreateOfferAnswer(t *testing.T) {
// so CreateAnswer should return an InvalidStateError
assert.Equal(t, answerPeerConn.SignalingState(), SignalingStateStable)
_, err = answerPeerConn.CreateAnswer(nil)
- assert.Error(t, err, &rtcerr.InvalidStateError{Err: ErrIncorrectSignalingState})
+ assert.Error(t, err)
closePairNow(t, offerPeerConn, answerPeerConn)
}
@@ -710,3 +734,17 @@ func TestAddTransceiver(t *testing.T) {
assert.NoError(t, pc.Close())
}
}
+
+// Assert that SCTPTransport -> DTLSTransport -> ICETransport works after connected
+func TestTransportChain(t *testing.T) {
+ offer, answer, err := newPair()
+ assert.NoError(t, err)
+
+ peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, offer, answer)
+ assert.NoError(t, signalPair(offer, answer))
+ peerConnectionsConnected.Wait()
+
+ assert.NotNil(t, offer.SCTP().Transport().ICETransport())
+
+ closePairNow(t, offer, answer)
+}
diff --git a/pkg/media/h264reader/h264reader.go b/pkg/media/h264reader/h264reader.go
index 79fbe8452dd..45dfc17431c 100644
--- a/pkg/media/h264reader/h264reader.go
+++ b/pkg/media/h264reader/h264reader.go
@@ -166,12 +166,14 @@ func (reader *H264Reader) processByte(readByte byte) (nalFound bool) {
if reader.countOfConsecutiveZeroBytes > 2 {
countOfConsecutiveZeroBytesInPrefix = 3
}
- nalUnitLength := len(reader.nalBuffer) - countOfConsecutiveZeroBytesInPrefix
- reader.nalBuffer = reader.nalBuffer[0:nalUnitLength]
- nalFound = true
- } else {
- reader.countOfConsecutiveZeroBytes = 0
+
+ if nalUnitLength := len(reader.nalBuffer) - countOfConsecutiveZeroBytesInPrefix; nalUnitLength > 0 {
+ reader.nalBuffer = reader.nalBuffer[0:nalUnitLength]
+ nalFound = true
+ }
}
+
+ reader.countOfConsecutiveZeroBytes = 0
default:
reader.countOfConsecutiveZeroBytes = 0
}
diff --git a/pkg/media/h264reader/h264reader_test.go b/pkg/media/h264reader/h264reader_test.go
index 1716750bda6..1d57db6a97c 100644
--- a/pkg/media/h264reader/h264reader_test.go
+++ b/pkg/media/h264reader/h264reader_test.go
@@ -98,3 +98,38 @@ func TestSkipSEI(t *testing.T) {
assert.Nil(err)
assert.Equal(byte(0xAB), nal.Data[0])
}
+
+func TestIssue1734_NextNal(t *testing.T) {
+ tt := [...][]byte{
+ []byte("\x00\x00\x010\x00\x00\x01\x00\x00\x01"),
+ []byte("\x00\x00\x00\x01\x00\x00\x01"),
+ }
+
+ for _, cur := range tt {
+ r, err := NewReader(bytes.NewReader(cur))
+ assert.NoError(t, err)
+
+ // Just make sure it doesn't crash
+ for {
+ nal, err := r.NextNAL()
+
+ if err != nil || nal == nil {
+ break
+ }
+ }
+ }
+}
+
+func TestTrailing01AfterStartCode(t *testing.T) {
+ r, err := NewReader(bytes.NewReader([]byte{
+ 0x0, 0x0, 0x0, 0x1, 0x01,
+ 0x0, 0x0, 0x0, 0x1, 0x01,
+ }))
+ assert.NoError(t, err)
+
+ for i := 0; i <= 1; i++ {
+ nal, err := r.NextNAL()
+ assert.NoError(t, err)
+ assert.NotNil(t, nal)
+ }
+}
diff --git a/pkg/media/h264writer/h264writer.go b/pkg/media/h264writer/h264writer.go
index 578af017873..8eea3fe654b 100644
--- a/pkg/media/h264writer/h264writer.go
+++ b/pkg/media/h264writer/h264writer.go
@@ -81,16 +81,25 @@ func (h *H264Writer) Close() error {
}
func isKeyFrame(data []byte) bool {
- const typeSTAPA = 24
+ const (
+ typeSTAPA = 24
+ typeSPS = 7
+ naluTypeBitmask = 0x1F
+ )
var word uint32
payload := bytes.NewReader(data)
- err := binary.Read(payload, binary.BigEndian, &word)
-
- if err != nil || (word&0x1F000000)>>24 != typeSTAPA {
+ if err := binary.Read(payload, binary.BigEndian, &word); err != nil {
return false
}
- return word&0x1F == 7
+ naluType := (word >> 24) & naluTypeBitmask
+ if naluType == typeSTAPA && word&naluTypeBitmask == typeSPS {
+ return true
+ } else if naluType == typeSPS {
+ return true
+ }
+
+ return false
}
diff --git a/pkg/media/h264writer/h264writer_test.go b/pkg/media/h264writer/h264writer_test.go
index b414d763deb..b40d3917401 100644
--- a/pkg/media/h264writer/h264writer_test.go
+++ b/pkg/media/h264writer/h264writer_test.go
@@ -37,10 +37,15 @@ func TestIsKeyFrame(t *testing.T) {
false,
},
{
- "When given a keyframe; it should return true",
+ "When given a SPS packetized with STAP-A; it should return true",
[]byte{0x38, 0x00, 0x03, 0x27, 0x90, 0x90, 0x00, 0x05, 0x28, 0x90, 0x90, 0x90, 0x90},
true,
},
+ {
+ "When given a SPS with no packetization; it should return true",
+ []byte{0x27, 0x90, 0x90, 0x00},
+ true,
+ },
}
for _, tt := range tests {
@@ -128,14 +133,12 @@ func TestWriteRTP(t *testing.T) {
if reuseH264Writer != nil {
h264Writer = reuseH264Writer
}
- packet := &rtp.Packet{
- Payload: tt.payload,
- }
-
- err := h264Writer.WriteRTP(packet)
- assert.Equal(t, tt.wantErr, err)
+ assert.Equal(t, tt.wantErr, h264Writer.WriteRTP(&rtp.Packet{
+ Payload: tt.payload,
+ }))
assert.True(t, bytes.Equal(tt.wantBytes, writer.Bytes()))
+
if !tt.reuseWriter {
assert.Nil(t, h264Writer.Close())
reuseWriter = nil
diff --git a/pkg/media/ivfwriter/ivfwriter.go b/pkg/media/ivfwriter/ivfwriter.go
index 3dcd16dab3e..611a6f52e11 100644
--- a/pkg/media/ivfwriter/ivfwriter.go
+++ b/pkg/media/ivfwriter/ivfwriter.go
@@ -76,6 +76,9 @@ func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error {
if i.ioWriter == nil {
return errFileNotOpened
}
+ if len(packet.Payload) == 0 {
+ return nil
+ }
vp8Packet := codecs.VP8Packet{}
if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil {
diff --git a/pkg/media/ivfwriter/ivfwriter_test.go b/pkg/media/ivfwriter/ivfwriter_test.go
index e5403395e0c..5b54e00430b 100644
--- a/pkg/media/ivfwriter/ivfwriter_test.go
+++ b/pkg/media/ivfwriter/ivfwriter_test.go
@@ -20,7 +20,62 @@ type ivfWriterPacketTest struct {
closeErr error
}
-func TestIVFWriter_AddPacketAndClose(t *testing.T) {
+func TestIVFWriter_Basic(t *testing.T) {
+ assert := assert.New(t)
+ addPacketTestCase := []ivfWriterPacketTest{
+ {
+ buffer: &bytes.Buffer{},
+ message: "IVFWriter shouldn't be able to write something to a closed file",
+ messageClose: "IVFWriter should be able to close an already closed file",
+ packet: nil,
+ err: errFileNotOpened,
+ closeErr: nil,
+ },
+ {
+ buffer: &bytes.Buffer{},
+ message: "IVFWriter shouldn't be able to write something an empty packet",
+ messageClose: "IVFWriter should be able to close the file",
+ packet: &rtp.Packet{},
+ err: errInvalidNilPacket,
+ closeErr: nil,
+ },
+ {
+ buffer: nil,
+ message: "IVFWriter shouldn't be able to write something to a closed file",
+ messageClose: "IVFWriter should be able to close an already closed file",
+ packet: nil,
+ err: errFileNotOpened,
+ closeErr: nil,
+ },
+ }
+
+ // First test case has a 'nil' file descriptor
+ writer, err := NewWith(addPacketTestCase[0].buffer)
+ assert.Nil(err, "IVFWriter should be created")
+ assert.NotNil(writer, "Writer shouldn't be nil")
+ assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false")
+ assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
+ err = writer.Close()
+ assert.Nil(err, "IVFWriter should be able to close the stream")
+ writer.ioWriter = nil
+ addPacketTestCase[0].writer = writer
+
+ // Second test tries to write an empty packet
+ writer, err = NewWith(addPacketTestCase[1].buffer)
+ assert.Nil(err, "IVFWriter should be created")
+ assert.NotNil(writer, "Writer shouldn't be nil")
+ assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false")
+ assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
+ addPacketTestCase[1].writer = writer
+
+ // Fourth test tries to write to a nil stream
+ writer, err = NewWith(addPacketTestCase[2].buffer)
+ assert.NotNil(err, "IVFWriter shouldn't be created")
+ assert.Nil(writer, "Writer should be nil")
+ addPacketTestCase[2].writer = writer
+}
+
+func TestIVFWriter_VP8(t *testing.T) {
// Construct valid packet
rawValidPkt := []byte{
0x90, 0xe0, 0x69, 0x8f, 0xd9, 0xc2, 0x93, 0xda, 0x1c, 0x64,
@@ -33,7 +88,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
Extension: true,
ExtensionProfile: 1,
Version: 2,
- PayloadOffset: 20,
PayloadType: 96,
SequenceNumber: 27023,
Timestamp: 3653407706,
@@ -41,7 +95,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
CSRC: []uint32{},
},
Payload: rawValidPkt[20:],
- Raw: rawValidPkt,
}
assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF}))
@@ -57,7 +110,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
Extension: true,
ExtensionProfile: 1,
Version: 2,
- PayloadOffset: 20,
PayloadType: 96,
SequenceNumber: 27023,
Timestamp: 3653407706,
@@ -65,7 +117,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
CSRC: []uint32{},
},
Payload: rawMidPartPkt[20:],
- Raw: rawMidPartPkt,
}
assert.NoError(t, midPartPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF}))
@@ -81,7 +132,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
Extension: true,
ExtensionProfile: 1,
Version: 2,
- PayloadOffset: 20,
PayloadType: 96,
SequenceNumber: 27023,
Timestamp: 3653407706,
@@ -89,7 +139,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
CSRC: []uint32{},
},
Payload: rawKeyframePkt[20:],
- Raw: rawKeyframePkt,
}
assert.NoError(t, keyframePacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF}))
@@ -119,22 +168,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
// The linter misbehave and thinks this code is the same as the tests in oggwriter_test
// nolint:dupl
addPacketTestCase := []ivfWriterPacketTest{
- {
- buffer: &bytes.Buffer{},
- message: "IVFWriter shouldn't be able to write something to a closed file",
- messageClose: "IVFWriter should be able to close an already closed file",
- packet: nil,
- err: errFileNotOpened,
- closeErr: nil,
- },
- {
- buffer: &bytes.Buffer{},
- message: "IVFWriter shouldn't be able to write something an empty packet",
- messageClose: "IVFWriter should be able to close the file",
- packet: &rtp.Packet{},
- err: errInvalidNilPacket,
- closeErr: nil,
- },
{
buffer: &bytes.Buffer{},
message: "IVFWriter should be able to write an IVF packet",
@@ -143,14 +176,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
err: nil,
closeErr: nil,
},
- {
- buffer: nil,
- message: "IVFWriter shouldn't be able to write something to a closed file",
- messageClose: "IVFWriter should be able to close an already closed file",
- packet: nil,
- err: errFileNotOpened,
- closeErr: nil,
- },
{
buffer: &bytes.Buffer{},
message: "IVFWriter should be able to write a Keframe IVF packet",
@@ -161,18 +186,15 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
},
}
- // First test case has a 'nil' file descriptor
+ // first test tries to write a valid VP8 packet
writer, err := NewWith(addPacketTestCase[0].buffer)
assert.Nil(err, "IVFWriter should be created")
assert.NotNil(writer, "Writer shouldn't be nil")
assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false")
assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
- err = writer.Close()
- assert.Nil(err, "IVFWriter should be able to close the stream")
- writer.ioWriter = nil
addPacketTestCase[0].writer = writer
- // Second test tries to write an empty packet
+ // second test tries to write a keyframe packet
writer, err = NewWith(addPacketTestCase[1].buffer)
assert.Nil(err, "IVFWriter should be created")
assert.NotNil(writer, "Writer shouldn't be nil")
@@ -180,28 +202,6 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
addPacketTestCase[1].writer = writer
- // Third test tries to write a valid VP8 packet
- writer, err = NewWith(addPacketTestCase[2].buffer)
- assert.Nil(err, "IVFWriter should be created")
- assert.NotNil(writer, "Writer shouldn't be nil")
- assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false")
- assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
- addPacketTestCase[2].writer = writer
-
- // Fourth test tries to write to a nil stream
- writer, err = NewWith(addPacketTestCase[3].buffer)
- assert.NotNil(err, "IVFWriter shouldn't be created")
- assert.Nil(writer, "Writer should be nil")
- addPacketTestCase[3].writer = writer
-
- // Fifth test tries to write a keyframe packet
- writer, err = NewWith(addPacketTestCase[4].buffer)
- assert.Nil(err, "IVFWriter should be created")
- assert.NotNil(writer, "Writer shouldn't be nil")
- assert.False(writer.seenKeyFrame, "Writer's seenKeyFrame should initialize false")
- assert.Equal(uint64(0), writer.count, "Writer's packet count should initialize 0")
- addPacketTestCase[4].writer = writer
-
for _, t := range addPacketTestCase {
if t.writer != nil {
res := t.writer.WriteRTP(t.packet)
@@ -210,18 +210,18 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
}
// Third test tries to write a valid VP8 packet - No Keyframe
- assert.False(addPacketTestCase[2].writer.seenKeyFrame, "Writer's seenKeyFrame should remain false")
- assert.Equal(uint64(0), addPacketTestCase[2].writer.count, "Writer's packet count should remain 0")
- assert.Equal(nil, addPacketTestCase[2].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet
- assert.Equal(uint64(0), addPacketTestCase[2].writer.count, "Writer's packet count should remain 0")
+ assert.False(addPacketTestCase[0].writer.seenKeyFrame, "Writer's seenKeyFrame should remain false")
+ assert.Equal(uint64(0), addPacketTestCase[0].writer.count, "Writer's packet count should remain 0")
+ assert.Equal(nil, addPacketTestCase[0].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet
+ assert.Equal(uint64(0), addPacketTestCase[0].writer.count, "Writer's packet count should remain 0")
// Fifth test tries to write a keyframe packet
- assert.True(addPacketTestCase[4].writer.seenKeyFrame, "Writer's seenKeyFrame should now be true")
- assert.Equal(uint64(1), addPacketTestCase[4].writer.count, "Writer's packet count should now be 1")
- assert.Equal(nil, addPacketTestCase[4].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet
- assert.Equal(uint64(1), addPacketTestCase[4].writer.count, "Writer's packet count should remain 1")
- assert.Equal(nil, addPacketTestCase[4].writer.WriteRTP(validPacket), "Write packet failed") // add a valid packet
- assert.Equal(uint64(2), addPacketTestCase[4].writer.count, "Writer's packet count should now be 2")
+ assert.True(addPacketTestCase[1].writer.seenKeyFrame, "Writer's seenKeyFrame should now be true")
+ assert.Equal(uint64(1), addPacketTestCase[1].writer.count, "Writer's packet count should now be 1")
+ assert.Equal(nil, addPacketTestCase[1].writer.WriteRTP(midPartPacket), "Write packet failed") // add a mid partition packet
+ assert.Equal(uint64(1), addPacketTestCase[1].writer.count, "Writer's packet count should remain 1")
+ assert.Equal(nil, addPacketTestCase[1].writer.WriteRTP(validPacket), "Write packet failed") // add a valid packet
+ assert.Equal(uint64(2), addPacketTestCase[1].writer.count, "Writer's packet count should now be 2")
for _, t := range addPacketTestCase {
if t.writer != nil {
@@ -230,3 +230,12 @@ func TestIVFWriter_AddPacketAndClose(t *testing.T) {
}
}
}
+
+func TestIVFWriter_EmptyPayload(t *testing.T) {
+ buffer := &bytes.Buffer{}
+
+ writer, err := NewWith(buffer)
+ assert.NoError(t, err)
+
+ assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{}}))
+}
diff --git a/pkg/media/oggreader/oggreader.go b/pkg/media/oggreader/oggreader.go
index 6a7005de365..cd14a01ae7b 100644
--- a/pkg/media/oggreader/oggreader.go
+++ b/pkg/media/oggreader/oggreader.go
@@ -29,7 +29,7 @@ var (
// OggReader is used to read Ogg files and return page payloads
type OggReader struct {
- stream io.ReadSeeker
+ stream io.Reader
bytesReadSuccesfully int64
checksumTable *[256]uint32
doChecksum bool
@@ -64,12 +64,12 @@ type OggPageHeader struct {
}
// NewWith returns a new Ogg reader and Ogg header
-// with an io.ReadSeeker input
-func NewWith(in io.ReadSeeker) (*OggReader, *OggHeader, error) {
+// with an io.Reader input
+func NewWith(in io.Reader) (*OggReader, *OggHeader, error) {
return newWith(in /* doChecksum */, true)
}
-func newWith(in io.ReadSeeker, doChecksum bool) (*OggReader, *OggHeader, error) {
+func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) {
if in == nil {
return nil, nil, errNilStream
}
@@ -192,7 +192,7 @@ func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) {
// 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) {
+func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) {
o.stream = reset(o.bytesReadSuccesfully)
}
diff --git a/pkg/media/oggwriter/oggwriter.go b/pkg/media/oggwriter/oggwriter.go
index e20492b7a83..54c1ce464ab 100644
--- a/pkg/media/oggwriter/oggwriter.go
+++ b/pkg/media/oggwriter/oggwriter.go
@@ -172,6 +172,9 @@ func (i *OggWriter) WriteRTP(packet *rtp.Packet) error {
if packet == nil {
return errInvalidNilPacket
}
+ if len(packet.Payload) == 0 {
+ return nil
+ }
opusPacket := codecs.OpusPacket{}
if _, err := opusPacket.Unmarshal(packet.Payload); err != nil {
diff --git a/pkg/media/oggwriter/oggwriter_test.go b/pkg/media/oggwriter/oggwriter_test.go
index 74f75992c52..23c4d004fb6 100644
--- a/pkg/media/oggwriter/oggwriter_test.go
+++ b/pkg/media/oggwriter/oggwriter_test.go
@@ -31,7 +31,6 @@ func TestOggWriter_AddPacketAndClose(t *testing.T) {
Extension: true,
ExtensionProfile: 1,
Version: 2,
- PayloadOffset: 20,
PayloadType: 111,
SequenceNumber: 27023,
Timestamp: 3653407706,
@@ -39,7 +38,6 @@ func TestOggWriter_AddPacketAndClose(t *testing.T) {
CSRC: []uint32{},
},
Payload: rawPkt[20:],
- Raw: rawPkt,
}
assert.NoError(t, validPacket.SetExtension(0, []byte{0xFF, 0xFF, 0xFF, 0xFF}))
@@ -58,9 +56,9 @@ func TestOggWriter_AddPacketAndClose(t *testing.T) {
},
{
buffer: &bytes.Buffer{},
- message: "OggWriter shouldn't be able to write an empty packet",
+ message: "OggWriter shouldn't be able to write a nil packet",
messageClose: "OggWriter should be able to close the file",
- packet: &rtp.Packet{},
+ packet: nil,
err: errInvalidNilPacket,
closeErr: nil,
},
@@ -123,3 +121,12 @@ func TestOggWriter_AddPacketAndClose(t *testing.T) {
}
}
}
+
+func TestOggWriter_EmptyPayload(t *testing.T) {
+ buffer := &bytes.Buffer{}
+
+ writer, err := NewWith(buffer, 48000, 2)
+ assert.NoError(t, err)
+
+ assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{}}))
+}
diff --git a/pkg/media/samplebuilder/sampleSequenceLocation.go b/pkg/media/samplebuilder/sampleSequenceLocation.go
index 56b9b970c5c..9b2930e328c 100644
--- a/pkg/media/samplebuilder/sampleSequenceLocation.go
+++ b/pkg/media/samplebuilder/sampleSequenceLocation.go
@@ -1,8 +1,6 @@
// Package samplebuilder provides functionality to reconstruct media frames from RTP packets.
package samplebuilder
-import "math"
-
type sampleSequenceLocation struct {
// head is the first packet in a sequence
head uint16
@@ -30,53 +28,23 @@ const (
slCompareAfter
)
-func minUint32(x, y uint32) uint32 {
- if x < y {
- return x
- }
- return y
-}
-
-// Distance between two seqnums
-func seqnumDistance32(x, y uint32) uint32 {
- diff := int32(x - y)
- if diff < 0 {
- return uint32(-diff)
- }
-
- return uint32(diff)
-}
-
func (l sampleSequenceLocation) compare(pos uint16) int {
- if l.empty() {
+ if l.head == l.tail {
return slCompareVoid
}
- head32 := uint32(l.head)
- count32 := uint32(l.count())
- tail32 := head32 + count32
-
- // pos32 is possibly two values, the normal value or a wrap
- // around the start value, figure out which it is...
-
- pos32Normal := uint32(pos)
- pos32Wrap := uint32(pos) + math.MaxUint16 + 1
-
- distNormal := minUint32(seqnumDistance32(head32, pos32Normal), seqnumDistance32(tail32, pos32Normal))
- distWrap := minUint32(seqnumDistance32(head32, pos32Wrap), seqnumDistance32(tail32, pos32Wrap))
-
- pos32 := pos32Normal
- if distWrap < distNormal {
- pos32 = pos32Wrap
+ if l.head < l.tail {
+ if l.head <= pos && pos < l.tail {
+ return slCompareInside
+ }
+ } else {
+ if l.head <= pos || pos < l.tail {
+ return slCompareInside
+ }
}
- if pos32 < head32 {
+ if l.head-pos <= pos-l.tail {
return slCompareBefore
}
-
- if pos32 >= tail32 {
- return slCompareAfter
- }
-
- return slCompareInside
+ return slCompareAfter
}
diff --git a/pkg/media/samplebuilder/sampleSequenceLocation_test.go b/pkg/media/samplebuilder/sampleSequenceLocation_test.go
new file mode 100644
index 00000000000..0cfa2469519
--- /dev/null
+++ b/pkg/media/samplebuilder/sampleSequenceLocation_test.go
@@ -0,0 +1,26 @@
+package samplebuilder
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSampleSequenceLocationCompare(t *testing.T) {
+ s1 := sampleSequenceLocation{32, 42}
+ assert.Equal(t, slCompareBefore, s1.compare(16))
+ assert.Equal(t, slCompareInside, s1.compare(32))
+ assert.Equal(t, slCompareInside, s1.compare(38))
+ assert.Equal(t, slCompareInside, s1.compare(41))
+ assert.Equal(t, slCompareAfter, s1.compare(42))
+ assert.Equal(t, slCompareAfter, s1.compare(0x57))
+
+ s2 := sampleSequenceLocation{0xffa0, 32}
+ assert.Equal(t, slCompareBefore, s2.compare(0xff00))
+ assert.Equal(t, slCompareInside, s2.compare(0xffa0))
+ assert.Equal(t, slCompareInside, s2.compare(0xffff))
+ assert.Equal(t, slCompareInside, s2.compare(0))
+ assert.Equal(t, slCompareInside, s2.compare(31))
+ assert.Equal(t, slCompareAfter, s2.compare(32))
+ assert.Equal(t, slCompareAfter, s2.compare(128))
+}
diff --git a/pkg/media/samplebuilder/samplebuilder.go b/pkg/media/samplebuilder/samplebuilder.go
index ac3303962d9..e928f14314d 100644
--- a/pkg/media/samplebuilder/samplebuilder.go
+++ b/pkg/media/samplebuilder/samplebuilder.go
@@ -22,9 +22,6 @@ type SampleBuilder struct {
// sampleRate allows us to compute duration of media.SamplecA
sampleRate uint32
- // Interface that checks whether the packet is the first fragment of the frame or not
- partitionHeadChecker rtp.PartitionHeadChecker
-
// the handler to be called when the builder is about to remove the
// reference to some packet.
packetReleaseHandler func(*rtp.Packet)
@@ -203,8 +200,8 @@ func (s *SampleBuilder) buildSample(purgingBuffers bool) *media.Sample {
var consume sampleSequenceLocation
- for i := s.active.head; s.buffer[i] != nil && i < s.active.tail; i++ {
- if s.depacketizer.IsDetectedFinalPacketInSequence(s.buffer[i].Marker) {
+ for i := s.active.head; s.buffer[i] != nil && s.active.compare(i) != slCompareAfter; i++ {
+ if s.depacketizer.IsPartitionTail(s.buffer[i].Marker, s.buffer[i].Payload) {
consume.head = s.active.head
consume.tail = i + 1
break
@@ -244,13 +241,11 @@ func (s *SampleBuilder) buildSample(purgingBuffers bool) *media.Sample {
// prior to decoding all the packets, check if this packet
// would end being disposed anyway
- if s.partitionHeadChecker != nil {
- if !s.partitionHeadChecker.IsPartitionHead(s.buffer[consume.head].Payload) {
- s.droppedPackets += consume.count()
- s.purgeConsumedLocation(consume, true)
- s.purgeConsumedBuffers()
- return nil
- }
+ if !s.depacketizer.IsPartitionHead(s.buffer[consume.head].Payload) {
+ s.droppedPackets += consume.count()
+ s.purgeConsumedLocation(consume, true)
+ s.purgeConsumedBuffers()
+ return nil
}
// merge all the buffers into a sample
@@ -329,11 +324,9 @@ func timestampDistance(x, y uint32) uint32 {
// An Option configures a SampleBuilder.
type Option func(o *SampleBuilder)
-// WithPartitionHeadChecker assigns a codec-specific PartitionHeadChecker to SampleBuilder.
-// Several PartitionHeadCheckers are available in package github.com/pion/rtp/codecs.
-func WithPartitionHeadChecker(checker rtp.PartitionHeadChecker) Option {
+// WithPartitionHeadChecker is obsolete, it does nothing.
+func WithPartitionHeadChecker(checker interface{}) Option {
return func(o *SampleBuilder) {
- o.partitionHeadChecker = checker
}
}
diff --git a/pkg/media/samplebuilder/samplebuilder_test.go b/pkg/media/samplebuilder/samplebuilder_test.go
index a5a918a5416..be4dfbd8689 100644
--- a/pkg/media/samplebuilder/samplebuilder_test.go
+++ b/pkg/media/samplebuilder/samplebuilder_test.go
@@ -20,21 +20,21 @@ type sampleBuilderTest struct {
maxLateTimestamp uint32
}
-type fakeDepacketizer struct{}
+type fakeDepacketizer struct {
+ headChecker bool
+ headBytes []byte
+}
func (f *fakeDepacketizer) Unmarshal(r []byte) ([]byte, error) {
return r, nil
}
-func (f *fakeDepacketizer) IsDetectedFinalPacketInSequence(rtpPacketMarketBit bool) bool {
- return rtpPacketMarketBit
-}
-
-type fakePartitionHeadChecker struct {
- headBytes []byte
-}
-
-func (f *fakePartitionHeadChecker) IsPartitionHead(payload []byte) bool {
+func (f *fakeDepacketizer) IsPartitionHead(payload []byte) bool {
+ if !f.headChecker {
+ // simulates a bug in the 3.0 version
+ // the tests should be fixed to not assume the bug
+ return true
+ }
for _, b := range f.headBytes {
if payload[0] == b {
return true
@@ -43,6 +43,10 @@ func (f *fakePartitionHeadChecker) IsPartitionHead(payload []byte) bool {
return false
}
+func (f *fakeDepacketizer) IsPartitionTail(marker bool, payload []byte) bool {
+ return marker
+}
+
func TestSampleBuilder(t *testing.T) {
testData := []sampleBuilderTest{
{
@@ -226,18 +230,17 @@ func TestSampleBuilder(t *testing.T) {
for _, t := range testData {
var opts []Option
- if t.withHeadChecker {
- opts = append(opts, WithPartitionHeadChecker(
- &fakePartitionHeadChecker{headBytes: t.headBytes},
- ))
- }
if t.maxLateTimestamp != 0 {
opts = append(opts, WithMaxTimeDelay(
time.Millisecond*time.Duration(int64(t.maxLateTimestamp)),
))
}
- s := New(t.maxLate, &fakeDepacketizer{}, 1, opts...)
+ d := &fakeDepacketizer{
+ headChecker: t.withHeadChecker,
+ headBytes: t.headBytes,
+ }
+ s := New(t.maxLate, d, 1, opts...)
samples := []*media.Sample{}
for _, p := range t.packets {
@@ -333,9 +336,12 @@ func TestSampleBuilderPushMaxZero(t *testing.T) {
pkts := []rtp.Packet{
{Header: rtp.Header{SequenceNumber: 0, Timestamp: 0, Marker: true}, Payload: []byte{0x01}},
}
- s := New(0, &fakeDepacketizer{}, 1, WithPartitionHeadChecker(
- &fakePartitionHeadChecker{headBytes: []byte{0x01}},
- ))
+ d := &fakeDepacketizer{
+ headChecker: true,
+ headBytes: []byte{0x01},
+ }
+
+ s := New(0, d, 1)
s.Push(&pkts[0])
if sample := s.Pop(); sample == nil {
t.Error("Should expect a popped sample")
@@ -388,3 +394,174 @@ func TestPopWithTimestamp(t *testing.T) {
assert.Equal(t, uint32(0), timestamp)
})
}
+
+type truePartitionHeadChecker struct{}
+
+func (f *truePartitionHeadChecker) IsPartitionHead(payload []byte) bool {
+ return true
+}
+
+func TestSampleBuilderData(t *testing.T) {
+ s := New(10, &fakeDepacketizer{}, 1,
+ WithPartitionHeadChecker(&truePartitionHeadChecker{}),
+ )
+ j := 0
+ for i := 0; i < 0x20000; i++ {
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i),
+ Timestamp: uint32(i + 42),
+ },
+ Payload: []byte{byte(i)},
+ }
+ s.Push(&p)
+ for {
+ sample, ts := s.PopWithTimestamp()
+ if sample == nil {
+ break
+ }
+ assert.Equal(t, ts, uint32(j+42), "timestamp")
+ assert.Equal(t, len(sample.Data), 1, "data length")
+ assert.Equal(t, byte(j), sample.Data[0], "data")
+ j++
+ }
+ }
+ // only the last packet should be dropped
+ assert.Equal(t, j, 0x1FFFF)
+}
+
+func BenchmarkSampleBuilderSequential(b *testing.B) {
+ s := New(100, &fakeDepacketizer{}, 1)
+ b.ResetTimer()
+ j := 0
+ for i := 0; i < b.N; i++ {
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i),
+ Timestamp: uint32(i + 42),
+ },
+ Payload: make([]byte, 50),
+ }
+ s.Push(&p)
+ for {
+ s := s.Pop()
+ if s == nil {
+ break
+ }
+ j++
+ }
+ }
+ if b.N > 200 && j < b.N-100 {
+ b.Errorf("Got %v (N=%v)", j, b.N)
+ }
+}
+
+func BenchmarkSampleBuilderLoss(b *testing.B) {
+ s := New(100, &fakeDepacketizer{}, 1)
+ b.ResetTimer()
+ j := 0
+ for i := 0; i < b.N; i++ {
+ if i%13 == 0 {
+ continue
+ }
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i),
+ Timestamp: uint32(i + 42),
+ },
+ Payload: make([]byte, 50),
+ }
+ s.Push(&p)
+ for {
+ s := s.Pop()
+ if s == nil {
+ break
+ }
+ j++
+ }
+ }
+ if b.N > 200 && j < b.N/2-100 {
+ b.Errorf("Got %v (N=%v)", j, b.N)
+ }
+}
+
+func BenchmarkSampleBuilderReordered(b *testing.B) {
+ s := New(100, &fakeDepacketizer{}, 1)
+ b.ResetTimer()
+ j := 0
+ for i := 0; i < b.N; i++ {
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i ^ 3),
+ Timestamp: uint32((i ^ 3) + 42),
+ },
+ Payload: make([]byte, 50),
+ }
+ s.Push(&p)
+ for {
+ s := s.Pop()
+ if s == nil {
+ break
+ }
+ j++
+ }
+ }
+ if b.N > 2 && j < b.N-5 && j > b.N {
+ b.Errorf("Got %v (N=%v)", j, b.N)
+ }
+}
+
+func BenchmarkSampleBuilderFragmented(b *testing.B) {
+ s := New(100, &fakeDepacketizer{}, 1)
+ b.ResetTimer()
+ j := 0
+ for i := 0; i < b.N; i++ {
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i),
+ Timestamp: uint32(i/2 + 42),
+ },
+ Payload: make([]byte, 50),
+ }
+ s.Push(&p)
+ for {
+ s := s.Pop()
+ if s == nil {
+ break
+ }
+ j++
+ }
+ }
+ if b.N > 200 && j < b.N/2-100 {
+ b.Errorf("Got %v (N=%v)", j, b.N)
+ }
+}
+
+func BenchmarkSampleBuilderFragmentedLoss(b *testing.B) {
+ s := New(100, &fakeDepacketizer{}, 1)
+ b.ResetTimer()
+ j := 0
+ for i := 0; i < b.N; i++ {
+ if i%13 == 0 {
+ continue
+ }
+ p := rtp.Packet{
+ Header: rtp.Header{
+ SequenceNumber: uint16(i),
+ Timestamp: uint32(i/2 + 42),
+ },
+ Payload: make([]byte, 50),
+ }
+ s.Push(&p)
+ for {
+ s := s.Pop()
+ if s == nil {
+ break
+ }
+ j++
+ }
+ }
+ if b.N > 200 && j < b.N/3-100 {
+ b.Errorf("Got %v (N=%v)", j, b.N)
+ }
+}
diff --git a/renovate.json b/renovate.json
index f84608c5136..f1614058a70 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,6 +1,7 @@
{
"extends": [
- "config:base"
+ "config:base",
+ ":disableDependencyDashboard"
],
"postUpdateOptions": [
"gomodTidy"
@@ -15,5 +16,12 @@
"packagePatterns": ["^golang.org/x/"],
"schedule": ["on the first day of the month"]
}
+ ],
+ "ignorePaths": [
+ ".github/workflows/generate-authors.yml",
+ ".github/workflows/lint.yaml",
+ ".github/workflows/renovate-go-mod-fix.yaml",
+ ".github/workflows/test.yaml",
+ ".github/workflows/tidy-check.yaml"
]
}
diff --git a/rtpcodec.go b/rtpcodec.go
index 30a0cd908f4..cde2b8e7183 100644
--- a/rtpcodec.go
+++ b/rtpcodec.go
@@ -2,6 +2,8 @@ package webrtc
import (
"strings"
+
+ "github.com/pion/webrtc/v3/internal/fmtp"
)
// RTPCodecType determines the type of a codec
@@ -97,12 +99,12 @@ const (
// Used for lookup up a codec in an existing list to find a match
// Returns codecMatchExact, codecMatchPartial, or codecMatchNone
func codecParametersFuzzySearch(needle RTPCodecParameters, haystack []RTPCodecParameters) (RTPCodecParameters, codecMatchType) {
- needleFmtp := parseFmtp(needle.RTPCodecCapability.SDPFmtpLine)
+ needleFmtp := fmtp.Parse(needle.RTPCodecCapability.MimeType, needle.RTPCodecCapability.SDPFmtpLine)
// First attempt to match on MimeType + SDPFmtpLine
for _, c := range haystack {
- if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) &&
- fmtpConsist(needleFmtp, parseFmtp(c.RTPCodecCapability.SDPFmtpLine)) {
+ cfmtp := fmtp.Parse(c.RTPCodecCapability.MimeType, c.RTPCodecCapability.SDPFmtpLine)
+ if needleFmtp.Match(cfmtp) {
return c, codecMatchExact
}
}
diff --git a/rtpcodingparameters.go b/rtpcodingparameters.go
index 8a08c332165..c5e12efaf21 100644
--- a/rtpcodingparameters.go
+++ b/rtpcodingparameters.go
@@ -1,10 +1,17 @@
package webrtc
+// RTPRtxParameters dictionary contains information relating to retransmission (RTX) settings.
+// https://draft.ortc.org/#dom-rtcrtprtxparameters
+type RTPRtxParameters struct {
+ SSRC SSRC `json:"ssrc"`
+}
+
// 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"`
+ RID string `json:"rid"`
+ SSRC SSRC `json:"ssrc"`
+ PayloadType PayloadType `json:"payloadType"`
+ RTX RTPRtxParameters `json:"rtx"`
}
diff --git a/rtpreceiver.go b/rtpreceiver.go
index 446e8ae9f06..a836b2ff81c 100644
--- a/rtpreceiver.go
+++ b/rtpreceiver.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -15,17 +16,23 @@ import (
)
// trackStreams maintains a mapping of RTP/RTCP streams to a specific track
-// a RTPReceiver may contain multiple streams if we are dealing with Multicast
+// a RTPReceiver may contain multiple streams if we are dealing with Simulcast
type trackStreams struct {
track *TrackRemote
- streamInfo interceptor.StreamInfo
+ streamInfo, repairStreamInfo *interceptor.StreamInfo
rtpReadStream *srtp.ReadStreamSRTP
rtpInterceptor interceptor.RTPReader
rtcpReadStream *srtp.ReadStreamSRTCP
rtcpInterceptor interceptor.RTCPReader
+
+ repairReadStream *srtp.ReadStreamSRTP
+ repairInterceptor interceptor.RTPReader
+
+ repairRtcpReadStream *srtp.ReadStreamSRTCP
+ repairRtcpInterceptor interceptor.RTCPReader
}
// RTPReceiver allows an application to inspect the receipt of a TrackRemote
@@ -38,6 +45,8 @@ type RTPReceiver struct {
closed, received chan interface{}
mu sync.RWMutex
+ tr *RTPTransceiver
+
// A reference to the associated api object
api *API
}
@@ -60,6 +69,12 @@ func (api *API) NewRTPReceiver(kind RTPCodecType, transport *DTLSTransport) (*RT
return r, nil
}
+func (r *RTPReceiver) setRTPTransceiver(tr *RTPTransceiver) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.tr = tr
+}
+
// Transport returns the currently-configured *DTLSTransport or nil
// if one has not yet been configured
func (r *RTPReceiver) Transport() *DTLSTransport {
@@ -68,10 +83,20 @@ func (r *RTPReceiver) Transport() *DTLSTransport {
return r.transport
}
+func (r *RTPReceiver) getParameters() RTPParameters {
+ parameters := r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+ if r.tr != nil {
+ parameters.Codecs = r.tr.getCodecs()
+ }
+ return parameters
+}
+
// 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})
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ return r.getParameters()
}
// Track returns the RtpTransceiver TrackRemote
@@ -98,8 +123,27 @@ func (r *RTPReceiver) Tracks() []*TrackRemote {
return tracks
}
-// Receive initialize the track and starts all the transports
-func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
+// configureReceive initialize the track
+func (r *RTPReceiver) configureReceive(parameters RTPReceiveParameters) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ for i := range parameters.Encodings {
+ t := trackStreams{
+ track: newTrackRemote(
+ r.kind,
+ parameters.Encodings[i].SSRC,
+ parameters.Encodings[i].RID,
+ r,
+ ),
+ }
+
+ r.tracks = append(r.tracks, t)
+ }
+}
+
+// startReceive starts all the transports
+func (r *RTPReceiver) startReceive(parameters RTPReceiveParameters) error {
r.mu.Lock()
defer r.mu.Unlock()
select {
@@ -109,45 +153,59 @@ func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
}
defer close(r.received)
- if len(parameters.Encodings) == 1 && parameters.Encodings[0].SSRC != 0 {
- t := trackStreams{
- track: newTrackRemote(
- r.kind,
- parameters.Encodings[0].SSRC,
- parameters.Encodings[0].RID,
- r,
- ),
+ globalParams := r.getParameters()
+ codec := RTPCodecCapability{}
+ if len(globalParams.Codecs) != 0 {
+ codec = globalParams.Codecs[0].RTPCodecCapability
+ }
+
+ for i := range parameters.Encodings {
+ if parameters.Encodings[i].RID != "" {
+ // RID based tracks will be set up in receiveForRid
+ continue
}
- globalParams := r.GetParameters()
- codec := RTPCodecCapability{}
- if len(globalParams.Codecs) != 0 {
- codec = globalParams.Codecs[0].RTPCodecCapability
+ var t *trackStreams
+ for idx, ts := range r.tracks {
+ if ts.track != nil && parameters.Encodings[i].SSRC != 0 && ts.track.SSRC() == parameters.Encodings[i].SSRC {
+ t = &r.tracks[idx]
+ break
+ }
+ }
+ if t == nil {
+ return fmt.Errorf("%w: %d", errRTPReceiverWithSSRCTrackStreamNotFound, parameters.Encodings[i].SSRC)
}
- t.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, t.streamInfo); err != nil {
- return err
+ if parameters.Encodings[i].SSRC != 0 {
+ t.streamInfo = createStreamInfo("", parameters.Encodings[i].SSRC, 0, codec, globalParams.HeaderExtensions)
+ var err error
+ if t.rtpReadStream, t.rtpInterceptor, t.rtcpReadStream, t.rtcpInterceptor, err = r.transport.streamsForSSRC(parameters.Encodings[i].SSRC, *t.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,
- ),
- })
+ if rtxSsrc := parameters.Encodings[i].RTX.SSRC; rtxSsrc != 0 {
+ streamInfo := createStreamInfo("", rtxSsrc, 0, codec, globalParams.HeaderExtensions)
+ rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor, err := r.transport.streamsForSSRC(rtxSsrc, *streamInfo)
+ if err != nil {
+ return err
+ }
+
+ if err = r.receiveForRtx(rtxSsrc, "", streamInfo, rtpReadStream, rtpInterceptor, rtcpReadStream, rtcpInterceptor); err != nil {
+ return err
+ }
}
}
return nil
}
+// Receive initialize the track and starts all the transports
+func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
+ r.configureReceive(parameters)
+ return r.startReceive(parameters)
+}
+
// Read reads incoming RTCP for this RTPReceiver
func (r *RTPReceiver) Read(b []byte) (n int, a interceptor.Attributes, err error) {
select {
@@ -176,7 +234,7 @@ func (r *RTPReceiver) ReadSimulcast(b []byte, rid string) (n int, a interceptor.
// 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)
+ b := make([]byte, r.api.settingEngine.getReceiveMTU())
i, attributes, err := r.Read(b)
if err != nil {
return nil, nil, err
@@ -192,7 +250,7 @@ func (r *RTPReceiver) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error)
// 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)
+ b := make([]byte, r.api.settingEngine.getReceiveMTU())
i, attributes, err := r.ReadSimulcast(b, rid)
if err != nil {
return nil, nil, err
@@ -236,8 +294,23 @@ func (r *RTPReceiver) Stop() error {
errs = append(errs, r.tracks[i].rtpReadStream.Close())
}
+ if r.tracks[i].repairReadStream != nil {
+ errs = append(errs, r.tracks[i].repairReadStream.Close())
+ }
+
+ if r.tracks[i].repairRtcpReadStream != nil {
+ errs = append(errs, r.tracks[i].repairRtcpReadStream.Close())
+ }
+
+ if r.tracks[i].streamInfo != nil {
+ r.api.interceptor.UnbindRemoteStream(r.tracks[i].streamInfo)
+ }
+
+ if r.tracks[i].repairStreamInfo != nil {
+ r.api.interceptor.UnbindRemoteStream(r.tracks[i].repairStreamInfo)
+ }
+
err = util.FlattenErrs(errs)
- r.api.interceptor.UnbindRemoteStream(&r.tracks[i].streamInfo)
}
default:
}
@@ -267,7 +340,7 @@ func (r *RTPReceiver) readRTP(b []byte, reader *TrackRemote) (n int, a intercept
// 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) {
+func (r *RTPReceiver) receiveForRid(rid string, params RTPParameters, streamInfo *interceptor.StreamInfo, rtpReadStream *srtp.ReadStreamSRTP, rtpInterceptor interceptor.RTPReader, rtcpReadStream *srtp.ReadStreamSRTCP, rtcpInterceptor interceptor.RTCPReader) (*TrackRemote, error) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -277,54 +350,56 @@ func (r *RTPReceiver) receiveForRid(rid string, params RTPParameters, ssrc SSRC)
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
- r.tracks[i].streamInfo = createStreamInfo("", ssrc, params.Codecs[0].PayloadType, params.Codecs[0].RTPCodecCapability, params.HeaderExtensions)
+ r.tracks[i].track.ssrc = SSRC(streamInfo.SSRC)
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, r.tracks[i].streamInfo); err != nil {
- return nil, err
- }
+ r.tracks[i].streamInfo = streamInfo
+ r.tracks[i].rtpReadStream = rtpReadStream
+ r.tracks[i].rtpInterceptor = rtpInterceptor
+ r.tracks[i].rtcpReadStream = rtcpReadStream
+ r.tracks[i].rtcpInterceptor = rtcpInterceptor
return r.tracks[i].track, nil
}
}
- return nil, fmt.Errorf("%w: %d", errRTPReceiverForSSRCTrackStreamNotFound, ssrc)
+ return nil, fmt.Errorf("%w: %s", errRTPReceiverForRIDTrackStreamNotFound, rid)
}
-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
+// receiveForRtx starts a routine that processes the repair stream
+// These packets aren't exposed to the user yet, but we need to process them for
+// TWCC
+func (r *RTPReceiver) receiveForRtx(ssrc SSRC, rsid string, streamInfo *interceptor.StreamInfo, rtpReadStream *srtp.ReadStreamSRTP, rtpInterceptor interceptor.RTPReader, rtcpReadStream *srtp.ReadStreamSRTCP, rtcpInterceptor interceptor.RTCPReader) error {
+ var track *trackStreams
+ if ssrc != 0 && len(r.tracks) == 1 {
+ track = &r.tracks[0]
+ } else {
+ for i := range r.tracks {
+ if r.tracks[i].track.RID() == rsid {
+ track = &r.tracks[i]
+ }
+ }
}
- rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(ssrc))
- if err != nil {
- return nil, nil, nil, nil, err
+ if track == nil {
+ return fmt.Errorf("%w: ssrc(%d) rsid(%s)", errRTPReceiverForRIDTrackStreamNotFound, ssrc, rsid)
}
- 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
+ track.repairStreamInfo = streamInfo
+ track.repairReadStream = rtpReadStream
+ track.repairInterceptor = rtpInterceptor
+ track.repairRtcpReadStream = rtcpReadStream
+ track.repairRtcpInterceptor = rtcpInterceptor
+
+ go func() {
+ b := make([]byte, r.api.settingEngine.getReceiveMTU())
+ for {
+ if _, _, readErr := track.repairInterceptor.Read(b, nil); readErr != nil {
+ return
+ }
+ }
+ }()
+ return nil
}
// SetReadDeadline sets the max amount of time the RTCP stream will block before returning. 0 is forever.
diff --git a/rtpreceiver_go.go b/rtpreceiver_go.go
index 7f347e4b3b6..430169548f6 100644
--- a/rtpreceiver_go.go
+++ b/rtpreceiver_go.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/rtpreceiver_go_test.go b/rtpreceiver_go_test.go
index 106cca2f3b6..50d9b29ac59 100644
--- a/rtpreceiver_go_test.go
+++ b/rtpreceiver_go_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -14,7 +15,7 @@ import (
func TestSetRTPParameters(t *testing.T) {
sender, receiver, wan := createVNetPair(t)
- outgoingTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ outgoingTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = sender.AddTrack(outgoingTrack)
diff --git a/rtpreceiver_js.go b/rtpreceiver_js.go
index bee5d9a4e51..866757fb8fe 100644
--- a/rtpreceiver_js.go
+++ b/rtpreceiver_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/rtpreceiver_test.go b/rtpreceiver_test.go
index 56bca12b127..858ddf1af3b 100644
--- a/rtpreceiver_test.go
+++ b/rtpreceiver_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -7,7 +8,6 @@ import (
"testing"
"time"
- "github.com/pion/transport/packetio"
"github.com/pion/transport/test"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/stretchr/testify/assert"
@@ -24,7 +24,7 @@ func Test_RTPReceiver_SetReadDeadline(t *testing.T) {
sender, receiver, wan := createVNetPair(t)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = sender.AddTrack(track)
@@ -41,10 +41,10 @@ func Test_RTPReceiver_SetReadDeadline(t *testing.T) {
assert.NoError(t, readErr)
_, _, readErr = trackRemote.ReadRTP()
- assert.Error(t, readErr, packetio.ErrTimeout)
+ assert.Error(t, readErr)
_, _, readErr = r.ReadRTCP()
- assert.Error(t, readErr, packetio.ErrTimeout)
+ assert.Error(t, readErr)
seenPacketCancel()
})
diff --git a/rtpsender.go b/rtpsender.go
index b9ba82ec25c..0de09625e6b 100644
--- a/rtpsender.go
+++ b/rtpsender.go
@@ -1,8 +1,10 @@
+//go:build !js
// +build !js
package webrtc
import (
+ "fmt"
"io"
"sync"
"time"
@@ -11,22 +13,30 @@ import (
"github.com/pion/randutil"
"github.com/pion/rtcp"
"github.com/pion/rtp"
+ "github.com/pion/webrtc/v3/internal/util"
)
-// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer
-type RTPSender struct {
+type trackEncoding struct {
track TrackLocal
- srtpStream *srtpWriterFuture
+ srtpStream *srtpWriterFuture
+
rtcpInterceptor interceptor.RTCPReader
streamInfo interceptor.StreamInfo
context TrackLocalContext
+ ssrc SSRC
+}
+
+// RTPSender allows an application to control how a given Track is encoded and transmitted to a remote peer
+type RTPSender struct {
+ trackEncodings []*trackEncoding
+
transport *DTLSTransport
payloadType PayloadType
- ssrc SSRC
+ kind RTPCodecType
// nolint:godox
// TODO(sgotti) remove this when in future we'll avoid replacing
@@ -38,6 +48,8 @@ type RTPSender struct {
api *API
id string
+ rtpTransceiver *RTPTransceiver
+
mu sync.RWMutex
sendCalled, stopCalled chan struct{}
}
@@ -56,22 +68,15 @@ func (api *API) NewRTPSender(track TrackLocal, transport *DTLSTransport) (*RTPSe
}
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{},
+ kind: track.Kind(),
}
- 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
- }))
+ r.addEncoding(track)
return r, nil
}
@@ -88,6 +93,12 @@ func (r *RTPSender) setNegotiated() {
r.negotiated = true
}
+func (r *RTPSender) setRTPTransceiver(rtpTransceiver *RTPTransceiver) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.rtpTransceiver = rtpTransceiver
+}
+
// Transport returns the currently-configured *DTLSTransport or nil
// if one has not yet been configured
func (r *RTPSender) Transport() *DTLSTransport {
@@ -96,30 +107,119 @@ func (r *RTPSender) Transport() *DTLSTransport {
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{
+func (r *RTPSender) getParameters() RTPSendParameters {
+ var encodings []RTPEncodingParameters
+ for _, trackEncoding := range r.trackEncodings {
+ var rid string
+ if trackEncoding.track != nil {
+ rid = trackEncoding.track.RID()
+ }
+ encodings = append(encodings, RTPEncodingParameters{
+ RTPCodingParameters: RTPCodingParameters{
+ RID: rid,
+ SSRC: trackEncoding.ssrc,
+ PayloadType: r.payloadType,
+ },
+ })
+ }
+ sendParameters := RTPSendParameters{
RTPParameters: r.api.mediaEngine.getRTPParametersByKind(
- r.track.Kind(),
+ r.kind,
[]RTPTransceiverDirection{RTPTransceiverDirectionSendonly},
),
- Encodings: []RTPEncodingParameters{
- {
- RTPCodingParameters: RTPCodingParameters{
- SSRC: r.ssrc,
- PayloadType: r.payloadType,
- },
- },
- },
+ Encodings: encodings,
+ }
+ if r.rtpTransceiver != nil {
+ sendParameters.Codecs = r.rtpTransceiver.getCodecs()
+ } else {
+ sendParameters.Codecs = r.api.mediaEngine.getCodecsByKind(r.kind)
+ }
+ return sendParameters
+}
+
+// GetParameters describes the current configuration for the encoding and
+// transmission of media on the sender's track.
+func (r *RTPSender) GetParameters() RTPSendParameters {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ return r.getParameters()
+}
+
+// AddEncoding adds an encoding to RTPSender. Used by simulcast senders.
+func (r *RTPSender) AddEncoding(track TrackLocal) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if track == nil {
+ return errRTPSenderTrackNil
+ }
+
+ if track.RID() == "" {
+ return errRTPSenderRidNil
+ }
+
+ if r.hasStopped() {
+ return errRTPSenderStopped
+ }
+
+ if r.hasSent() {
+ return errRTPSenderSendAlreadyCalled
+ }
+
+ var refTrack TrackLocal
+ if len(r.trackEncodings) != 0 {
+ refTrack = r.trackEncodings[0].track
+ }
+ if refTrack == nil || refTrack.RID() == "" {
+ return errRTPSenderNoBaseEncoding
+ }
+
+ if refTrack.ID() != track.ID() || refTrack.StreamID() != track.StreamID() || refTrack.Kind() != track.Kind() {
+ return errRTPSenderBaseEncodingMismatch
+ }
+
+ for _, encoding := range r.trackEncodings {
+ if encoding.track == nil {
+ continue
+ }
+
+ if encoding.track.RID() == track.RID() {
+ return errRTPSenderRIDCollision
+ }
}
+
+ r.addEncoding(track)
+ return nil
+}
+
+func (r *RTPSender) addEncoding(track TrackLocal) {
+ ssrc := SSRC(randutil.NewMathRandomGenerator().Uint32())
+ trackEncoding := &trackEncoding{
+ track: track,
+ srtpStream: &srtpWriterFuture{ssrc: ssrc},
+ ssrc: ssrc,
+ }
+ trackEncoding.srtpStream.rtpSender = r
+ trackEncoding.rtcpInterceptor = r.api.interceptor.BindRTCPReader(
+ interceptor.RTPReaderFunc(func(in []byte, a interceptor.Attributes) (n int, attributes interceptor.Attributes, err error) {
+ n, err = trackEncoding.srtpStream.Read(in)
+ return n, a, err
+ }),
+ )
+
+ r.trackEncodings = append(r.trackEncodings, trackEncoding)
}
// Track returns the RTCRtpTransceiver track, or nil
func (r *RTPSender) Track() TrackLocal {
r.mu.RLock()
defer r.mu.RUnlock()
- return r.track
+
+ if len(r.trackEncodings) == 0 {
+ return nil
+ }
+
+ return r.trackEncodings[0].track
}
// ReplaceTrack replaces the track currently being used as the sender's source with a new TrackLocal.
@@ -129,27 +229,54 @@ 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 {
+ if track != nil && r.kind != track.Kind() {
+ return ErrRTPSenderNewTrackHasIncorrectKind
+ }
+
+ // cannot replace simulcast envelope
+ if track != nil && len(r.trackEncodings) > 1 {
+ return ErrRTPSenderNewTrackHasIncorrectEnvelope
+ }
+
+ var replacedTrack TrackLocal
+ var context *TrackLocalContext
+ if len(r.trackEncodings) != 0 {
+ replacedTrack = r.trackEncodings[0].track
+ context = &r.trackEncodings[0].context
+ }
+ if r.hasSent() && replacedTrack != nil {
+ if err := replacedTrack.Unbind(*context); err != nil {
return err
}
}
if !r.hasSent() || track == nil {
- r.track = track
+ r.trackEncodings[0].track = track
return nil
}
- if _, err := track.Bind(r.context); err != nil {
+ codec, err := track.Bind(TrackLocalContext{
+ id: context.id,
+ params: r.api.mediaEngine.getRTPParametersByKind(track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
+ ssrc: context.ssrc,
+ writeStream: context.writeStream,
+ rtcpInterceptor: context.rtcpInterceptor,
+ })
+ if err != nil {
// Re-bind the original track
- if _, reBindErr := r.track.Bind(r.context); reBindErr != nil {
+ if _, reBindErr := replacedTrack.Bind(*context); reBindErr != nil {
return reBindErr
}
return err
}
- r.track = track
+ // Codec has changed
+ if r.payloadType != codec.PayloadType {
+ context.params.Codecs = []RTPCodecParameters{codec}
+ }
+
+ r.trackEncodings[0].track = track
return nil
}
@@ -158,29 +285,45 @@ func (r *RTPSender) Send(parameters RTPSendParameters) error {
r.mu.Lock()
defer r.mu.Unlock()
- if r.hasSent() {
+ switch {
+ case r.hasSent():
return errRTPSenderSendAlreadyCalled
+ case r.trackEncodings[0].track == nil:
+ return errRTPSenderTrackRemoved
}
- 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,
- }
+ for idx, trackEncoding := range r.trackEncodings {
+ writeStream := &interceptorToTrackLocalWriter{}
+ trackEncoding.context = TrackLocalContext{
+ id: r.id,
+ params: r.api.mediaEngine.getRTPParametersByKind(trackEncoding.track.Kind(), []RTPTransceiverDirection{RTPTransceiverDirectionSendonly}),
+ ssrc: parameters.Encodings[idx].SSRC,
+ writeStream: writeStream,
+ rtcpInterceptor: trackEncoding.rtcpInterceptor,
+ }
- codec, err := r.track.Bind(r.context)
- if err != nil {
- return err
+ codec, err := trackEncoding.track.Bind(trackEncoding.context)
+ if err != nil {
+ return err
+ }
+ trackEncoding.context.params.Codecs = []RTPCodecParameters{codec}
+
+ trackEncoding.streamInfo = *createStreamInfo(
+ r.id,
+ parameters.Encodings[idx].SSRC,
+ codec.PayloadType,
+ codec.RTPCodecCapability,
+ parameters.HeaderExtensions,
+ )
+ srtpStream := trackEncoding.srtpStream
+ rtpInterceptor := r.api.interceptor.BindLocalStream(
+ &trackEncoding.streamInfo,
+ interceptor.RTPWriterFunc(func(header *rtp.Header, payload []byte, attributes interceptor.Attributes) (int, error) {
+ return srtpStream.WriteRTP(header, payload)
+ }),
+ )
+ writeStream.interceptor.Store(rtpInterceptor)
}
- r.context.params.Codecs = []RTPCodecParameters{codec}
-
- r.streamInfo = createStreamInfo(r.id, parameters.Encodings[0].SSRC, codec.PayloadType, codec.RTPCodecCapability, parameters.HeaderExtensions)
- rtpInterceptor := r.api.interceptor.BindLocalStream(&r.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
@@ -206,16 +349,20 @@ func (r *RTPSender) Stop() error {
return err
}
- r.api.interceptor.UnbindLocalStream(&r.streamInfo)
+ errs := []error{}
+ for _, trackEncoding := range r.trackEncodings {
+ r.api.interceptor.UnbindLocalStream(&trackEncoding.streamInfo)
+ errs = append(errs, trackEncoding.srtpStream.Close())
+ }
- return r.srtpStream.Close()
+ return util.FlattenErrs(errs)
}
-// Read reads incoming RTCP for this RTPReceiver
+// Read reads incoming RTCP for this RTPSender
func (r *RTPSender) Read(b []byte) (n int, a interceptor.Attributes, err error) {
select {
case <-r.sendCalled:
- return r.rtcpInterceptor.Read(b, a)
+ return r.trackEncodings[0].rtcpInterceptor.Read(b, a)
case <-r.stopCalled:
return 0, nil, io.ErrClosedPipe
}
@@ -223,7 +370,7 @@ func (r *RTPSender) Read(b []byte) (n int, a interceptor.Attributes, err error)
// 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)
+ b := make([]byte, r.api.settingEngine.getReceiveMTU())
i, attributes, err := r.Read(b)
if err != nil {
return nil, nil, err
@@ -237,10 +384,50 @@ func (r *RTPSender) ReadRTCP() ([]rtcp.Packet, interceptor.Attributes, error) {
return pkts, attributes, nil
}
+// ReadSimulcast reads incoming RTCP for this RTPSender for given rid
+func (r *RTPSender) ReadSimulcast(b []byte, rid string) (n int, a interceptor.Attributes, err error) {
+ select {
+ case <-r.sendCalled:
+ for _, t := range r.trackEncodings {
+ if t.track != nil && t.track.RID() == rid {
+ return t.rtcpInterceptor.Read(b, a)
+ }
+ }
+ return 0, nil, fmt.Errorf("%w: %s", errRTPSenderNoTrackForRID, rid)
+ case <-r.stopCalled:
+ return 0, nil, io.ErrClosedPipe
+ }
+}
+
+// ReadSimulcastRTCP is a convenience method that wraps ReadSimulcast and unmarshal for you
+func (r *RTPSender) ReadSimulcastRTCP(rid string) ([]rtcp.Packet, interceptor.Attributes, error) {
+ b := make([]byte, r.api.settingEngine.getReceiveMTU())
+ i, attributes, err := r.ReadSimulcast(b, rid)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ pkts, err := rtcp.Unmarshal(b[:i])
+ return pkts, attributes, err
+}
+
// SetReadDeadline sets the deadline for the Read operation.
// Setting to zero means no deadline.
func (r *RTPSender) SetReadDeadline(t time.Time) error {
- return r.srtpStream.SetReadDeadline(t)
+ return r.trackEncodings[0].srtpStream.SetReadDeadline(t)
+}
+
+// SetReadDeadlineSimulcast sets the max amount of time the RTCP stream for a given rid will block before returning. 0 is forever.
+func (r *RTPSender) SetReadDeadlineSimulcast(deadline time.Time, rid string) error {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ for _, t := range r.trackEncodings {
+ if t.track != nil && t.track.RID() == rid {
+ return t.srtpStream.SetReadDeadline(deadline)
+ }
+ }
+ return fmt.Errorf("%w: %s", errRTPSenderNoTrackForRID, rid)
}
// hasSent tells if data has been ever sent for this instance
diff --git a/rtpsender_js.go b/rtpsender_js.go
index cd22492045c..cdb9dd59541 100644
--- a/rtpsender_js.go
+++ b/rtpsender_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/rtpsender_test.go b/rtpsender_test.go
index 8afa7fd106e..28831bb6f93 100644
--- a/rtpsender_test.go
+++ b/rtpsender_test.go
@@ -1,38 +1,21 @@
+//go:build !js
// +build !js
package webrtc
import (
- "bytes"
"context"
"errors"
"io"
- "sync"
"sync/atomic"
"testing"
"time"
- "github.com/pion/transport/packetio"
"github.com/pion/transport/test"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/stretchr/testify/assert"
)
-func untilConnectionState(state PeerConnectionState, peers ...*PeerConnection) *sync.WaitGroup {
- var triggered sync.WaitGroup
- triggered.Add(len(peers))
-
- hdlr := func(p PeerConnectionState) {
- if p == state {
- triggered.Done()
- }
- }
- for _, p := range peers {
- p.OnConnectionStateChange(hdlr)
- }
- return &triggered
-}
-
func Test_RTPSender_ReplaceTrack(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
@@ -40,117 +23,107 @@ func Test_RTPSender_ReplaceTrack(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
- t.Run("Basic", func(t *testing.T) {
- s := SettingEngine{}
- s.DisableSRTPReplayProtection(true)
-
- m := &MediaEngine{}
- assert.NoError(t, m.RegisterDefaultCodecs())
+ s := SettingEngine{}
+ s.DisableSRTPReplayProtection(true)
- sender, receiver, err := NewAPI(WithMediaEngine(m), WithSettingEngine(s)).newPair(Configuration{})
- assert.NoError(t, err)
+ m := &MediaEngine{}
+ assert.NoError(t, m.RegisterDefaultCodecs())
- trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
- assert.NoError(t, err)
+ sender, receiver, err := NewAPI(WithMediaEngine(m), WithSettingEngine(s)).newPair(Configuration{})
+ assert.NoError(t, err)
- trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
- assert.NoError(t, err)
+ trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
- rtpSender, err := sender.AddTrack(trackA)
- assert.NoError(t, err)
+ trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264}, "video", "pion")
+ assert.NoError(t, err)
- seenPacketA, seenPacketACancel := context.WithCancel(context.Background())
- seenPacketB, seenPacketBCancel := context.WithCancel(context.Background())
+ rtpSender, err := sender.AddTrack(trackA)
+ assert.NoError(t, err)
- var onTrackCount uint64
- receiver.OnTrack(func(track *TrackRemote, _ *RTPReceiver) {
- assert.Equal(t, uint64(1), atomic.AddUint64(&onTrackCount, 1))
+ seenPacketA, seenPacketACancel := context.WithCancel(context.Background())
+ seenPacketB, seenPacketBCancel := context.WithCancel(context.Background())
- for {
- pkt, _, err := track.ReadRTP()
- if err != nil {
- assert.True(t, errors.Is(io.EOF, err))
- return
- }
+ var onTrackCount uint64
+ receiver.OnTrack(func(track *TrackRemote, _ *RTPReceiver) {
+ assert.Equal(t, uint64(1), atomic.AddUint64(&onTrackCount, 1))
- switch {
- case bytes.Equal(pkt.Payload, []byte{0x10, 0xAA}):
- seenPacketACancel()
- case bytes.Equal(pkt.Payload, []byte{0x10, 0xBB}):
- seenPacketBCancel()
- }
- }
- })
-
- assert.NoError(t, signalPair(sender, receiver))
-
- // Block Until packet with 0xAA has been seen
- func() {
- for range time.Tick(time.Millisecond * 20) {
- select {
- case <-seenPacketA.Done():
- return
- default:
- assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
- }
- }
- }()
-
- assert.NoError(t, rtpSender.ReplaceTrack(trackB))
-
- // Block Until packet with 0xBB has been seen
- func() {
- for range time.Tick(time.Millisecond * 20) {
- select {
- case <-seenPacketB.Done():
- return
- default:
- assert.NoError(t, trackB.WriteSample(media.Sample{Data: []byte{0xBB}, Duration: time.Second}))
- }
+ for {
+ pkt, _, err := track.ReadRTP()
+ if err != nil {
+ assert.True(t, errors.Is(io.EOF, err))
+ return
}
- }()
- closePairNow(t, sender, receiver)
+ switch {
+ case pkt.Payload[len(pkt.Payload)-1] == 0xAA:
+ assert.Equal(t, track.Codec().MimeType, MimeTypeVP8)
+ seenPacketACancel()
+ case pkt.Payload[len(pkt.Payload)-1] == 0xBB:
+ assert.Equal(t, track.Codec().MimeType, MimeTypeH264)
+ seenPacketBCancel()
+ default:
+ t.Fatalf("Unexpected RTP Data % 02x", pkt.Payload[len(pkt.Payload)-1])
+ }
+ }
})
- t.Run("Invalid Codec Change", func(t *testing.T) {
- sender, receiver, err := newPair()
- assert.NoError(t, err)
+ assert.NoError(t, signalPair(sender, receiver))
- trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
- assert.NoError(t, err)
+ // Block Until packet with 0xAA has been seen
+ func() {
+ for range time.Tick(time.Millisecond * 20) {
+ select {
+ case <-seenPacketA.Done():
+ return
+ default:
+ assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
+ }
+ }
+ }()
+
+ assert.NoError(t, rtpSender.ReplaceTrack(trackB))
+
+ // Block Until packet with 0xBB has been seen
+ func() {
+ for range time.Tick(time.Millisecond * 20) {
+ select {
+ case <-seenPacketB.Done():
+ return
+ default:
+ assert.NoError(t, trackB.WriteSample(media.Sample{Data: []byte{0xBB}, Duration: time.Second}))
+ }
+ }
+ }()
- trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "video", "pion")
- assert.NoError(t, err)
+ closePairNow(t, sender, receiver)
+}
- rtpSender, err := sender.AddTrack(trackA)
- assert.NoError(t, err)
+func Test_RTPSender_GetParameters(t *testing.T) {
+ lim := test.TimeOut(time.Second * 10)
+ defer lim.Stop()
- assert.NoError(t, signalPair(sender, receiver))
+ report := test.CheckRoutines(t)
+ defer report()
- seenPacket, seenPacketCancel := context.WithCancel(context.Background())
- receiver.OnTrack(func(_ *TrackRemote, _ *RTPReceiver) {
- seenPacketCancel()
- })
+ offerer, answerer, err := newPair()
+ assert.NoError(t, err)
- func() {
- for range time.Tick(time.Millisecond * 20) {
- select {
- case <-seenPacket.Done():
- return
- default:
- assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
- }
- }
- }()
+ rtpTransceiver, err := offerer.AddTransceiverFromKind(RTPCodecTypeVideo)
+ assert.NoError(t, err)
- assert.True(t, errors.Is(rtpSender.ReplaceTrack(trackB), ErrUnsupportedCodec))
+ assert.NoError(t, signalPair(offerer, answerer))
- closePairNow(t, sender, receiver)
- })
+ parameters := rtpTransceiver.Sender().GetParameters()
+ assert.NotEqual(t, 0, len(parameters.Codecs))
+ assert.Equal(t, 1, len(parameters.Encodings))
+ assert.Equal(t, rtpTransceiver.Sender().trackEncodings[0].ssrc, parameters.Encodings[0].SSRC)
+ assert.Equal(t, "", parameters.Encodings[0].RID)
+
+ closePairNow(t, offerer, answerer)
}
-func Test_RTPSender_GetParameters(t *testing.T) {
+func Test_RTPSender_GetParameters_WithRID(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
@@ -165,10 +138,14 @@ func Test_RTPSender_GetParameters(t *testing.T) {
assert.NoError(t, signalPair(offerer, answerer))
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("moo"))
+ assert.NoError(t, err)
+
+ err = rtpTransceiver.setSendingTrack(track)
+ assert.NoError(t, err)
+
parameters := rtpTransceiver.Sender().GetParameters()
- assert.NotEqual(t, 0, len(parameters.Codecs))
- assert.Equal(t, 1, len(parameters.Encodings))
- assert.Equal(t, rtpTransceiver.Sender().ssrc, parameters.Encodings[0].SSRC)
+ assert.Equal(t, track.RID(), parameters.Encodings[0].RID)
closePairNow(t, offerer, answerer)
}
@@ -182,7 +159,7 @@ func Test_RTPSender_SetReadDeadline(t *testing.T) {
sender, receiver, wan := createVNetPair(t)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
rtpSender, err := sender.AddTrack(track)
@@ -196,8 +173,231 @@ func Test_RTPSender_SetReadDeadline(t *testing.T) {
assert.NoError(t, rtpSender.SetReadDeadline(time.Now().Add(1*time.Second)))
_, _, err = rtpSender.ReadRTCP()
- assert.Error(t, err, packetio.ErrTimeout)
+ assert.Error(t, err)
assert.NoError(t, wan.Stop())
closePairNow(t, sender, receiver)
}
+
+func Test_RTPSender_ReplaceTrack_InvalidTrackKindChange(t *testing.T) {
+ lim := test.TimeOut(time.Second * 10)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ sender, receiver, err := newPair()
+ assert.NoError(t, err)
+
+ trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio", "pion")
+ assert.NoError(t, err)
+
+ rtpSender, err := sender.AddTrack(trackA)
+ assert.NoError(t, err)
+
+ assert.NoError(t, signalPair(sender, receiver))
+
+ seenPacket, seenPacketCancel := context.WithCancel(context.Background())
+ receiver.OnTrack(func(_ *TrackRemote, _ *RTPReceiver) {
+ seenPacketCancel()
+ })
+
+ func() {
+ for range time.Tick(time.Millisecond * 20) {
+ select {
+ case <-seenPacket.Done():
+ return
+ default:
+ assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
+ }
+ }
+ }()
+
+ assert.True(t, errors.Is(rtpSender.ReplaceTrack(trackB), ErrRTPSenderNewTrackHasIncorrectKind))
+
+ closePairNow(t, sender, receiver)
+}
+
+func Test_RTPSender_ReplaceTrack_InvalidCodecChange(t *testing.T) {
+ lim := test.TimeOut(time.Second * 10)
+ defer lim.Stop()
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ sender, receiver, err := newPair()
+ assert.NoError(t, err)
+
+ trackA, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ trackB, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP9}, "video", "pion")
+ assert.NoError(t, err)
+
+ rtpSender, err := sender.AddTrack(trackA)
+ assert.NoError(t, err)
+
+ err = rtpSender.rtpTransceiver.SetCodecPreferences([]RTPCodecParameters{{
+ RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
+ PayloadType: 96,
+ }})
+ assert.NoError(t, err)
+
+ assert.NoError(t, signalPair(sender, receiver))
+
+ seenPacket, seenPacketCancel := context.WithCancel(context.Background())
+ receiver.OnTrack(func(_ *TrackRemote, _ *RTPReceiver) {
+ seenPacketCancel()
+ })
+
+ func() {
+ for range time.Tick(time.Millisecond * 20) {
+ select {
+ case <-seenPacket.Done():
+ return
+ default:
+ assert.NoError(t, trackA.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
+ }
+ }
+ }()
+
+ assert.True(t, errors.Is(rtpSender.ReplaceTrack(trackB), ErrUnsupportedCodec))
+
+ closePairNow(t, sender, receiver)
+}
+
+func Test_RTPSender_GetParameters_NilTrack(t *testing.T) {
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ rtpSender, err := peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ assert.NoError(t, rtpSender.ReplaceTrack(nil))
+ rtpSender.GetParameters()
+
+ assert.NoError(t, peerConnection.Close())
+}
+
+func Test_RTPSender_Send(t *testing.T) {
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ rtpSender, err := peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ parameter := rtpSender.GetParameters()
+ err = rtpSender.Send(parameter)
+ <-rtpSender.sendCalled
+ assert.NoError(t, err)
+
+ assert.NoError(t, peerConnection.Close())
+}
+
+func Test_RTPSender_Send_Called_Once(t *testing.T) {
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ rtpSender, err := peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ parameter := rtpSender.GetParameters()
+ err = rtpSender.Send(parameter)
+ <-rtpSender.sendCalled
+ assert.NoError(t, err)
+
+ err = rtpSender.Send(parameter)
+ assert.Equal(t, errRTPSenderSendAlreadyCalled, err)
+
+ assert.NoError(t, peerConnection.Close())
+}
+
+func Test_RTPSender_Send_Track_Removed(t *testing.T) {
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ rtpSender, err := peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ parameter := rtpSender.GetParameters()
+ assert.NoError(t, peerConnection.RemoveTrack(rtpSender))
+ assert.Equal(t, errRTPSenderTrackRemoved, rtpSender.Send(parameter))
+
+ assert.NoError(t, peerConnection.Close())
+}
+
+func Test_RTPSender_Add_Encoding(t *testing.T) {
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+
+ peerConnection, err := NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ rtpSender, err := peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ assert.Equal(t, errRTPSenderTrackNil, rtpSender.AddEncoding(nil))
+
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderRidNil, rtpSender.AddEncoding(track1))
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("h"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderNoBaseEncoding, rtpSender.AddEncoding(track1))
+
+ track, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("q"))
+ assert.NoError(t, err)
+
+ rtpSender, err = peerConnection.AddTrack(track)
+ assert.NoError(t, err)
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video1", "pion", WithRTPStreamID("h"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1))
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1", WithRTPStreamID("h"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1))
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "video", "pion", WithRTPStreamID("h"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderBaseEncodingMismatch, rtpSender.AddEncoding(track1))
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("q"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderRIDCollision, rtpSender.AddEncoding(track1))
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("h"))
+ assert.NoError(t, err)
+ assert.NoError(t, rtpSender.AddEncoding(track1))
+
+ err = rtpSender.Send(rtpSender.GetParameters())
+ assert.NoError(t, err)
+
+ track1, err = NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion", WithRTPStreamID("f"))
+ assert.NoError(t, err)
+ assert.Equal(t, errRTPSenderSendAlreadyCalled, rtpSender.AddEncoding(track1))
+
+ err = rtpSender.Stop()
+ assert.NoError(t, err)
+
+ assert.Equal(t, errRTPSenderStopped, rtpSender.AddEncoding(track1))
+
+ assert.NoError(t, peerConnection.Close())
+}
diff --git a/rtptransceiver.go b/rtptransceiver.go
index 622915659cb..4ff4ead7d64 100644
--- a/rtptransceiver.go
+++ b/rtptransceiver.go
@@ -1,9 +1,11 @@
+//go:build !js
// +build !js
package webrtc
import (
"fmt"
+ "sync"
"sync/atomic"
"github.com/pion/rtp"
@@ -16,8 +18,13 @@ type RTPTransceiver struct {
receiver atomic.Value // *RTPReceiver
direction atomic.Value // RTPTransceiverDirection
+ codecs []RTPCodecParameters // User provided codecs via SetCodecPreferences
+
stopped bool
kind RTPCodecType
+
+ api *API
+ mu sync.RWMutex
}
func newRTPTransceiver(
@@ -25,14 +32,54 @@ func newRTPTransceiver(
sender *RTPSender,
direction RTPTransceiverDirection,
kind RTPCodecType,
+ api *API,
) *RTPTransceiver {
- t := &RTPTransceiver{kind: kind}
+ t := &RTPTransceiver{kind: kind, api: api}
t.setReceiver(receiver)
t.setSender(sender)
t.setDirection(direction)
return t
}
+// SetCodecPreferences sets preferred list of supported codecs
+// if codecs is empty or nil we reset to default from MediaEngine
+func (t *RTPTransceiver) SetCodecPreferences(codecs []RTPCodecParameters) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ for _, codec := range codecs {
+ if _, matchType := codecParametersFuzzySearch(codec, t.api.mediaEngine.getCodecsByKind(t.kind)); matchType == codecMatchNone {
+ return fmt.Errorf("%w %s", errRTPTransceiverCodecUnsupported, codec.MimeType)
+ }
+ }
+
+ t.codecs = codecs
+ return nil
+}
+
+// Codecs returns list of supported codecs
+func (t *RTPTransceiver) getCodecs() []RTPCodecParameters {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ mediaEngineCodecs := t.api.mediaEngine.getCodecsByKind(t.kind)
+ if len(t.codecs) == 0 {
+ return mediaEngineCodecs
+ }
+
+ filteredCodecs := []RTPCodecParameters{}
+ for _, codec := range t.codecs {
+ if c, matchType := codecParametersFuzzySearch(codec, mediaEngineCodecs); matchType != codecMatchNone {
+ if codec.PayloadType == 0 {
+ codec.PayloadType = c.PayloadType
+ }
+ filteredCodecs = append(filteredCodecs, codec)
+ }
+ }
+
+ return filteredCodecs
+}
+
// Sender returns the RTPTransceiver's RTPSender if it has one
func (t *RTPTransceiver) Sender() *RTPSender {
if v := t.sender.Load(); v != nil {
@@ -49,6 +96,14 @@ func (t *RTPTransceiver) SetSender(s *RTPSender, track TrackLocal) error {
}
func (t *RTPTransceiver) setSender(s *RTPSender) {
+ if s != nil {
+ s.setRTPTransceiver(t)
+ }
+
+ if prevSender := t.Sender(); prevSender != nil {
+ prevSender.setRTPTransceiver(nil)
+ }
+
t.sender.Store(s)
}
@@ -61,8 +116,8 @@ func (t *RTPTransceiver) Receiver() *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 {
+// 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)
}
@@ -90,13 +145,13 @@ func (t *RTPTransceiver) Direction() RTPTransceiverDirection {
// Stop irreversibly stops the RTPTransceiver
func (t *RTPTransceiver) Stop() error {
- if t.Sender() != nil {
- if err := t.Sender().Stop(); err != nil {
+ if sender := t.Sender(); sender != nil {
+ if err := sender.Stop(); err != nil {
return err
}
}
- if t.Receiver() != nil {
- if err := t.Receiver().Stop(); err != nil {
+ if receiver := t.Receiver(); receiver != nil {
+ if err := receiver.Stop(); err != nil {
return err
}
}
@@ -106,6 +161,14 @@ func (t *RTPTransceiver) Stop() error {
}
func (t *RTPTransceiver) setReceiver(r *RTPReceiver) {
+ if r != nil {
+ r.setRTPTransceiver(t)
+ }
+
+ if prevReceiver := t.Receiver(); prevReceiver != nil {
+ prevReceiver.setRTPTransceiver(nil)
+ }
+
t.receiver.Store(r)
}
@@ -183,7 +246,7 @@ func satisfyTypeAndDirection(remoteKind RTPCodecType, remoteDirection RTPTransce
// 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) {
+func handleUnknownRTPPacket(buf []byte, midExtensionID, streamIDExtensionID, repairStreamIDExtensionID uint8, mid, rid, rsid *string) (payloadType PayloadType, err error) {
rp := &rtp.Packet{}
if err = rp.Unmarshal(buf); err != nil {
return
@@ -195,11 +258,15 @@ func handleUnknownRTPPacket(buf []byte, midExtensionID, streamIDExtensionID uint
payloadType = PayloadType(rp.PayloadType)
if payload := rp.GetExtension(midExtensionID); payload != nil {
- mid = string(payload)
+ *mid = string(payload)
}
if payload := rp.GetExtension(streamIDExtensionID); payload != nil {
- rid = string(payload)
+ *rid = string(payload)
+ }
+
+ if payload := rp.GetExtension(repairStreamIDExtensionID); payload != nil {
+ *rsid = string(payload)
}
return
diff --git a/rtptransceiver_js.go b/rtptransceiver_js.go
index 3c5655fd74d..0e761315fda 100644
--- a/rtptransceiver_js.go
+++ b/rtptransceiver_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/rtptransceiver_test.go b/rtptransceiver_test.go
new file mode 100644
index 00000000000..72697c95a48
--- /dev/null
+++ b/rtptransceiver_test.go
@@ -0,0 +1,134 @@
+//go:build !js
+// +build !js
+
+package webrtc
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_RTPTransceiver_SetCodecPreferences(t *testing.T) {
+ me := &MediaEngine{}
+ api := NewAPI(WithMediaEngine(me))
+ assert.NoError(t, me.RegisterDefaultCodecs())
+
+ me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
+ me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
+
+ tr := RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
+ assert.EqualValues(t, me.videoCodecs, tr.getCodecs())
+
+ failTestCases := [][]RTPCodecParameters{
+ {
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
+ PayloadType: 111,
+ },
+ },
+ {
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 96,
+ },
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
+ PayloadType: 111,
+ },
+ },
+ }
+
+ for _, testCase := range failTestCases {
+ assert.ErrorIs(t, tr.SetCodecPreferences(testCase), errRTPTransceiverCodecUnsupported)
+ }
+
+ successTestCases := [][]RTPCodecParameters{
+ {
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 96,
+ },
+ },
+ {
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 96,
+ },
+ {
+ RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil},
+ PayloadType: 97,
+ },
+
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", nil},
+ PayloadType: 98,
+ },
+ {
+ RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil},
+ PayloadType: 99,
+ },
+ },
+ }
+
+ for _, testCase := range successTestCases {
+ assert.NoError(t, tr.SetCodecPreferences(testCase))
+ }
+
+ assert.NoError(t, tr.SetCodecPreferences(nil))
+ assert.NotEqual(t, 0, len(tr.getCodecs()))
+
+ assert.NoError(t, tr.SetCodecPreferences([]RTPCodecParameters{}))
+ assert.NotEqual(t, 0, len(tr.getCodecs()))
+}
+
+// Assert that SetCodecPreferences properly filters codecs and PayloadTypes are respected
+func Test_RTPTransceiver_SetCodecPreferences_PayloadType(t *testing.T) {
+ testCodec := RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{"video/testCodec", 90000, 0, "", nil},
+ PayloadType: 50,
+ }
+
+ m := &MediaEngine{}
+ assert.NoError(t, m.RegisterDefaultCodecs())
+
+ offerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ assert.NoError(t, m.RegisterCodec(testCodec, RTPCodecTypeVideo))
+
+ answerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
+ assert.NoError(t, err)
+
+ _, err = offerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+ assert.NoError(t, err)
+
+ answerTransceiver, err := answerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+ assert.NoError(t, err)
+
+ assert.NoError(t, answerTransceiver.SetCodecPreferences([]RTPCodecParameters{
+ testCodec,
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 51,
+ },
+ }))
+
+ offer, err := offerPC.CreateOffer(nil)
+ assert.NoError(t, err)
+
+ assert.NoError(t, offerPC.SetLocalDescription(offer))
+ assert.NoError(t, answerPC.SetRemoteDescription(offer))
+
+ answer, err := answerPC.CreateAnswer(nil)
+ assert.NoError(t, err)
+
+ // VP8 with proper PayloadType
+ assert.NotEqual(t, -1, strings.Index(answer.SDP, "a=rtpmap:51 VP8/90000"))
+
+ // testCodec is ignored since offerer doesn't support
+ assert.Equal(t, -1, strings.Index(answer.SDP, "testCodec"))
+
+ closePairNow(t, offerPC, answerPC)
+}
diff --git a/sctptransport.go b/sctptransport.go
index 3c61c1e267f..66bfc8efecd 100644
--- a/sctptransport.go
+++ b/sctptransport.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -114,10 +115,26 @@ func (r *SCTPTransport) Start(remoteCaps SCTPCapabilities) error {
}
r.lock.Lock()
- defer r.lock.Unlock()
-
r.sctpAssociation = sctpAssociation
r.state = SCTPTransportStateConnected
+ dataChannels := append([]*DataChannel{}, r.dataChannels...)
+ r.lock.Unlock()
+
+ var openedDCCount uint32
+ for _, d := range dataChannels {
+ if d.ReadyState() == DataChannelStateConnecting {
+ err := d.open(r, false)
+ if err != nil {
+ r.log.Warnf("failed to open data channel: %s", err)
+ continue
+ }
+ openedDCCount++
+ }
+ }
+
+ r.lock.Lock()
+ r.dataChannelsOpened += openedDCCount
+ r.lock.Unlock()
go r.acceptDataChannels()
@@ -184,19 +201,31 @@ func (r *SCTPTransport) acceptDataChannels() {
atomic.StoreUint32(&r.isAcceptLoopRunning, 1)
defer atomic.StoreUint32(&r.isAcceptLoopRunning, 0)
+ r.lock.RLock()
+ dataChannels := make([]*datachannel.DataChannel, 0, len(r.dataChannels))
+ for _, dc := range r.dataChannels {
+ dc.mu.Lock()
+ scopedDataChannel := dc.dataChannel
+ dc.mu.Unlock()
+
+ if scopedDataChannel != nil {
+ dataChannels = append(dataChannels, scopedDataChannel)
+ }
+ }
+ r.lock.RUnlock()
+
+ACCEPT:
for {
- r.lock.RLock()
- a := r.sctpAssociation
- r.lock.RUnlock()
+ // Safely access the most recent association
+ a := r.association()
dc, err := datachannel.Accept(a, &datachannel.Config{
LoggerFactory: r.api.settingEngine.LoggerFactory,
- })
+ }, dataChannels...)
if err != nil {
if err != io.EOF {
- r.lock.RLock()
- didRestart := r.sctpAssociation != a
- r.lock.RUnlock()
+ didRestart := r.association() != a
+
if didRestart {
// During a restart, `Accept` will return an `EOF`.
// If the association is different, it means the SCTPTransport just
@@ -211,6 +240,11 @@ func (r *SCTPTransport) acceptDataChannels() {
}
return
}
+ for _, ch := range dataChannels {
+ if ch.StreamIdentifier() == dc.StreamIdentifier() {
+ continue ACCEPT
+ }
+ }
var (
maxRetransmits *uint16
@@ -256,7 +290,7 @@ func (r *SCTPTransport) acceptDataChannels() {
}
<-r.onDataChannel(rtcDC)
- rtcDC.handleOpen(dc)
+ rtcDC.handleOpen(dc, true, dc.Config.Negotiated)
r.lock.Lock()
r.dataChannelsOpened++
@@ -399,15 +433,6 @@ func (r *SCTPTransport) collectStats(collector *statsReportCollector) {
}
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++
@@ -417,8 +442,19 @@ func (r *SCTPTransport) generateAndSetDataChannelID(dtlsRole DTLSRole, idOut **u
r.lock.Lock()
defer r.lock.Unlock()
+
+ // Create map of ids so we can compare without double-looping each time.
+ idsMap := make(map[uint16]struct{}, len(r.dataChannels))
+ for _, dc := range r.dataChannels {
+ if dc.ID() == nil {
+ continue
+ }
+
+ idsMap[*dc.ID()] = struct{}{}
+ }
+
for ; id < max-1; id += 2 {
- if isChannelWithID(id) {
+ if _, ok := idsMap[id]; ok {
continue
}
*idOut = &id
diff --git a/sctptransport_js.go b/sctptransport_js.go
new file mode 100644
index 00000000000..5a4d1573ed0
--- /dev/null
+++ b/sctptransport_js.go
@@ -0,0 +1,24 @@
+//go:build js && wasm
+// +build js,wasm
+
+package webrtc
+
+import "syscall/js"
+
+// SCTPTransport provides details about the SCTP transport.
+type SCTPTransport struct {
+ // Pointer to the underlying JavaScript SCTPTransport object.
+ underlying js.Value
+}
+
+// Transport returns the DTLSTransport instance the SCTPTransport is sending over.
+func (r *SCTPTransport) Transport() *DTLSTransport {
+ underlying := r.underlying.Get("transport")
+ if underlying.IsNull() || underlying.IsUndefined() {
+ return nil
+ }
+
+ return &DTLSTransport{
+ underlying: underlying,
+ }
+}
diff --git a/sctptransport_test.go b/sctptransport_test.go
index faff51ba377..822eb21a081 100644
--- a/sctptransport_test.go
+++ b/sctptransport_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/sdp.go b/sdp.go
index af4c36461a3..843bcb7f398 100644
--- a/sdp.go
+++ b/sdp.go
@@ -1,8 +1,10 @@
+//go:build !js
// +build !js
package webrtc
import (
+ "errors"
"fmt"
"net/url"
"regexp"
@@ -18,18 +20,32 @@ import (
// 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
+ mid string
+ kind RTPCodecType
+ streamID string
+ id string
+ ssrcs []SSRC
+ repairSsrc *SSRC
+ rids []string
}
func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails {
for i := range trackDetails {
- if trackDetails[i].ssrc == ssrc {
- return &trackDetails[i]
+ for j := range trackDetails[i].ssrcs {
+ if trackDetails[i].ssrcs[j] == ssrc {
+ return &trackDetails[i]
+ }
+ }
+ }
+ return nil
+}
+
+func trackDetailsForRID(trackDetails []trackDetails, rid string) *trackDetails {
+ for i := range trackDetails {
+ for j := range trackDetails[i].rids {
+ if trackDetails[i].rids[j] == rid {
+ return &trackDetails[i]
+ }
}
}
return nil
@@ -37,20 +53,31 @@ func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails {
func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails {
filtered := []trackDetails{}
+ doesTrackHaveSSRC := func(t trackDetails) bool {
+ for i := range t.ssrcs {
+ if t.ssrcs[i] == ssrc {
+ return true
+ }
+ }
+
+ return false
+ }
+
for i := range incomingTracks {
- if incomingTracks[i].ssrc != ssrc {
+ if !doesTrackHaveSSRC(incomingTracks[i]) {
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{}
-
+func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) (incomingTracks []trackDetails) { // nolint:gocognit
for _, media := range s.MediaDescriptions {
+ tracksInMediaSection := []trackDetails{}
+ rtxRepairFlows := map[uint64]uint64{}
+
// Plan B can have multiple tracks in a signle media section
streamID := ""
trackID := ""
@@ -82,7 +109,7 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
// 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)
+ baseSsrc, err := strconv.ParseUint(split[1], 10, 32)
if err != nil {
log.Warnf("Failed to parse SSRC: %v", err)
continue
@@ -92,8 +119,8 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
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
+ rtxRepairFlows[rtxRepairFlow] = baseSsrc
+ tracksInMediaSection = filterTrackWithSSRC(tracksInMediaSection, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before
}
}
@@ -115,7 +142,7 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
continue
}
- if rtxRepairFlow := rtxRepairFlows[uint32(ssrc)]; rtxRepairFlow {
+ if _, ok := rtxRepairFlows[ssrc]; ok {
continue // This ssrc is a RTX repair flow, ignore
}
@@ -126,10 +153,12 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
isNewTrack := true
trackDetails := &trackDetails{}
- for i := range incomingTracks {
- if incomingTracks[i].ssrc == SSRC(ssrc) {
- trackDetails = &incomingTracks[i]
- isNewTrack = false
+ for i := range tracksInMediaSection {
+ for j := range tracksInMediaSection[i].ssrcs {
+ if tracksInMediaSection[i].ssrcs[j] == SSRC(ssrc) {
+ trackDetails = &tracksInMediaSection[i]
+ isNewTrack = false
+ }
}
}
@@ -137,16 +166,23 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
trackDetails.kind = codecType
trackDetails.streamID = streamID
trackDetails.id = trackID
- trackDetails.ssrc = SSRC(ssrc)
+ trackDetails.ssrcs = []SSRC{SSRC(ssrc)}
+
+ for r, baseSsrc := range rtxRepairFlows {
+ if baseSsrc == ssrc {
+ repairSsrc := SSRC(r)
+ trackDetails.repairSsrc = &repairSsrc
+ }
+ }
if isNewTrack {
- incomingTracks = append(incomingTracks, *trackDetails)
+ tracksInMediaSection = append(tracksInMediaSection, *trackDetails)
}
}
}
if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" {
- newTrack := trackDetails{
+ simulcastTrack := trackDetails{
mid: midValue,
kind: codecType,
streamID: streamID,
@@ -154,19 +190,45 @@ func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) [
rids: []string{},
}
for rid := range rids {
- newTrack.rids = append(newTrack.rids, rid)
+ simulcastTrack.rids = append(simulcastTrack.rids, rid)
}
- incomingTracks = append(incomingTracks, newTrack)
+ tracksInMediaSection = []trackDetails{simulcastTrack}
}
+
+ incomingTracks = append(incomingTracks, tracksInMediaSection...)
}
+
return incomingTracks
}
+func trackDetailsToRTPReceiveParameters(t *trackDetails) RTPReceiveParameters {
+ encodingSize := len(t.ssrcs)
+ if len(t.rids) >= encodingSize {
+ encodingSize = len(t.rids)
+ }
+
+ encodings := make([]RTPDecodingParameters, encodingSize)
+ for i := range encodings {
+ if len(t.rids) > i {
+ encodings[i].RID = t.rids[i]
+ }
+ if len(t.ssrcs) > i {
+ encodings[i].SSRC = t.ssrcs[i]
+ }
+
+ if t.repairSsrc != nil {
+ encodings[i].RTX.SSRC = *t.repairSsrc
+ }
+ }
+
+ return RTPReceiveParameters{Encodings: encodings}
+}
+
func getRids(media *sdp.MediaDescription) map[string]string {
rids := map[string]string{}
for _, attr := range media.Attributes {
- if attr.Key == "rid" {
+ if attr.Key == sdpAttributeRid {
split := strings.Split(attr.Value, " ")
rids[split[0]] = attr.Value
}
@@ -278,7 +340,60 @@ func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGathe
}
}
-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) {
+func addSenderSDP(
+ mediaSection mediaSection,
+ isPlanB bool,
+ media *sdp.MediaDescription,
+) {
+ for _, mt := range mediaSection.transceivers {
+ sender := mt.Sender()
+ if sender == nil {
+ continue
+ }
+
+ track := sender.Track()
+ if track == nil {
+ continue
+ }
+
+ sendParameters := sender.GetParameters()
+ for _, encoding := range sendParameters.Encodings {
+ media = media.WithMediaSource(uint32(encoding.SSRC), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID())
+ if !isPlanB {
+ media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID())
+ }
+ }
+
+ if len(sendParameters.Encodings) > 1 {
+ sendRids := make([]string, 0, len(sendParameters.Encodings))
+
+ for _, encoding := range sendParameters.Encodings {
+ media.WithValueAttribute(sdpAttributeRid, encoding.RID+" send")
+ sendRids = append(sendRids, encoding.RID)
+ }
+ // Simulcast
+ media.WithValueAttribute("simulcast", "send "+strings.Join(sendRids, ";"))
+ }
+
+ if !isPlanB {
+ break
+ }
+ }
+}
+
+func addTransceiverSDP(
+ d *sdp.SessionDescription,
+ isPlanB bool,
+ 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
@@ -292,7 +407,7 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
WithPropertyAttribute(sdp.AttrKeyRTCPMux).
WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
- codecs := mediaEngine.getCodecsByKind(t.kind)
+ codecs := t.getCodecs()
for _, codec := range codecs {
name := strings.TrimPrefix(codec.MimeType, "audio/")
name = strings.TrimPrefix(name, "video/")
@@ -303,6 +418,11 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
}
}
if len(codecs) == 0 {
+ // If we are sender and we have no codecs throw an error early
+ if t.Sender() != nil {
+ return false, ErrSenderWithNoCodecs
+ }
+
// Explicitly reject track if we don't have the codec
d.WithMedia(&sdp.MediaDescription{
MediaName: sdp.MediaName{
@@ -336,23 +456,14 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
recvRids := make([]string, 0, len(mediaSection.ridMap))
for rid := range mediaSection.ridMap {
- media.WithValueAttribute("rid", rid+" recv")
+ media.WithValueAttribute(sdpAttributeRid, 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
- }
- }
- }
+ addSenderSDP(mediaSection, isPlanB, media)
media = media.WithPropertyAttribute(t.Direction().String())
@@ -495,7 +606,7 @@ func extractFingerprint(desc *sdp.SessionDescription) (string, string, error) {
return parts[1], parts[0], nil
}
-func extractICEDetails(desc *sdp.SessionDescription) (string, string, []ICECandidate, error) {
+func extractICEDetails(desc *sdp.SessionDescription, log logging.LeveledLogger) (string, string, []ICECandidate, error) { // nolint:gocognit
candidates := []ICECandidate{}
remotePwds := []string{}
remoteUfrags := []string{}
@@ -519,6 +630,10 @@ func extractICEDetails(desc *sdp.SessionDescription) (string, string, []ICECandi
if a.IsICECandidate() {
c, err := ice.UnmarshalCandidate(a.Value)
if err != nil {
+ if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) {
+ log.Warnf("Discarding remote candidate: %s", err)
+ continue
+ }
return "", "", nil, err
}
@@ -648,7 +763,6 @@ func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int,
// for subsequent calling, it updates Origin for SessionDescription from saved one
// and increments session version by one.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-25#section-5.2.2
-// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-25#section-5.3.2
func updateSDPOrigin(origin *sdp.Origin, d *sdp.SessionDescription) {
if atomic.CompareAndSwapUint64(&origin.SessionVersion, 0, d.Origin.SessionVersion) { // store
atomic.StoreUint64(&origin.SessionID, d.Origin.SessionID)
@@ -662,3 +776,13 @@ func updateSDPOrigin(origin *sdp.Origin, d *sdp.SessionDescription) {
d.Origin.SessionVersion = atomic.AddUint64(&origin.SessionVersion, 1)
}
}
+
+func isIceLiteSet(desc *sdp.SessionDescription) bool {
+ for _, a := range desc.Attributes {
+ if strings.TrimSpace(a.Key) == sdp.AttrKeyICELite {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/sdp_test.go b/sdp_test.go
index 7bed5081f8d..1d74426680c 100644
--- a/sdp_test.go
+++ b/sdp_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -78,7 +79,7 @@ func TestExtractICEDetails(t *testing.T) {
},
}
- _, _, _, err := extractICEDetails(s)
+ _, _, _, err := extractICEDetails(s, nil)
assert.Equal(t, err, ErrSessionDescriptionMissingIcePwd)
})
@@ -89,7 +90,7 @@ func TestExtractICEDetails(t *testing.T) {
},
}
- _, _, _, err := extractICEDetails(s)
+ _, _, _, err := extractICEDetails(s, nil)
assert.Equal(t, err, ErrSessionDescriptionMissingIceUfrag)
})
@@ -102,7 +103,7 @@ func TestExtractICEDetails(t *testing.T) {
MediaDescriptions: []*sdp.MediaDescription{},
}
- ufrag, pwd, _, err := extractICEDetails(s)
+ ufrag, pwd, _, err := extractICEDetails(s, nil)
assert.Equal(t, ufrag, defaultUfrag)
assert.Equal(t, pwd, defaultPwd)
assert.NoError(t, err)
@@ -120,7 +121,7 @@ func TestExtractICEDetails(t *testing.T) {
},
}
- ufrag, pwd, _, err := extractICEDetails(s)
+ ufrag, pwd, _, err := extractICEDetails(s, nil)
assert.Equal(t, ufrag, defaultUfrag)
assert.Equal(t, pwd, defaultPwd)
assert.NoError(t, err)
@@ -134,7 +135,7 @@ func TestExtractICEDetails(t *testing.T) {
},
}
- _, _, _, err := extractICEDetails(s)
+ _, _, _, err := extractICEDetails(s, nil)
assert.Equal(t, err, ErrSessionDescriptionConflictingIceUfrag)
})
@@ -146,7 +147,7 @@ func TestExtractICEDetails(t *testing.T) {
},
}
- _, _, _, err := extractICEDetails(s)
+ _, _, _, err := extractICEDetails(s, nil)
assert.Equal(t, err, ErrSessionDescriptionConflictingIcePwd)
})
}
@@ -204,7 +205,7 @@ func TestTrackDetailsFromSDP(t *testing.T) {
},
Attributes: []sdp.Attribute{
{Key: "sendonly"},
- {Key: "rid", Value: "f send pt=97;max-width=1280;max-height=720"},
+ {Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
},
},
},
@@ -219,14 +220,14 @@ func TestTrackDetailsFromSDP(t *testing.T) {
assert.Fail(t, "missing audio track with ssrc:2000")
} else {
assert.Equal(t, RTPCodecTypeAudio, track.kind)
- assert.Equal(t, SSRC(2000), track.ssrc)
+ assert.Equal(t, SSRC(2000), track.ssrcs[0])
assert.Equal(t, "audio_trk_label", track.streamID)
}
if track := trackDetailsForSSRC(tracks, 3000); track == nil {
assert.Fail(t, "missing video track with ssrc:3000")
} else {
assert.Equal(t, RTPCodecTypeVideo, track.kind)
- assert.Equal(t, SSRC(3000), track.ssrc)
+ assert.Equal(t, SSRC(3000), track.ssrcs[0])
assert.Equal(t, "video_trk_label", track.streamID)
}
if track := trackDetailsForSSRC(tracks, 4000); track != nil {
@@ -236,7 +237,7 @@ func TestTrackDetailsFromSDP(t *testing.T) {
assert.Fail(t, "missing video track with ssrc:5000")
} else {
assert.Equal(t, RTPCodecTypeVideo, track.kind)
- assert.Equal(t, SSRC(5000), track.ssrc)
+ assert.Equal(t, SSRC(5000), track.ssrcs[0])
assert.Equal(t, "video_trk_id", track.id)
assert.Equal(t, "video_stream_id", track.streamID)
}
@@ -307,6 +308,8 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
engine := &MediaEngine{}
assert.NoError(t, engine.RegisterDefaultCodecs())
+ api := NewAPI(WithMediaEngine(engine))
+
sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.NoError(t, err)
@@ -317,13 +320,17 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
{
id: "video",
transceivers: []*RTPTransceiver{{
- kind: RTPCodecTypeVideo,
+ kind: RTPCodecTypeVideo,
+ api: api,
+ codecs: engine.getCodecsByKind(RTPCodecTypeVideo),
}},
},
{
id: "audio",
transceivers: []*RTPTransceiver{{
- kind: RTPCodecTypeAudio,
+ kind: RTPCodecTypeAudio,
+ api: api,
+ codecs: engine.getCodecsByKind(RTPCodecTypeAudio),
}},
},
{
@@ -362,22 +369,23 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
}
func TestPopulateSDP(t *testing.T) {
- t.Run("Rid", func(t *testing.T) {
- tr := &RTPTransceiver{kind: RTPCodecTypeVideo}
+ t.Run("rid", func(t *testing.T) {
+ se := SettingEngine{}
+
+ me := &MediaEngine{}
+ assert.NoError(t, me.RegisterDefaultCodecs())
+ api := NewAPI(WithMediaEngine(me))
+
+ tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
tr.setDirection(RTPTransceiverDirectionRecvonly)
ridMap := map[string]string{
"ridkey": "some",
}
mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}, ridMap: ridMap}}
- se := SettingEngine{}
-
- m := MediaEngine{}
- assert.NoError(t, m.RegisterDefaultCodecs())
-
d := &sdp.SessionDescription{}
- offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
+ offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
assert.Nil(t, err)
// Test contains rid map keys
@@ -387,7 +395,7 @@ func TestPopulateSDP(t *testing.T) {
continue
}
for _, a := range desc.Attributes {
- if a.Key == "rid" {
+ if a.Key == sdpAttributeRid {
if strings.Contains(a.Value, "ridkey") {
found = true
break
@@ -397,6 +405,50 @@ func TestPopulateSDP(t *testing.T) {
}
assert.Equal(t, true, found, "Rid key should be present")
})
+ t.Run("SetCodecPreferences", func(t *testing.T) {
+ se := SettingEngine{}
+
+ me := &MediaEngine{}
+ assert.NoError(t, me.RegisterDefaultCodecs())
+ api := NewAPI(WithMediaEngine(me))
+ me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
+ me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
+
+ tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
+ tr.setDirection(RTPTransceiverDirectionRecvonly)
+ codecErr := tr.SetCodecPreferences([]RTPCodecParameters{
+ {
+ RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+ PayloadType: 96,
+ },
+ })
+ assert.NoError(t, codecErr)
+
+ mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
+
+ d := &sdp.SessionDescription{}
+
+ offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
+ assert.Nil(t, err)
+
+ // Test codecs
+ foundVP8 := false
+ for _, desc := range offerSdp.MediaDescriptions {
+ if desc.MediaName.Media != "video" {
+ continue
+ }
+ for _, a := range desc.Attributes {
+ if strings.Contains(a.Key, "rtpmap") {
+ if a.Value == "98 VP9/90000" {
+ t.Fatal("vp9 should not be present in sdp")
+ } else if a.Value == "96 VP8/90000" {
+ foundVP8 = true
+ }
+ }
+ }
+ }
+ assert.Equal(t, true, foundVP8, "vp8 should be present in sdp")
+ })
}
func TestGetRIDs(t *testing.T) {
@@ -407,7 +459,7 @@ func TestGetRIDs(t *testing.T) {
},
Attributes: []sdp.Attribute{
{Key: "sendonly"},
- {Key: "rid", Value: "f send pt=97;max-width=1280;max-height=720"},
+ {Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
},
},
}
diff --git a/sdpsemantics_test.go b/sdpsemantics_test.go
index b935dac1559..5930e98d754 100644
--- a/sdpsemantics_test.go
+++ b/sdpsemantics_test.go
@@ -1,8 +1,10 @@
+//go:build !js
// +build !js
package webrtc
import (
+ "errors"
"strings"
"testing"
"time"
@@ -46,7 +48,7 @@ func getMdNames(sdp *sdp.SessionDescription) []string {
func extractSsrcList(md *sdp.MediaDescription) []string {
ssrcMap := map[string]struct{}{}
for _, attr := range md.Attributes {
- if attr.Key == ssrcStr {
+ if attr.Key == sdp.AttrKeySSRC {
ssrc := strings.Fields(attr.Value)[0]
ssrcMap[ssrc] = struct{}{}
}
@@ -68,35 +70,30 @@ func TestSDPSemantics_PlanBOfferTransceivers(t *testing.T) {
opc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsPlanB,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
- }); err != nil {
- t.Errorf("AddTransceiver failed: %v", err)
- }
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
+ })
+ assert.NoError(t, err)
+
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
- }); err != nil {
- t.Errorf("AddTransceiver failed: %v", err)
- }
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
+ })
+ assert.NoError(t, err)
+
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
- }); err != nil {
- t.Errorf("AddTransceiver failed: %v", err)
- }
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
+ })
+ assert.NoError(t, err)
+
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
Direction: RTPTransceiverDirectionSendrecv,
- }); err != nil {
- t.Errorf("AddTransceiver failed: %v", err)
- }
+ })
+ assert.NoError(t, err)
offer, err := opc.CreateOffer(nil)
- if err != nil {
- t.Errorf("Plan B CreateOffer failed: %s", err)
- }
+ assert.NoError(t, err)
mdNames := getMdNames(offer.parsed)
assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
@@ -113,18 +110,12 @@ func TestSDPSemantics_PlanBOfferTransceivers(t *testing.T) {
apc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsPlanB,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- if err = apc.SetRemoteDescription(offer); err != nil {
- t.Errorf("SetRemoteDescription failed: %s", err)
- }
+ assert.NoError(t, apc.SetRemoteDescription(offer))
answer, err := apc.CreateAnswer(nil)
- if err != nil {
- t.Errorf("Plan B CreateAnswer failed: %s", err)
- }
+ assert.NoError(t, err)
mdNames = getMdNames(answer.parsed)
assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
@@ -142,76 +133,58 @@ func TestSDPSemantics_PlanBAnswerSenders(t *testing.T) {
opc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsPlanB,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
Direction: RTPTransceiverDirectionRecvonly,
- }); err != nil {
- t.Errorf("Failed to add transceiver")
- }
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
+ })
+ assert.NoError(t, err)
+
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
Direction: RTPTransceiverDirectionRecvonly,
- }); err != nil {
- t.Errorf("Failed to add transceiver")
- }
+ })
+ assert.NoError(t, err)
offer, err := opc.CreateOffer(nil)
- if err != nil {
- t.Errorf("Plan B CreateOffer failed: %s", err)
- }
+ assert.NoError(t, err)
- mdNames := getMdNames(offer.parsed)
- assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
+ assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"})
apc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsPlanB,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
- if err != nil {
- t.Errorf("Failed to create video track")
- }
- if _, err = apc.AddTrack(video1); err != nil {
- t.Errorf("Failed to add video track")
- }
- video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
- if err != nil {
- t.Errorf("Failed to create video track")
- }
- if _, err = apc.AddTrack(video2); err != nil {
- t.Errorf("Failed to add video track")
- }
- audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "3", "3")
- if err != nil {
- t.Errorf("Failed to create audio track")
- }
- if _, err = apc.AddTrack(audio1); err != nil {
- t.Errorf("Failed to add audio track")
- }
- audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "4", "4")
- if err != nil {
- t.Errorf("Failed to create audio track")
- }
- if _, err = apc.AddTrack(audio2); err != nil {
- t.Errorf("Failed to add audio track")
- }
+ video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
+ assert.NoError(t, err)
- if err = apc.SetRemoteDescription(offer); err != nil {
- t.Errorf("SetRemoteDescription failed: %s", err)
- }
+ _, err = apc.AddTrack(video1)
+ assert.NoError(t, err)
+
+ video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(video2)
+ assert.NoError(t, err)
+
+ audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(audio1)
+ assert.NoError(t, err)
+
+ audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(audio2)
+ assert.NoError(t, err)
+
+ assert.NoError(t, apc.SetRemoteDescription(offer))
answer, err := apc.CreateAnswer(nil)
- if err != nil {
- t.Errorf("Plan B CreateAnswer failed: %s", err)
- }
+ assert.NoError(t, err)
- mdNames = getMdNames(answer.parsed)
- assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
+ assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"})
// Verify that each section has 2 SSRCs (one for each sender)
for _, section := range []string{"video", "audio"} {
@@ -235,81 +208,63 @@ func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) {
opc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsPlanB,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{
Direction: RTPTransceiverDirectionRecvonly,
- }); err != nil {
- t.Errorf("Failed to add transceiver")
- }
- if _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
+ })
+ assert.NoError(t, err)
+
+ _, err = opc.AddTransceiverFromKind(RTPCodecTypeAudio, RTPTransceiverInit{
Direction: RTPTransceiverDirectionRecvonly,
- }); err != nil {
- t.Errorf("Failed to add transceiver")
- }
+ })
+ assert.NoError(t, err)
offer, err := opc.CreateOffer(nil)
- if err != nil {
- t.Errorf("Plan B CreateOffer failed: %s", err)
- }
+ assert.NoError(t, err)
- mdNames := getMdNames(offer.parsed)
- assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
+ assert.ObjectsAreEqual(getMdNames(offer.parsed), []string{"video", "audio", "data"})
apc, err := NewPeerConnection(Configuration{
SDPSemantics: SDPSemanticsUnifiedPlanWithFallback,
})
- if err != nil {
- t.Errorf("NewPeerConnection failed: %v", err)
- }
+ assert.NoError(t, err)
- video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
- if err != nil {
- t.Errorf("Failed to create video track")
- }
- if _, err = apc.AddTrack(video1); err != nil {
- t.Errorf("Failed to add video track")
- }
- video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
- if err != nil {
- t.Errorf("Failed to create video track")
- }
- if _, err = apc.AddTrack(video2); err != nil {
- t.Errorf("Failed to add video track")
- }
- audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "3", "3")
- if err != nil {
- t.Errorf("Failed to create audio track")
- }
- if _, err = apc.AddTrack(audio1); err != nil {
- t.Errorf("Failed to add audio track")
- }
- audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "audio/opus"}, "4", "4")
- if err != nil {
- t.Errorf("Failed to create audio track")
- }
- if _, err = apc.AddTrack(audio2); err != nil {
- t.Errorf("Failed to add audio track")
- }
+ video1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1")
+ assert.NoError(t, err)
- if err = apc.SetRemoteDescription(offer); err != nil {
- t.Errorf("SetRemoteDescription failed: %s", err)
- }
+ _, err = apc.AddTrack(video1)
+ assert.NoError(t, err)
+
+ video2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "2", "2")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(video2)
+ assert.NoError(t, err)
+
+ audio1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "3", "3")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(audio1)
+ assert.NoError(t, err)
+
+ audio2, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "4", "4")
+ assert.NoError(t, err)
+
+ _, err = apc.AddTrack(audio2)
+ assert.NoError(t, err)
+
+ assert.NoError(t, apc.SetRemoteDescription(offer))
answer, err := apc.CreateAnswer(nil)
- if err != nil {
- t.Errorf("Plan B CreateAnswer failed: %s", err)
- }
+ assert.NoError(t, err)
- mdNames = getMdNames(answer.parsed)
- assert.ObjectsAreEqual(mdNames, []string{"video", "audio", "data"})
+ assert.ObjectsAreEqual(getMdNames(answer.parsed), []string{"video", "audio", "data"})
extractSsrcList := func(md *sdp.MediaDescription) []string {
ssrcMap := map[string]struct{}{}
for _, attr := range md.Attributes {
- if attr.Key == ssrcStr {
+ if attr.Key == sdp.AttrKeySSRC {
ssrc := strings.Fields(attr.Value)[0]
ssrcMap[ssrc] = struct{}{}
}
@@ -331,3 +286,45 @@ func TestSDPSemantics_UnifiedPlanWithFallback(t *testing.T) {
closePairNow(t, apc, opc)
}
+
+// Assert that we can catch Remote SessionDescription that don't match our Semantics
+func TestSDPSemantics_SetRemoteDescription_Mismatch(t *testing.T) {
+ planBOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video audio\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:video\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:audio\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n"
+ unifiedPlanOffer := "v=0\r\no=- 4648475892259889561 3 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\na=ice-ufrag:1hhfzwf0ijpzm\r\na=ice-pwd:jm5puo2ab1op3vs59ca53bdk7s\r\na=fingerprint:sha-256 40:42:FB:47:87:52:BF:CB:EC:3A:DF:EB:06:DA:2D:B7:2F:59:42:10:23:7B:9D:4C:C9:58:DD:FF:A2:8F:17:67\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:0\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 goog-remb\r\na=fmtp:96 packetization-mode=1;profile-level-id=42e01f\r\na=ssrc:1505338584 cname:10000000b5810aac\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=setup:passive\r\na=mid:1\r\na=sendonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=ssrc:697641945 cname:10000000b5810aac\r\n"
+
+ report := test.CheckRoutines(t)
+ defer report()
+
+ lim := test.TimeOut(time.Second * 30)
+ defer lim.Stop()
+
+ t.Run("PlanB", func(t *testing.T) {
+ pc, err := NewPeerConnection(Configuration{
+ SDPSemantics: SDPSemanticsUnifiedPlan,
+ })
+ assert.NoError(t, err)
+
+ err = pc.SetRemoteDescription(SessionDescription{SDP: planBOffer, Type: SDPTypeOffer})
+ assert.NoError(t, err)
+
+ _, err = pc.CreateAnswer(nil)
+ assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics))
+
+ assert.NoError(t, pc.Close())
+ })
+
+ t.Run("UnifiedPlan", func(t *testing.T) {
+ pc, err := NewPeerConnection(Configuration{
+ SDPSemantics: SDPSemanticsPlanB,
+ })
+ assert.NoError(t, err)
+
+ err = pc.SetRemoteDescription(SessionDescription{SDP: unifiedPlanOffer, Type: SDPTypeOffer})
+ assert.NoError(t, err)
+
+ _, err = pc.CreateAnswer(nil)
+ assert.True(t, errors.Is(err, ErrIncorrectSDPSemantics))
+
+ assert.NoError(t, pc.Close())
+ })
+}
diff --git a/settingengine.go b/settingengine.go
index e2113a81ebc..a2f5ef41882 100644
--- a/settingengine.go
+++ b/settingengine.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -50,6 +51,9 @@ type SettingEngine struct {
SRTP *uint
SRTCP *uint
}
+ dtls struct {
+ retransmissionInterval time.Duration
+ }
sdpMediaLevelFingerprints bool
answeringDTLSRole DTLSRole
disableCertificateFingerprintVerification bool
@@ -63,6 +67,16 @@ type SettingEngine struct {
iceProxyDialer proxy.Dialer
disableMediaEngineCopy bool
srtpProtectionProfiles []dtls.SRTPProtectionProfile
+ receiveMTU uint
+}
+
+// getReceiveMTU returns the configured MTU. If SettingEngine's MTU is configured to 0 it returns the default
+func (e *SettingEngine) getReceiveMTU() uint {
+ if e.receiveMTU != 0 {
+ return e.receiveMTU
+ }
+
+ return receiveMTU
}
// DetachDataChannels enables detaching data channels. When enabled
@@ -279,3 +293,14 @@ func (e *SettingEngine) SetICEProxyDialer(d proxy.Dialer) {
func (e *SettingEngine) DisableMediaEngineCopy(isDisabled bool) {
e.disableMediaEngineCopy = isDisabled
}
+
+// SetReceiveMTU sets the size of read buffer that copies incoming packets. This is optional.
+// Leave this 0 for the default receiveMTU
+func (e *SettingEngine) SetReceiveMTU(receiveMTU uint) {
+ e.receiveMTU = receiveMTU
+}
+
+// SetDTLSRetransmissionInterval sets the retranmission interval for DTLS.
+func (e *SettingEngine) SetDTLSRetransmissionInterval(interval time.Duration) {
+ e.dtls.retransmissionInterval = interval
+}
diff --git a/settingengine_js.go b/settingengine_js.go
index 5b77d660237..a4ae0d0eefa 100644
--- a/settingengine_js.go
+++ b/settingengine_js.go
@@ -1,3 +1,4 @@
+//go:build js && wasm
// +build js,wasm
package webrtc
diff --git a/settingengine_test.go b/settingengine_test.go
index 17d6dfdfdce..80817ca34bf 100644
--- a/settingengine_test.go
+++ b/settingengine_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -212,3 +213,23 @@ func TestSettingEngine_SetDisableMediaEngineCopy(t *testing.T) {
closePairNow(t, offerer, answerer)
})
}
+
+func TestSetDTLSRetransmissionInterval(t *testing.T) {
+ s := SettingEngine{}
+
+ if s.dtls.retransmissionInterval != 0 {
+ t.Fatalf("SettingEngine defaults aren't as expected.")
+ }
+
+ s.SetDTLSRetransmissionInterval(100 * time.Millisecond)
+ if s.dtls.retransmissionInterval == 0 ||
+ s.dtls.retransmissionInterval != 100*time.Millisecond {
+ t.Errorf("Failed to set DTLS retransmission interval")
+ }
+
+ s.SetDTLSRetransmissionInterval(1 * time.Second)
+ if s.dtls.retransmissionInterval == 0 ||
+ s.dtls.retransmissionInterval != 1*time.Second {
+ t.Errorf("Failed to set DTLS retransmission interval")
+ }
+}
diff --git a/srtp_writer_future.go b/srtp_writer_future.go
index 4d8bafe1bc8..834d46132c5 100644
--- a/srtp_writer_future.go
+++ b/srtp_writer_future.go
@@ -1,9 +1,11 @@
+//go:build !js
// +build !js
package webrtc
import (
"io"
+ "sync"
"sync/atomic"
"time"
@@ -14,9 +16,12 @@ import (
// srtpWriterFuture blocks Read/Write calls until
// the SRTP Session is available
type srtpWriterFuture struct {
+ ssrc SSRC
rtpSender *RTPSender
rtcpReadStream atomic.Value // *srtp.ReadStreamSRTCP
rtpWriteStream atomic.Value // *srtp.WriteStreamSRTP
+ mu sync.Mutex
+ closed bool
}
func (s *srtpWriterFuture) init(returnWhenNoSRTP bool) error {
@@ -36,12 +41,19 @@ func (s *srtpWriterFuture) init(returnWhenNoSRTP bool) error {
}
}
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.closed {
+ return io.ErrClosedPipe
+ }
+
srtcpSession, err := s.rtpSender.transport.getSRTCPSession()
if err != nil {
return err
}
- rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(s.rtpSender.ssrc))
+ rtcpReadStream, err := srtcpSession.OpenReadStream(uint32(s.ssrc))
if err != nil {
return err
}
@@ -62,6 +74,14 @@ func (s *srtpWriterFuture) init(returnWhenNoSRTP bool) error {
}
func (s *srtpWriterFuture) Close() error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ if s.closed {
+ return nil
+ }
+ s.closed = true
+
if value := s.rtcpReadStream.Load(); value != nil {
return value.(*srtp.ReadStreamSRTCP).Close()
}
diff --git a/stats_go.go b/stats_go.go
index e1f622e5a2d..10ec55c9639 100644
--- a/stats_go.go
+++ b/stats_go.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/stats_go_test.go b/stats_go_test.go
index a1b94d83262..5d4de40110b 100644
--- a/stats_go_test.go
+++ b/stats_go_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -203,7 +204,7 @@ func TestPeerConnection_GetStats(t *testing.T) {
offerPC, answerPC, err := newPair()
assert.NoError(t, err)
- track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion1")
+ track1, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion1")
require.NoError(t, err)
_, err = offerPC.AddTrack(track1)
diff --git a/test-wasm/node_shim.js b/test-wasm/node_shim.js
index 9aeb245c0f4..a20b4c54f2f 100644
--- a/test-wasm/node_shim.js
+++ b/test-wasm/node_shim.js
@@ -1,10 +1,10 @@
// This file adds RTCPeerConnection to the global context, making Node.js more
// closely match the browser API for WebRTC.
-const wrtc = require("wrtc");
+const wrtc = require('wrtc')
global.window = {
RTCPeerConnection: wrtc.RTCPeerConnection
-};
+}
-global.RTCPeerConnection = wrtc.RTCPeerConnection;
+global.RTCPeerConnection = wrtc.RTCPeerConnection
diff --git a/track_local.go b/track_local.go
index e6e1da1f490..4b1c0ca6e09 100644
--- a/track_local.go
+++ b/track_local.go
@@ -1,6 +1,9 @@
package webrtc
-import "github.com/pion/rtp"
+import (
+ "github.com/pion/interceptor"
+ "github.com/pion/rtp"
+)
// TrackLocalWriter is the Writer for outbound RTP Packets
type TrackLocalWriter interface {
@@ -14,10 +17,11 @@ type TrackLocalWriter interface {
// TrackLocalContext is the Context passed when a TrackLocal has been Binded/Unbinded from a PeerConnection, and used
// in Interceptors.
type TrackLocalContext struct {
- id string
- params RTPParameters
- ssrc SSRC
- writeStream TrackLocalWriter
+ id string
+ params RTPParameters
+ ssrc SSRC
+ writeStream TrackLocalWriter
+ rtcpInterceptor interceptor.RTCPReader
}
// CodecParameters returns the negotiated RTPCodecParameters. These are the codecs supported by both
@@ -49,8 +53,13 @@ func (t *TrackLocalContext) ID() string {
return t.id
}
+// RTCPReader returns the RTCP interceptor for this TrackLocal. Used to read RTCP of this TrackLocal.
+func (t *TrackLocalContext) RTCPReader() interceptor.RTCPReader {
+ return t.rtcpInterceptor
+}
+
// TrackLocal is an interface that controls how the user can send media
-// The user can provide their own TrackLocal implementatiosn, or use
+// The user can provide their own TrackLocal implementations, or use
// the implementations in pkg/media
type TrackLocal interface {
// Bind should implement the way how the media data flows from the Track to the PeerConnection
@@ -67,6 +76,9 @@ type TrackLocal interface {
// and StreamID would be 'desktop' or 'webcam'
ID() string
+ // RID is the RTP Stream ID for this track.
+ RID() string
+
// StreamID is the group this track belongs too. This must be unique
StreamID() string
diff --git a/track_local_static.go b/track_local_static.go
index 4275eb85173..f1ca8b9e070 100644
--- a/track_local_static.go
+++ b/track_local_static.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -24,20 +25,33 @@ type trackBinding struct {
// TrackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets.
// If you wish to send a media.Sample use TrackLocalStaticSample
type TrackLocalStaticRTP struct {
- mu sync.RWMutex
- bindings []trackBinding
- codec RTPCodecCapability
- id, streamID string
+ mu sync.RWMutex
+ bindings []trackBinding
+ codec RTPCodecCapability
+ id, rid, streamID string
}
// NewTrackLocalStaticRTP returns a TrackLocalStaticRTP.
-func NewTrackLocalStaticRTP(c RTPCodecCapability, id, streamID string) (*TrackLocalStaticRTP, error) {
- return &TrackLocalStaticRTP{
+func NewTrackLocalStaticRTP(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticRTP, error) {
+ t := &TrackLocalStaticRTP{
codec: c,
bindings: []trackBinding{},
id: id,
streamID: streamID,
- }, nil
+ }
+
+ for _, option := range options {
+ option(t)
+ }
+
+ return t, nil
+}
+
+// WithRTPStreamID sets the RTP stream ID for this TrackLocalStaticRTP.
+func WithRTPStreamID(rid string) func(*TrackLocalStaticRTP) {
+ return func(t *TrackLocalStaticRTP) {
+ t.rid = rid
+ }
}
// Bind is called by the PeerConnection after negotiation is complete
@@ -86,6 +100,9 @@ func (s *TrackLocalStaticRTP) ID() string { return s.id }
// StreamID is the group this track belongs too. This must be unique
func (s *TrackLocalStaticRTP) StreamID() string { return s.streamID }
+// RID is the RTP stream identifier.
+func (s *TrackLocalStaticRTP) RID() string { return s.rid }
+
// Kind controls if this TrackLocal is audio or video
func (s *TrackLocalStaticRTP) Kind() RTPCodecType {
switch {
@@ -173,8 +190,8 @@ type TrackLocalStaticSample struct {
}
// NewTrackLocalStaticSample returns a TrackLocalStaticSample
-func NewTrackLocalStaticSample(c RTPCodecCapability, id, streamID string) (*TrackLocalStaticSample, error) {
- rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID)
+func NewTrackLocalStaticSample(c RTPCodecCapability, id, streamID string, options ...func(*TrackLocalStaticRTP)) (*TrackLocalStaticSample, error) {
+ rtpTrack, err := NewTrackLocalStaticRTP(c, id, streamID, options...)
if err != nil {
return nil, err
}
@@ -192,6 +209,9 @@ func (s *TrackLocalStaticSample) ID() string { return s.rtpTrack.ID() }
// StreamID is the group this track belongs too. This must be unique
func (s *TrackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() }
+// RID is the RTP stream identifier.
+func (s *TrackLocalStaticSample) RID() string { return s.rtpTrack.RID() }
+
// Kind controls if this TrackLocal is audio or video
func (s *TrackLocalStaticSample) Kind() RTPCodecType { return s.rtpTrack.Kind() }
diff --git a/track_local_static_test.go b/track_local_static_test.go
index 67cd62cb265..d6feebbabf6 100644
--- a/track_local_static_test.go
+++ b/track_local_static_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -22,7 +23,7 @@ func Test_TrackLocalStatic_NoCodecIntersection(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
t.Run("Offerer", func(t *testing.T) {
@@ -93,7 +94,7 @@ func Test_TrackLocalStatic_Closed(t *testing.T) {
_, err = pcAnswer.AddTransceiverFromKind(RTPCodecTypeVideo)
assert.NoError(t, err)
- vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pcOffer.AddTrack(vp8Writer)
@@ -135,7 +136,7 @@ func Test_TrackLocalStatic_PayloadType(t *testing.T) {
answerer, err := NewAPI(WithMediaEngine(mediaEngineTwo)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
- track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = offerer.AddTransceiverFromKind(RTPCodecTypeVideo)
@@ -171,7 +172,7 @@ func Test_TrackLocalStatic_Mutate_Input(t *testing.T) {
pcOffer, pcAnswer, err := newPair()
assert.NoError(t, err)
- vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pcOffer.AddTrack(vp8Writer)
@@ -203,7 +204,7 @@ func Test_TrackLocalStatic_Binding_NonBlocking(t *testing.T) {
_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo)
assert.NoError(t, err)
- vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ vp8Writer, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = pcAnswer.AddTrack(vp8Writer)
@@ -231,7 +232,7 @@ func BenchmarkTrackLocalWrite(b *testing.B) {
b.Fatalf("Failed to create a PC pair for testing")
}
- track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion")
+ track, err := NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(b, err)
_, err = offerPC.AddTrack(track)
diff --git a/track_remote.go b/track_remote.go
index 6733b7cfbed..e98620c31a1 100644
--- a/track_remote.go
+++ b/track_remote.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
@@ -116,16 +117,48 @@ func (t *TrackRemote) Read(b []byte) (n int, attributes interceptor.Attributes,
// released the lock. Deal with it.
if data != nil {
n = copy(b, data)
+ err = t.checkAndUpdateTrack(b)
return
}
}
- return r.readRTP(b, t)
+ n, attributes, err = r.readRTP(b, t)
+ if err != nil {
+ return
+ }
+
+ err = t.checkAndUpdateTrack(b)
+ return
+}
+
+// checkAndUpdateTrack checks payloadType for every incoming packet
+// once a different payloadType is detected the track will be updated
+func (t *TrackRemote) checkAndUpdateTrack(b []byte) error {
+ if len(b) < 2 {
+ return errRTPTooShort
+ }
+
+ if payloadType := PayloadType(b[1] & rtpPayloadTypeBitmask); payloadType != t.PayloadType() {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ params, err := t.receiver.api.mediaEngine.getRTPParametersByPayloadType(payloadType)
+ if err != nil {
+ return err
+ }
+
+ t.kind = t.receiver.kind
+ t.payloadType = payloadType
+ t.codec = params.Codecs[0]
+ t.params = params
+ }
+
+ return nil
}
// ReadRTP is a convenience method that wraps Read and unmarshals for you.
func (t *TrackRemote) ReadRTP() (*rtp.Packet, interceptor.Attributes, error) {
- b := make([]byte, receiveMTU)
+ b := make([]byte, t.receiver.api.settingEngine.getReceiveMTU())
i, attributes, err := t.Read(b)
if err != nil {
return nil, nil, err
@@ -138,26 +171,6 @@ func (t *TrackRemote) ReadRTP() (*rtp.Packet, interceptor.Attributes, error) {
return r, attributes, nil
}
-// determinePayloadType blocks and reads a single packet to determine the PayloadType for this Track
-// this is useful because we can't announce it to the user until we know the payloadType
-func (t *TrackRemote) determinePayloadType() error {
- b := make([]byte, receiveMTU)
- n, _, err := t.peek(b)
- if err != nil {
- return err
- }
- r := rtp.Packet{}
- if err := r.Unmarshal(b[:n]); err != nil {
- return err
- }
-
- t.mu.Lock()
- t.payloadType = PayloadType(r.PayloadType)
- defer t.mu.Unlock()
-
- return nil
-}
-
// peek is like Read, but it doesn't discard the packet read
func (t *TrackRemote) peek(b []byte) (n int, a interceptor.Attributes, err error) {
n, a, err = t.Read(b)
diff --git a/track_test.go b/track_test.go
index 9e01a698d04..48d8ea294ce 100644
--- a/track_test.go
+++ b/track_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc
diff --git a/vnet_test.go b/vnet_test.go
index 14e8372c3c7..11bdb4bd24b 100644
--- a/vnet_test.go
+++ b/vnet_test.go
@@ -1,3 +1,4 @@
+//go:build !js
// +build !js
package webrtc