mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-27 04:46:10 +08:00
Compare commits
372 Commits
v0.1.1
...
renovate/g
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dc3668a627 | ||
![]() |
cb394eb4c5 | ||
![]() |
e9f3dc20b6 | ||
![]() |
0710906fc7 | ||
![]() |
7fdafa9598 | ||
![]() |
5a19127623 | ||
![]() |
8ca6903676 | ||
![]() |
de517d790b | ||
![]() |
81cfc047d5 | ||
![]() |
1406108fb2 | ||
![]() |
a2a211857c | ||
![]() |
c0721738c4 | ||
![]() |
6047a32ea0 | ||
![]() |
60bf158757 | ||
![]() |
c4fd28c7df | ||
![]() |
c79e16706b | ||
![]() |
89420ae84d | ||
![]() |
4db71e5b52 | ||
![]() |
a45a5e50cd | ||
![]() |
d90220699e | ||
![]() |
71deb52047 | ||
![]() |
84ccb15157 | ||
![]() |
ec6a4b6925 | ||
![]() |
551fb6afd8 | ||
![]() |
20e8c50735 | ||
![]() |
7211d077ee | ||
![]() |
cd5f8eb43a | ||
![]() |
2d7bdd4e24 | ||
![]() |
5cad3f1b41 | ||
![]() |
9d5e9cb3ea | ||
![]() |
9a47a07eba | ||
![]() |
4c70a5f686 | ||
![]() |
36a03e823e | ||
![]() |
24e3a722cf | ||
![]() |
ce9b412d0e | ||
![]() |
8f916c5c67 | ||
![]() |
c10fb000db | ||
![]() |
309b50a801 | ||
![]() |
6f6c42a695 | ||
![]() |
4027590fcb | ||
![]() |
5ac53a463c | ||
![]() |
f2a550d5e2 | ||
![]() |
9ea1754cc7 | ||
![]() |
bc0b11e3bf | ||
![]() |
ac66b130b9 | ||
![]() |
cf1be6fd31 | ||
![]() |
6339a1890f | ||
![]() |
6a00ffcdf8 | ||
![]() |
c6aa90a133 | ||
![]() |
e3eae5d8db | ||
![]() |
7020457f63 | ||
![]() |
e3fef141d9 | ||
![]() |
8fb8d65764 | ||
![]() |
ae63fa65bf | ||
![]() |
23d77e2bb2 | ||
![]() |
02d4e0e896 | ||
![]() |
38a3b829f6 | ||
![]() |
c4bb9eb649 | ||
![]() |
bc3bdc1855 | ||
![]() |
a6ffeb31ab | ||
![]() |
a9046e6ac1 | ||
![]() |
2bc011f39f | ||
![]() |
68df5b3eb5 | ||
![]() |
bf52b1af58 | ||
![]() |
ccee17d04c | ||
![]() |
f4f5a24ce4 | ||
![]() |
dec2300a95 | ||
![]() |
0c8f3cfc7a | ||
![]() |
6829d71e58 | ||
![]() |
1b36c0360d | ||
![]() |
5aa157e8fc | ||
![]() |
cd8e34f3ed | ||
![]() |
d03383a6fd | ||
![]() |
c4fd1de9a0 | ||
![]() |
47638e3290 | ||
![]() |
09b497727d | ||
![]() |
7ae82fbda7 | ||
![]() |
4477898296 | ||
![]() |
d5d41e9ca5 | ||
![]() |
9fb24fb036 | ||
![]() |
1cd1b136cc | ||
![]() |
08fb3e8a48 | ||
![]() |
4aae1bc842 | ||
![]() |
3c9fee958e | ||
![]() |
68092afe36 | ||
![]() |
cd92becd1c | ||
![]() |
2bad953124 | ||
![]() |
634addc04d | ||
![]() |
d0c1677cfb | ||
![]() |
b4770e5fbf | ||
![]() |
855441dad1 | ||
![]() |
2882fd42d5 | ||
![]() |
694b4abd83 | ||
![]() |
afb2f78e3c | ||
![]() |
b14ce7987c | ||
![]() |
728207526d | ||
![]() |
3582e5d017 | ||
![]() |
774b7de8d2 | ||
![]() |
96efc0932e | ||
![]() |
a1b4c0f69a | ||
![]() |
6bba5f1663 | ||
![]() |
79aef00e07 | ||
![]() |
03c44ee803 | ||
![]() |
dad145ef11 | ||
![]() |
72c1d7bb89 | ||
![]() |
fc301a8a92 | ||
![]() |
aca3ee9126 | ||
![]() |
4924411e88 | ||
![]() |
b88f541c4f | ||
![]() |
09f6bdeac4 | ||
![]() |
e64f0d8697 | ||
![]() |
36f908c6e2 | ||
![]() |
9987e01d3f | ||
![]() |
ae173b1b61 | ||
![]() |
4eea55285e | ||
![]() |
18cf1fe38a | ||
![]() |
138499b52d | ||
![]() |
87486146d5 | ||
![]() |
cadb155755 | ||
![]() |
2372f55064 | ||
![]() |
8568b1b20d | ||
![]() |
f0f6be7350 | ||
![]() |
8146e84f2f | ||
![]() |
4ff24bd656 | ||
![]() |
64dbe507f0 | ||
![]() |
dffaf0fcb4 | ||
![]() |
d3adaeea1a | ||
![]() |
7a414948c6 | ||
![]() |
e1616b8cc2 | ||
![]() |
52a080b55a | ||
![]() |
c2fe66c579 | ||
![]() |
ac50077e77 | ||
![]() |
2f5c61e1f3 | ||
![]() |
0715258726 | ||
![]() |
bccff100e5 | ||
![]() |
0507093a59 | ||
![]() |
bf290b026c | ||
![]() |
0dc4f43c94 | ||
![]() |
14bfaa5dbd | ||
![]() |
09c31a264c | ||
![]() |
30badd819d | ||
![]() |
dc8aeea11f | ||
![]() |
57c9ba0fc5 | ||
![]() |
b9ce5bb861 | ||
![]() |
e4ac96ea6b | ||
![]() |
11bf55f80c | ||
![]() |
55881ddd41 | ||
![]() |
62009a882b | ||
![]() |
dbd37689e4 | ||
![]() |
d561715bf9 | ||
![]() |
76ba048312 | ||
![]() |
5da0ebf443 | ||
![]() |
f8f8511d94 | ||
![]() |
85597da5bb | ||
![]() |
6ac1424488 | ||
![]() |
73a158d097 | ||
![]() |
cb23f1fa82 | ||
![]() |
a0f090dced | ||
![]() |
4f7542b614 | ||
![]() |
860f4a1490 | ||
![]() |
09f3bcc013 | ||
![]() |
f0ad407da8 | ||
![]() |
8d7ccada1c | ||
![]() |
2d9208de5b | ||
![]() |
52cf6e72b1 | ||
![]() |
7335797301 | ||
![]() |
3bec69bbf8 | ||
![]() |
58dc90d03a | ||
![]() |
8ad810e61e | ||
![]() |
6f204fa3d1 | ||
![]() |
5215057409 | ||
![]() |
3bcbed0286 | ||
![]() |
907e0d68e2 | ||
![]() |
9fd2d01dbe | ||
![]() |
285f8cd23c | ||
![]() |
b309c30ca0 | ||
![]() |
601f27c014 | ||
![]() |
2a04a14225 | ||
![]() |
416bbc33f3 | ||
![]() |
4a682a48c1 | ||
![]() |
14db2b8130 | ||
![]() |
a3c15d1fb0 | ||
![]() |
43272ea965 | ||
![]() |
e32fc1bdb8 | ||
![]() |
2af325d1a5 | ||
![]() |
5e0df5e5cf | ||
![]() |
d038133783 | ||
![]() |
cd6aaa1393 | ||
![]() |
82cc32308b | ||
![]() |
8fce8a2bb5 | ||
![]() |
5b99500290 | ||
![]() |
e371c0d955 | ||
![]() |
69f9cbe008 | ||
![]() |
b5acc5d7f6 | ||
![]() |
55e65027f9 | ||
![]() |
f0ff9261b4 | ||
![]() |
08a396571f | ||
![]() |
0d09f7f458 | ||
![]() |
e780bdc6f9 | ||
![]() |
ff18b21629 | ||
![]() |
eaf9ff42a8 | ||
![]() |
5ba49e03e7 | ||
![]() |
1250e06923 | ||
![]() |
651c847674 | ||
![]() |
3b2316081e | ||
![]() |
70261260cb | ||
![]() |
548cdac668 | ||
![]() |
79f9fc31f6 | ||
![]() |
1f92ea40da | ||
![]() |
4beb7e5a23 | ||
![]() |
9bb5755cd2 | ||
![]() |
e316b30964 | ||
![]() |
596b8c4e11 | ||
![]() |
be5f684ea6 | ||
![]() |
a88c2daf89 | ||
![]() |
1f313a9d61 | ||
![]() |
19eaf375ff | ||
![]() |
b3c94a1f7b | ||
![]() |
86cb9f8ce8 | ||
![]() |
f8d1f974cf | ||
![]() |
809f74cafc | ||
![]() |
eb2db82766 | ||
![]() |
b4c6eb5409 | ||
![]() |
b263026d52 | ||
![]() |
070ab924f9 | ||
![]() |
23177a5d75 | ||
![]() |
3a04686875 | ||
![]() |
5f95b84719 | ||
![]() |
8919ba4fe5 | ||
![]() |
153c36e461 | ||
![]() |
10769b702e | ||
![]() |
2948735964 | ||
![]() |
ba848b3416 | ||
![]() |
94c6b66e46 | ||
![]() |
96fd92142c | ||
![]() |
d71b72c64d | ||
![]() |
8c2c8a9b27 | ||
![]() |
1e03f61b4b | ||
![]() |
b863c105c8 | ||
![]() |
6411b00e93 | ||
![]() |
acd2cb992b | ||
![]() |
dafd208de7 | ||
![]() |
fcec5a9149 | ||
![]() |
3d3830f7ff | ||
![]() |
655b513810 | ||
![]() |
eaaaacfc6b | ||
![]() |
fa95e47bad | ||
![]() |
020de77bc9 | ||
![]() |
33b6412c26 | ||
![]() |
f29d08ae6b | ||
![]() |
b5b0653697 | ||
![]() |
a1087f7f4e | ||
![]() |
217e634f7e | ||
![]() |
60b8e3ae1b | ||
![]() |
7df3114cdc | ||
![]() |
9741508d2b | ||
![]() |
7a569f0901 | ||
![]() |
ee40fcd070 | ||
![]() |
499c08d513 | ||
![]() |
5a1bd11087 | ||
![]() |
7ce935eac8 | ||
![]() |
a359005a7d | ||
![]() |
ca4116b5ce | ||
![]() |
8a4e0779d7 | ||
![]() |
d222ff3d74 | ||
![]() |
b9bb4fdc34 | ||
![]() |
cc823958e1 | ||
![]() |
64f39187b8 | ||
![]() |
c56cc487a3 | ||
![]() |
d86fc4a3e9 | ||
![]() |
2f21d9e738 | ||
![]() |
3316476b30 | ||
![]() |
0b1a19f343 | ||
![]() |
ad37c826b9 | ||
![]() |
d84d0a3b0c | ||
![]() |
c068f1176d | ||
![]() |
76908aca89 | ||
![]() |
f3ac1f5480 | ||
![]() |
6280b450ba | ||
![]() |
db0884a77c | ||
![]() |
1b5203d3a0 | ||
![]() |
f472618b71 | ||
![]() |
7f4d1bc5ad | ||
![]() |
97046bc6ec | ||
![]() |
7bcc9111f4 | ||
![]() |
044b5566d1 | ||
![]() |
7f41f9b8df | ||
![]() |
5d5001d0b4 | ||
![]() |
c734c53b00 | ||
![]() |
92ac89a620 | ||
![]() |
ce032411a7 | ||
![]() |
3ea7120130 | ||
![]() |
282d0a4fb4 | ||
![]() |
356eee19ce | ||
![]() |
02d0cd3f44 | ||
![]() |
a8dbecff30 | ||
![]() |
273457b370 | ||
![]() |
9846a7eb67 | ||
![]() |
b2e72af884 | ||
![]() |
b8dd3811ac | ||
![]() |
c1958b62a2 | ||
![]() |
ea90f86abd | ||
![]() |
716da16e4a | ||
![]() |
1550a68003 | ||
![]() |
d65170dfe3 | ||
![]() |
4057524bf0 | ||
![]() |
8dd84b269c | ||
![]() |
a73b1922ed | ||
![]() |
11aea3eb85 | ||
![]() |
cd49cd9910 | ||
![]() |
6900da9a5e | ||
![]() |
0a1944dc77 | ||
![]() |
c3100355e5 | ||
![]() |
b35246730d | ||
![]() |
0c61817369 | ||
![]() |
2fe26ea1f7 | ||
![]() |
9d98eb8aaf | ||
![]() |
3ea35bebab | ||
![]() |
83c08e6c5f | ||
![]() |
2f17017450 | ||
![]() |
7cbda134b0 | ||
![]() |
115be126ec | ||
![]() |
79dcb4f1af | ||
![]() |
5db4007e73 | ||
![]() |
77ebcecac6 | ||
![]() |
a0d0949954 | ||
![]() |
f396092609 | ||
![]() |
ee6cf08c44 | ||
![]() |
6a211aa19f | ||
![]() |
b089610c27 | ||
![]() |
1d34ec9c5d | ||
![]() |
7bd3efc8b7 | ||
![]() |
8396fd7aac | ||
![]() |
3787158dba | ||
![]() |
640eeb0cc0 | ||
![]() |
16ceb45c25 | ||
![]() |
c98b3b0909 | ||
![]() |
e6c98a844f | ||
![]() |
2a70c031b8 | ||
![]() |
047013be95 | ||
![]() |
765318feb6 | ||
![]() |
af6d31fde5 | ||
![]() |
2f5e4ee914 | ||
![]() |
1720eee38c | ||
![]() |
00877c74a0 | ||
![]() |
559c6a13a1 | ||
![]() |
f4a4edcabd | ||
![]() |
c8547c4597 | ||
![]() |
21bb12dd6b | ||
![]() |
fd43659fed | ||
![]() |
82f33cb572 | ||
![]() |
4f9822349a | ||
![]() |
16bcd0b7dd | ||
![]() |
2022a4b7f7 | ||
![]() |
0b6549eb8f | ||
![]() |
1b0a237438 | ||
![]() |
36edbd9485 | ||
![]() |
eb689a3c79 | ||
![]() |
e4b1b1aaba | ||
![]() |
0f5df05c16 | ||
![]() |
9dcfaf1c1e | ||
![]() |
238f190e71 | ||
![]() |
0210ec6ca6 | ||
![]() |
abdd96e6b2 | ||
![]() |
c9779e7f73 | ||
![]() |
5703fd7e4b | ||
![]() |
db5d8f23bd | ||
![]() |
d6ba28af8c | ||
![]() |
09c2998408 | ||
![]() |
d129e982c7 | ||
![]() |
74986c010b | ||
![]() |
b8be865ff3 |
78
.github/workflows/ci.yaml
vendored
78
.github/workflows/ci.yaml
vendored
@@ -11,14 +11,18 @@ jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ '1.15', '1.14' ]
|
||||
go:
|
||||
- '1.21' # oldest version this package supports
|
||||
- '1.22' # oldstable Go version
|
||||
- '1.23' # stable Go version
|
||||
name: Linux Go ${{ matrix.go }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install dependencies
|
||||
@@ -28,52 +32,54 @@ jobs:
|
||||
libopus-dev \
|
||||
libva-dev \
|
||||
libvpx-dev \
|
||||
libx264-dev
|
||||
- name: go vet
|
||||
run: go vet -tags nolibopusfile ./...
|
||||
- name: go build
|
||||
run: go build -tags nolibopusfile ./...
|
||||
- name: go build without CGO
|
||||
run: go build . pkg/...
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: go test
|
||||
run: go test -tags nolibopusfile ./... -v -race
|
||||
- name: go test without CGO
|
||||
run: go test . pkg/... -v
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
libx11-dev \
|
||||
libx264-dev \
|
||||
libxext-dev
|
||||
- name: Run Test Suite
|
||||
run: make test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
build-darwin:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ '1.15', '1.14' ]
|
||||
go:
|
||||
- '1.22'
|
||||
- '1.23'
|
||||
runs-on: macos-latest
|
||||
name: Darwin Go ${{ matrix.go }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
which brew
|
||||
brew install \
|
||||
pkg-config \
|
||||
opus \
|
||||
libvpx \
|
||||
x264
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
- name: go build
|
||||
run: go build ./...
|
||||
- name: go build without CGO
|
||||
run: go build . pkg/...
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: go test
|
||||
run: go test ./... -v -race
|
||||
- name: go test without CGO
|
||||
run: go test . pkg/... -v
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: Run Test Suite
|
||||
run: make test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
check-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check Licenses
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Installing go-licenses
|
||||
run: go install github.com/google/go-licenses@latest
|
||||
- name: Checking licenses
|
||||
run: go-licenses check ./...
|
||||
|
2
.github/workflows/renovate-go-mod-fix.yaml
vendored
2
.github/workflows/renovate-go-mod-fix.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: fix
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,3 +10,8 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
scripts/cross
|
||||
coverage.txt
|
||||
|
||||
.idea
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "cvendor/src/openh264"]
|
||||
path = cvendor/src/openh264
|
||||
url = https://github.com/cisco/openh264.git
|
||||
|
@@ -1 +0,0 @@
|
||||
* @lherman-cs @at-wat
|
127
Makefile
127
Makefile
@@ -1,56 +1,83 @@
|
||||
vendor_dir = cvendor
|
||||
src_dir = $(vendor_dir)/src
|
||||
lib_dir = $(vendor_dir)/lib
|
||||
include_dir = $(vendor_dir)/include
|
||||
docker_owner := lherman
|
||||
docker_prefix := cross
|
||||
toolchain_dockerfiles := dockerfiles
|
||||
script_path := $(realpath scripts)
|
||||
toolchain_path := $(script_path)/$(docker_prefix)
|
||||
os_list := \
|
||||
linux \
|
||||
windows \
|
||||
darwin
|
||||
arch_list := \
|
||||
armv7 \
|
||||
arm64 \
|
||||
x64
|
||||
supported_platforms := \
|
||||
linux-armv7 \
|
||||
linux-arm64 \
|
||||
linux-x64 \
|
||||
windows-x64 \
|
||||
darwin-x64 \
|
||||
darwin-arm64
|
||||
cmd_build := build
|
||||
cmd_test := test
|
||||
examples_dir := examples
|
||||
codec_dir := pkg/codec
|
||||
codec_list := $(shell ls $(codec_dir)/*/Makefile)
|
||||
codec_list := $(codec_list:$(codec_dir)/%/Makefile=%)
|
||||
targets := $(foreach codec, $(codec_list), $(addprefix $(cmd_build)-$(codec)-, $(supported_platforms)))
|
||||
pkgs_without_ext_device := $(shell go list ./... | grep -v mmal | grep -v vaapi)
|
||||
pkgs_without_cgo := $(shell go list ./... | grep -v pkg/codec | grep -v pkg/driver | grep -v pkg/avfoundation)
|
||||
|
||||
make_args.x86_64-windows = \
|
||||
CC=x86_64-w64-mingw32-gcc \
|
||||
CXX=x86_64-w64-mingw32-g++ \
|
||||
ARCH=x86_64 \
|
||||
OS=mingw_nt
|
||||
make_args.x86_64-darwin = \
|
||||
CC=o64-clang \
|
||||
CXX=o64-clang++ \
|
||||
AR=llvm-ar \
|
||||
ARCH=x86_64 \
|
||||
OS=darwin
|
||||
define BUILD_TEMPLATE
|
||||
ifneq (,$$(findstring $(2)-$(3),$$(supported_platforms)))
|
||||
$$(cmd_build)-$(1)-$(2)-$(3): toolchain-$(2)-$(3)
|
||||
$$(MAKE) --directory=$$(codec_dir)/$(1) \
|
||||
MEDIADEVICES_TOOLCHAIN_BIN=$$(toolchain_path)/$(docker_prefix)-$(2)-$(3) \
|
||||
MEDIADEVICES_TARGET_PLATFORM=$(2)-$(3) \
|
||||
MEDIADEVICES_TARGET_OS=$(2) \
|
||||
MEDIADEVICES_TARGET_ARCH=$(3)
|
||||
endif
|
||||
endef
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: \
|
||||
$(include_dir)/openh264 \
|
||||
cross-libraries
|
||||
.PHONY: all
|
||||
all: $(cmd_test) $(cmd_build)
|
||||
|
||||
$(include_dir)/openh264: $(src_dir)/openh264
|
||||
mkdir -p $@
|
||||
cp $^/codec/api/svc/*.h $@
|
||||
# Subcommand:
|
||||
# make build[-<codec_name>-<os>-<arch>]
|
||||
#
|
||||
# Description:
|
||||
# Build codec dependencies to multiple platforms.
|
||||
#
|
||||
# Examples:
|
||||
# * make build: build all codecs for all supported platforms
|
||||
# * make build-opus-darwin-x64: only build opus for darwin-x64 platform
|
||||
$(cmd_build): $(targets)
|
||||
|
||||
$(lib_dir)/openh264/libopenh264.x86_64-linux.a: $(src_dir)/openh264
|
||||
$(MAKE) -C $^ clean \
|
||||
&& $(MAKE) -C $^ libraries
|
||||
mkdir -p $(dir $@)
|
||||
cp $^/libopenh264.a $@
|
||||
toolchain-%: $(toolchain_dockerfiles)
|
||||
$(MAKE) --directory=$< "$*" \
|
||||
MEDIADEVICES_DOCKER_OWNER=$(docker_owner) \
|
||||
MEDIADEVICES_DOCKER_PREFIX=$(docker_prefix)
|
||||
@mkdir -p $(toolchain_path)
|
||||
@docker run $(docker_owner)/$(docker_prefix)-$* > \
|
||||
$(toolchain_path)/$(docker_prefix)-$*
|
||||
@chmod +x $(toolchain_path)/$(docker_prefix)-$*
|
||||
|
||||
$(lib_dir)/openh264/libopenh264.x86_64-windows.a: $(src_dir)/openh264
|
||||
$(MAKE) -C $^ clean \
|
||||
&& $(MAKE) -C $^ $(make_args.x86_64-windows) libraries
|
||||
mkdir -p $(dir $@)
|
||||
cp $^/libopenh264.a $@
|
||||
$(foreach codec, $(codec_list), \
|
||||
$(foreach os, $(os_list), \
|
||||
$(foreach arch, $(arch_list), \
|
||||
$(eval $(call BUILD_TEMPLATE,$(codec),$(os),$(arch))))))
|
||||
|
||||
$(lib_dir)/openh264/libopenh264.x86_64-darwin.a: $(src_dir)/openh264
|
||||
$(MAKE) -C $^ clean \
|
||||
&& $(MAKE) -C $^ $(make_args.x86_64-darwin) libraries
|
||||
mkdir -p $(dir $@)
|
||||
cp $^/libopenh264.a $@
|
||||
|
||||
.PHONY: cross-libraries
|
||||
cross-libraries:
|
||||
docker build -t mediadevices-libs-builder -f libs-builder.Dockerfile .
|
||||
docker run --rm \
|
||||
-v $(CURDIR):/go/src/github.com/pion/mediadevices \
|
||||
mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-linux.a
|
||||
docker run --rm \
|
||||
-v $(CURDIR):/go/src/github.com/pion/mediadevices \
|
||||
mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-windows.a
|
||||
docker run --rm \
|
||||
-v $(CURDIR):/go/src/github.com/pion/mediadevices \
|
||||
mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-darwin.a
|
||||
# Subcommand:
|
||||
# make test
|
||||
#
|
||||
# Description:
|
||||
# Run a series of tests
|
||||
$(cmd_test):
|
||||
go vet $(pkgs_without_ext_device)
|
||||
go build $(pkgs_without_ext_device)
|
||||
# go build without CGO
|
||||
CGO_ENABLED=0 go build $(pkgs_without_cgo)
|
||||
# go build with CGO
|
||||
CGO_ENABLED=1 go build $(pkgs_without_ext_device)
|
||||
$(MAKE) --directory=$(examples_dir)
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(pkgs_without_ext_device)
|
||||
|
261
README.md
261
README.md
@@ -1,75 +1,232 @@
|
||||
# mediadevices
|
||||
<h1 align="center">
|
||||
<br>
|
||||
Pion MediaDevices
|
||||
<br>
|
||||
</h1>
|
||||
<h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4>
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a> <a href="https://twitter.com/_pion?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40_pion&style=social&url=https%3A%2F%2Ftwitter.com%2F_pion" alt="Twitter Widget"></a>
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/mediadevices/test.yaml">
|
||||
<a href="https://pkg.go.dev/github.com/pion/mediadevices"><img src="https://pkg.go.dev/badge/github.com/pion/mediadevices.svg" alt="Go Reference"></a>
|
||||
<a href="https://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
Go implementation of the [MediaDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) API.
|
||||
`mediadevices` provides access to media input devices like cameras, microphones, and screen capture. It can also be used to encode your video/audio stream to various codec selections. `mediadevices` abstracts away the complexities of interacting with things like hardware and codecs allowing you to focus on building appilcations, interacting only with an amazingly simple, easy, and elegant API!
|
||||
|
||||

|
||||
### Install
|
||||
|
||||
## Interfaces
|
||||
```bash
|
||||
go get -u github.com/pion/mediadevices
|
||||
```
|
||||
|
||||
| Interface | Linux | Mac | Windows |
|
||||
### Usage
|
||||
|
||||
The following snippet shows how to capture a camera stream and store a frame as a jpeg image:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/jpeg"
|
||||
"os"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
|
||||
// This is required to register camera adapter
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera"
|
||||
// Note: If you don't have a camera or your adapters are not supported,
|
||||
// you can always swap your adapters with our dummy adapters below.
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
)
|
||||
|
||||
func main() {
|
||||
stream, _ := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(constraint *mediadevices.MediaTrackConstraints) {
|
||||
// Query for ideal resolutions
|
||||
constraint.Width = prop.Int(600)
|
||||
constraint.Height = prop.Int(400)
|
||||
},
|
||||
})
|
||||
|
||||
// Since track can represent audio as well, we need to cast it to
|
||||
// *mediadevices.VideoTrack to get video specific functionalities
|
||||
track := stream.GetVideoTracks()[0]
|
||||
videoTrack := track.(*mediadevices.VideoTrack)
|
||||
defer videoTrack.Close()
|
||||
|
||||
// Create a new video reader to get the decoded frames. Release is used
|
||||
// to return the buffer to hold frame back to the source so that the buffer
|
||||
// can be reused for the next frames.
|
||||
videoReader := videoTrack.NewReader(false)
|
||||
frame, release, _ := videoReader.Read()
|
||||
defer release()
|
||||
|
||||
// Since frame is the standard image.Image, it's compatible with Go standard
|
||||
// library. For example, capturing the first frame and store it as a jpeg image.
|
||||
output, _ := os.Create("frame.jpg")
|
||||
jpeg.Encode(output, frame, nil)
|
||||
}
|
||||
```
|
||||
|
||||
### More Examples
|
||||
* [Webrtc](/examples/webrtc) - Use Webrtc to create a realtime peer-to-peer video call
|
||||
* [Face Detection](/examples/facedetection) - Use a machine learning algorithm to detect faces in a camera stream
|
||||
* [RTP Stream](examples/rtp) - Capture camera stream, encode it in H264/VP8/VP9, and send it to a RTP server
|
||||
* [HTTP Broadcast](/examples/http) - Broadcast camera stream through HTTP with MJPEG
|
||||
* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera
|
||||
|
||||
### Available Media Inputs
|
||||
| Input | Linux | Mac | Windows |
|
||||
| :--------: | :---: | :-: | :-----: |
|
||||
| Camera | ✔️ | ✔️ | ✔️ |
|
||||
| Microphone | ✔️ | ✖️ | ✔️ |
|
||||
| Screen | ✔️ | ✖️ | ✖️ |
|
||||
| Camera | ✔️ | ✔️ | ✔️ |
|
||||
| Microphone | ✔️ | ✔️ | ✔️ |
|
||||
| Screen | ✔️ | ✔️ | ✔️ |
|
||||
|
||||
### Camera
|
||||
By default, there's no media input registered. This decision was made to allow you to play only what you need. Therefore, you need to import the associated packages for the media inputs. For example, if you want to use a camera, you need to import the camera package as a side effect:
|
||||
|
||||
| OS | Library/Interface |
|
||||
| :-----: | :---------------------------------------------------------------------: |
|
||||
| Linux | [Video4Linux](https://en.wikipedia.org/wiki/Video4Linux) |
|
||||
| Mac | [AVFoundation](https://developer.apple.com/av-foundation/) |
|
||||
| Windows | [DirectShow](https://docs.microsoft.com/en-us/windows/win32/directshow) |
|
||||
```go
|
||||
import (
|
||||
...
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera"
|
||||
)
|
||||
```
|
||||
|
||||
| Pixel Format | Linux | Mac | Windows |
|
||||
| :---------------------------------------------------: | :---: | :-: | :-----: |
|
||||
| [YUY2](https://www.fourcc.org/pixel-format/yuv-yuy2/) | ✔️ | ✖️ | ✔️ |
|
||||
| [UYVY](https://www.fourcc.org/pixel-format/yuv-uyvy/) | ✔️ | ✔️ | ✖️ |
|
||||
| [I420](https://www.fourcc.org/pixel-format/yuv-i420/) | ✔️ | ✖️ | ✖️ |
|
||||
| [NV21](https://www.fourcc.org/pixel-format/yuv-nv21/) | ✔️ | ✔️ | ✖️ |
|
||||
| [MJPEG](https://www.fourcc.org/mjpg/) | ✔️ | ✖️ | ✖️ |
|
||||
### Available Codecs
|
||||
In order to encode your video/audio, `mediadevices` needs to know what codecs that you want to use and their parameters. To do this, you need to import the associated packages for the codecs, and add them to the codec selector that you'll pass to `GetUserMedia`:
|
||||
|
||||
### Microphone
|
||||
```go
|
||||
package main
|
||||
|
||||
| OS | Library/Interface |
|
||||
| :-----: | :---------------------------------------------------------------------: |
|
||||
| Linux | [PulseAudio](https://en.wikipedia.org/wiki/PulseAudio) |
|
||||
| Mac | N/A |
|
||||
| Windows | [waveIn](https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/) |
|
||||
import (
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
)
|
||||
|
||||
### Screen casting
|
||||
func main() {
|
||||
// configure codec specific parameters
|
||||
x264Params, _ := x264.NewParams()
|
||||
x264Params.Preset = x264.PresetMedium
|
||||
x264Params.BitRate = 1_000_000 // 1mbps
|
||||
|
||||
| OS | Library/Interface |
|
||||
| :-----: | :---------------------------------------------------------------------: |
|
||||
| Linux | [X11](https://en.wikipedia.org/wiki/X_Window_System) |
|
||||
| Mac | N/A |
|
||||
| Windows | N/A |
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
)
|
||||
|
||||
mediaStream, _ := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {},
|
||||
Codec: codecSelector, // let GetUsermedia know available codecs
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Codecs
|
||||
Since `mediadevices` doesn't implement the video/audio codecs, it needs to call the codec libraries from the system through cgo. Therefore, you're required to install the codec libraries before you can use them in `mediadevices`. In the next section, it shows a list of available codecs, where the packages are defined (documentation linked), and installation instructions.
|
||||
|
||||
| Audio Codec | Library/Interface |
|
||||
| :---------: | :------------------------------------------------------: |
|
||||
| OPUS | [libopus](http://opus-codec.org/) |
|
||||
Note: we do not provide recommendations on choosing one codec or another as it is very complex and can be subjective.
|
||||
|
||||
| Video Codec | Library/Interface |
|
||||
| :---------: | :------------------------------------------------------: |
|
||||
| H.264 | [OpenH264](https://www.openh264.org/) |
|
||||
| VP8 | [libvpx](https://www.webmproject.org/code/) |
|
||||
| VP9 | [libvpx](https://www.webmproject.org/code/) |
|
||||
#### Video Codecs
|
||||
|
||||
## Usage
|
||||
##### x264
|
||||
A free software library and application for encoding video streams into the H.264/MPEG-4 AVC compression format.
|
||||
|
||||
[Wiki](https://github.com/pion/mediadevices/wiki)
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/x264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/x264)
|
||||
* Installation:
|
||||
* Mac: `brew install x264`
|
||||
* Ubuntu: `apt install libx264-dev`
|
||||
|
||||
##### mmal
|
||||
A framework to enable H264 hardware encoding for Raspberry Pi or boards that use VideoCore GPUs.
|
||||
|
||||
## Contributing
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/mmal](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/mmal)
|
||||
* Installation: no installation needed, mmal should come built in Raspberry Pi devices
|
||||
|
||||
- [Lukas Herman](https://github.com/lherman-cs) - _Original Author_
|
||||
* [Atsushi Watanabe](https://github.com/at-wat) - _VP8, Screencast, etc._
|
||||
##### openh264
|
||||
A codec library which supports H.264 encoding and decoding. It is suitable for use in real time applications.
|
||||
|
||||
## Project Status
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/openh264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/openh264)
|
||||
* Installation: no installation needed, included as a static binary
|
||||
|
||||
[](https://starchart.cc/pion/mediadevices)
|
||||
##### vpx
|
||||
A free software video codec library from Google and the Alliance for Open Media that implements VP8/VP9 video coding formats.
|
||||
|
||||
## References
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/vpx](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vpx)
|
||||
* Installation:
|
||||
* Mac: `brew install libvpx`
|
||||
* Ubuntu: `apt install libvpx-dev`
|
||||
|
||||
##### vaapi
|
||||
An open source API that allows applications such as VLC media player or GStreamer to use hardware video acceleration capabilities (currently support VP8/VP9).
|
||||
|
||||
- https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs
|
||||
- https://tools.ietf.org/html/rfc7742
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/vaapi](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vaapi)
|
||||
* Installation:
|
||||
* Ubuntu: `apt install libva-dev`
|
||||
|
||||
|
||||
#### Audio Codecs
|
||||
|
||||
##### opus
|
||||
A totally open, royalty-free, highly versatile audio codec.
|
||||
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/opus](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/opus)
|
||||
* Installation:
|
||||
* Mac: `brew install opus`
|
||||
* Ubuntu: `apt install libopus-dev`
|
||||
|
||||
### Benchmark
|
||||
Result as of Nov 4, 2020 with Go 1.14 on a Raspberry pi 3, `mediadevices` can produce video, encode, send across network, and decode at **720p, 30 fps with < 500 ms latency**.
|
||||
|
||||
The test was taken by capturing a camera stream, decoding the raw frames, encoding the video stream with mmal, and sending the stream through Webrtc.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### Failed to find the best driver that fits the constraints
|
||||
`mediadevices` provides an automated driver discovery through `GetUserMedia` and `GetDisplayMedia`. The driver discover algorithm works something like:
|
||||
|
||||
1. Open all registered drivers
|
||||
2. Get all properties (property describes what a driver is capable of, e.g. resolution, frame rate, etc.) from opened drivers
|
||||
3. Find the best property that meets the criteria
|
||||
|
||||
So, when `mediadevices` returns `failed to find the best driver that fits the constraints` error, one of the following conditions might have occured:
|
||||
* Driver was not imported as a side effect in your program, e.g. `import _ github.com/pion/mediadevices/pkg/driver/camera`
|
||||
* Your constraint is too strict that there's no driver can fullfil your requirements. In this case, you can try to turn up the debug level by specifying the following environment variable: `export PION_LOG_DEBUG=all` to see what was too strict and tune that.
|
||||
* Your driver is not supported/implemented. In this case, you can either let us know (file an issue) and wait for the maintainers to implement it. Or, you can implement it yourself and register it through `RegisterDriverAdapter`
|
||||
* If trying to use `import _ github.com/pion/mediadevices/pkg/driver/screen` note that you will need to use `GetDisplayMedia` instead of `GetUserMedia`
|
||||
|
||||
#### Failed to find vpx/x264/mmal/opus codecs
|
||||
Since `mediadevices` uses cgo to access video/audio codecs, it needs to find these libraries from the system. To accomplish this, [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) is used for library discovery.
|
||||
|
||||
If you see the following error message at compile time:
|
||||
|
||||
```bash
|
||||
# pkg-config --cflags -- vpx
|
||||
Package vpx was not found in the pkg-config search path.
|
||||
Perhaps you should add the directory containing `vpx.pc'
|
||||
to the PKG_CONFIG_PATH environment variable
|
||||
No package 'vpx' found
|
||||
pkg-config: exit status 1
|
||||
```
|
||||
|
||||
There are 2 common problems:
|
||||
|
||||
* The required codec library is not installed (vpx in this example). In this case, please refer to the [available codecs](#available-codecs).
|
||||
* Pkg-config fails to find the `.pc` files for this codec ([reference](https://people.freedesktop.org/~dbn/pkg-config-guide.html#using)). In this case, you need to find where the codec library's `.pc` is stored, and let pkg-config knows with: `export PKG_CONFIG_PATH=/path/to/directory`.
|
||||
|
||||
### Roadmap
|
||||
The library can be used with our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
|
||||
|
||||
### Community
|
||||
Pion has an active community on [Discord](https://discord.gg/PngbdqpFbt).
|
||||
|
||||
Follow the [Pion Twitter](https://twitter.com/_pion) and the [Pion Bluesky](https://bsky.app/profile/pion.ly) for project updates and important WebRTC news.
|
||||
|
||||
We are always looking to support **your projects**. Please reach out if you have something to build!
|
||||
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
|
||||
|
||||
### Contributing
|
||||
Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible
|
||||
|
||||
### License
|
||||
MIT License - see [LICENSE](LICENSE) for full text
|
||||
|
141
codec.go
Normal file
141
codec.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// CodecSelector is a container of video and audio encoder builders, which later will be used
|
||||
// for codec matching.
|
||||
type CodecSelector struct {
|
||||
videoEncoders []codec.VideoEncoderBuilder
|
||||
audioEncoders []codec.AudioEncoderBuilder
|
||||
}
|
||||
|
||||
// CodecSelectorOption is a type for specifying CodecSelector options
|
||||
type CodecSelectorOption func(*CodecSelector)
|
||||
|
||||
// WithVideoEncoders replace current video codecs with listed encoders
|
||||
func WithVideoEncoders(encoders ...codec.VideoEncoderBuilder) CodecSelectorOption {
|
||||
return func(t *CodecSelector) {
|
||||
t.videoEncoders = encoders
|
||||
}
|
||||
}
|
||||
|
||||
// WithVideoEncoders replace current audio codecs with listed encoders
|
||||
func WithAudioEncoders(encoders ...codec.AudioEncoderBuilder) CodecSelectorOption {
|
||||
return func(t *CodecSelector) {
|
||||
t.audioEncoders = encoders
|
||||
}
|
||||
}
|
||||
|
||||
// NewCodecSelector constructs CodecSelector with given variadic options
|
||||
func NewCodecSelector(opts ...CodecSelectorOption) *CodecSelector {
|
||||
var track CodecSelector
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&track)
|
||||
}
|
||||
|
||||
return &track
|
||||
}
|
||||
|
||||
// Populate lets the webrtc engine be aware of supported codecs that are contained in CodecSelector
|
||||
func (selector *CodecSelector) Populate(setting *webrtc.MediaEngine) {
|
||||
for _, encoder := range selector.videoEncoders {
|
||||
setting.RegisterCodec(encoder.RTPCodec().RTPCodecParameters, webrtc.RTPCodecTypeVideo)
|
||||
}
|
||||
|
||||
for _, encoder := range selector.audioEncoders {
|
||||
setting.RegisterCodec(encoder.RTPCodec().RTPCodecParameters, webrtc.RTPCodecTypeAudio)
|
||||
}
|
||||
}
|
||||
|
||||
// selectVideoCodecByNames selects a single codec that can be built and matched. codecNames can be formatted as "video/<codecName>" or "<codecName>"
|
||||
func (selector *CodecSelector) selectVideoCodecByNames(reader video.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) {
|
||||
var selectedEncoder codec.VideoEncoderBuilder
|
||||
var encodedReader codec.ReadCloser
|
||||
var errReasons []string
|
||||
var err error
|
||||
|
||||
outer:
|
||||
for _, wantCodec := range codecNames {
|
||||
wantCodecLower := strings.ToLower(wantCodec)
|
||||
for _, encoder := range selector.videoEncoders {
|
||||
// MimeType is formated as "video/<codecName>"
|
||||
if strings.HasSuffix(strings.ToLower(encoder.RTPCodec().MimeType), wantCodecLower) {
|
||||
encodedReader, err = encoder.BuildVideoEncoder(reader, inputProp)
|
||||
if err == nil {
|
||||
selectedEncoder = encoder
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().MimeType, err))
|
||||
}
|
||||
}
|
||||
|
||||
if selectedEncoder == nil {
|
||||
return nil, nil, errors.New(strings.Join(errReasons, "\n\n"))
|
||||
}
|
||||
|
||||
return encodedReader, selectedEncoder.RTPCodec(), nil
|
||||
}
|
||||
|
||||
func (selector *CodecSelector) selectVideoCodec(reader video.Reader, inputProp prop.Media, codecs ...webrtc.RTPCodecParameters) (codec.ReadCloser, *codec.RTPCodec, error) {
|
||||
var codecNames []string
|
||||
|
||||
for _, codec := range codecs {
|
||||
codecNames = append(codecNames, codec.MimeType)
|
||||
}
|
||||
|
||||
return selector.selectVideoCodecByNames(reader, inputProp, codecNames...)
|
||||
}
|
||||
|
||||
// selectAudioCodecByNames selects a single codec that can be built and matched. codecNames can be formatted as "audio/<codecName>" or "<codecName>"
|
||||
func (selector *CodecSelector) selectAudioCodecByNames(reader audio.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) {
|
||||
var selectedEncoder codec.AudioEncoderBuilder
|
||||
var encodedReader codec.ReadCloser
|
||||
var errReasons []string
|
||||
var err error
|
||||
|
||||
outer:
|
||||
for _, wantCodec := range codecNames {
|
||||
wantCodecLower := strings.ToLower(wantCodec)
|
||||
for _, encoder := range selector.audioEncoders {
|
||||
// MimeType is formated as "audio/<codecName>"
|
||||
if strings.HasSuffix(strings.ToLower(encoder.RTPCodec().MimeType), wantCodecLower) {
|
||||
encodedReader, err = encoder.BuildAudioEncoder(reader, inputProp)
|
||||
if err == nil {
|
||||
selectedEncoder = encoder
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().MimeType, err))
|
||||
}
|
||||
}
|
||||
|
||||
if selectedEncoder == nil {
|
||||
return nil, nil, errors.New(strings.Join(errReasons, "\n\n"))
|
||||
}
|
||||
|
||||
return encodedReader, selectedEncoder.RTPCodec(), nil
|
||||
}
|
||||
|
||||
func (selector *CodecSelector) selectAudioCodec(reader audio.Reader, inputProp prop.Media, codecs ...webrtc.RTPCodecParameters) (codec.ReadCloser, *codec.RTPCodec, error) {
|
||||
var codecNames []string
|
||||
|
||||
for _, codec := range codecs {
|
||||
codecNames = append(codecNames, codec.MimeType)
|
||||
}
|
||||
|
||||
return selector.selectAudioCodecByNames(reader, inputProp, codecNames...)
|
||||
}
|
10
codecov.yml
Normal file
10
codecov.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# Allow decreasing 2% of total coverage to avoid noise.
|
||||
threshold: 2%
|
||||
patch: off
|
||||
|
||||
ignore:
|
||||
- "examples/*"
|
@@ -1,15 +0,0 @@
|
||||
//The current file is auto-generated by script: generate_codec_ver.sh
|
||||
#ifndef CODEC_VER_H
|
||||
#define CODEC_VER_H
|
||||
|
||||
#include "codec_app_def.h"
|
||||
|
||||
static const OpenH264Version g_stCodecVersion = {2, 0, 0, 1905};
|
||||
static const char* const g_strCodecVer = "OpenH264 version:2.0.0.1905";
|
||||
|
||||
#define OPENH264_MAJOR (2)
|
||||
#define OPENH264_MINOR (0)
|
||||
#define OPENH264_REVISION (0)
|
||||
#define OPENH264_RESERVED (1905)
|
||||
|
||||
#endif // CODEC_VER_H
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule cvendor/src/openh264 deleted from 71374015cd
11
dockerfiles/Makefile
Normal file
11
dockerfiles/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
dockerfiles := $(wildcard *.Dockerfile)
|
||||
supported_platforms := $(dockerfiles:.Dockerfile=)
|
||||
|
||||
.PHONY: all
|
||||
all: $(supported_platforms)
|
||||
|
||||
%: %.Dockerfile guard-MEDIADEVICES_DOCKER_OWNER guard-MEDIADEVICES_DOCKER_PREFIX
|
||||
docker build -t "$(MEDIADEVICES_DOCKER_OWNER)/$(MEDIADEVICES_DOCKER_PREFIX)-$@" -f "$<" .
|
||||
|
||||
guard-%:
|
||||
@if [ -z ${$*} ]; then echo "$* is a required environment variable"; exit 1; fi
|
47
dockerfiles/darwin-arm64.Dockerfile
Normal file
47
dockerfiles/darwin-arm64.Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
FROM dockercore/golang-cross as m1cross
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update -qq && apt-get install -y -q --no-install-recommends \
|
||||
cmake \
|
||||
git \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libz-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV SDK_VERSION=11.3 \
|
||||
TARGET_DIR=/osxcross/target \
|
||||
UNATTENDED=1
|
||||
|
||||
WORKDIR /work
|
||||
RUN git clone --depth=1 https://github.com/tpoechtrager/osxcross.git /work \
|
||||
&& cd /work/tarballs \
|
||||
&& wget -q https://github.com/phracker/MacOSX-SDKs/releases/download/${SDK_VERSION}/MacOSX${SDK_VERSION}.sdk.tar.xz
|
||||
|
||||
# Build cross compile toolchain for Apple silicon
|
||||
RUN ./build.sh
|
||||
|
||||
|
||||
FROM dockcross/base
|
||||
|
||||
ENV OSX_CROSS_PATH=/osxcross
|
||||
|
||||
COPY --from=m1cross "${OSX_CROSS_PATH}/." "${OSX_CROSS_PATH}/"
|
||||
ENV PATH=${OSX_CROSS_PATH}/target/bin:$PATH
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
||||
|
||||
ENV CC=arm64-apple-darwin20.4-clang \
|
||||
CXX=arm64-apple-darwin20.4-clang++ \
|
||||
CPP=arm64-apple-darwin20.4-clang++ \
|
||||
AR=arm64-apple-darwin20.4-ar \
|
||||
AS=arm64-apple-darwin20.4-as \
|
||||
LD=arm64-apple-darwin20.4-ld
|
||||
|
||||
COPY darwin-arm64.cmake ${OSX_CROSS_PATH}/
|
||||
ENV CMAKE_TOOLCHAIN_FILE ${OSX_CROSS_PATH}/darwin-arm64.cmake
|
||||
|
||||
ARG IMAGE=lherman/cross-darwin-arm64
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
8
dockerfiles/darwin-arm64.cmake
Normal file
8
dockerfiles/darwin-arm64.cmake
Normal file
@@ -0,0 +1,8 @@
|
||||
set(CMAKE_SYSTEM_NAME Darwin)
|
||||
set(CMAKE_SYSTEM_VERSION 1)
|
||||
set(CMAKE_SYSTEM_PROCESSOR arm64)
|
||||
|
||||
set(CMAKE_C_COMPILER $ENV{CC})
|
||||
set(CMAKE_CXX_COMPILER $ENV{CXX})
|
||||
set(CMAKE_AR $ENV{AR})
|
||||
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
|
23
dockerfiles/darwin-x64.Dockerfile
Normal file
23
dockerfiles/darwin-x64.Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM dockcross/base
|
||||
|
||||
ENV OSX_CROSS_PATH=/osxcross
|
||||
|
||||
COPY --from=dockercore/golang-cross "${OSX_CROSS_PATH}/." "${OSX_CROSS_PATH}/"
|
||||
ENV PATH=${OSX_CROSS_PATH}/target/bin:$PATH
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
||||
|
||||
ENV CC=x86_64-apple-darwin14-clang \
|
||||
CXX=x86_64-apple-darwin14-clang++ \
|
||||
CPP=x86_64-apple-darwin14-clang++ \
|
||||
AR=x86_64-apple-darwin14-ar \
|
||||
AS=x86_64-apple-darwin14-as \
|
||||
LD=x86_64-apple-darwin14-ld
|
||||
|
||||
COPY darwin-x64.cmake ${OSX_CROSS_PATH}/
|
||||
ENV CMAKE_TOOLCHAIN_FILE ${OSX_CROSS_PATH}/darwin-x64.cmake
|
||||
|
||||
ARG IMAGE=lherman/cross-darwin-x64
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
8
dockerfiles/darwin-x64.cmake
Normal file
8
dockerfiles/darwin-x64.cmake
Normal file
@@ -0,0 +1,8 @@
|
||||
set(CMAKE_SYSTEM_NAME Darwin)
|
||||
set(CMAKE_SYSTEM_VERSION 1)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
set(CMAKE_C_COMPILER $ENV{CC})
|
||||
set(CMAKE_CXX_COMPILER $ENV{CXX})
|
||||
set(CMAKE_AR $ENV{AR})
|
||||
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
|
7
dockerfiles/init.sh
Executable file
7
dockerfiles/init.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
apt-get update
|
||||
apt-get install -y nasm clang llvm
|
||||
|
||||
curl -L https://golang.org/dl/go1.15.6.linux-amd64.tar.gz | tar -C /usr/local -xzf -
|
||||
ln -s /usr/local/go/bin/go /usr/local/bin/go
|
8
dockerfiles/linux-arm64.Dockerfile
Normal file
8
dockerfiles/linux-arm64.Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM dockcross/linux-arm64
|
||||
|
||||
ARG IMAGE=lherman/cross-linux-arm64
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
8
dockerfiles/linux-armv7.Dockerfile
Normal file
8
dockerfiles/linux-armv7.Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM dockcross/linux-armv7
|
||||
|
||||
ARG IMAGE=lherman/cross-linux-armv7
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
8
dockerfiles/linux-x64.Dockerfile
Normal file
8
dockerfiles/linux-x64.Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM dockcross/linux-x64
|
||||
|
||||
ARG IMAGE=lherman/cross-linux-x64
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
11
dockerfiles/windows-x64.Dockerfile
Normal file
11
dockerfiles/windows-x64.Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM dockcross/windows-static-x64-posix
|
||||
|
||||
ARG IMAGE=lherman/cross-windows-x64
|
||||
ARG VERSION=latest
|
||||
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
|
||||
|
||||
COPY init.sh /tmp/init.sh
|
||||
RUN bash /tmp/init.sh
|
||||
RUN ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-gcc /usr/bin/x86_64-w64-mingw32-gcc && \
|
||||
ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-g++ /usr/bin/x86_64-w64-mingw32-g++ && \
|
||||
ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-ar /usr/bin/x86_64-w64-mingw32-ar
|
1
examples/.gitignore
vendored
1
examples/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
go.sum
|
8
examples/Makefile
Normal file
8
examples/Makefile
Normal file
@@ -0,0 +1,8 @@
|
||||
examples := $(shell find * -maxdepth 0 -type d)
|
||||
examples := $(filter-out internal,$(examples))
|
||||
|
||||
.PHONY: all $(examples)
|
||||
all: $(examples)
|
||||
|
||||
$(examples):
|
||||
cd $@ && go build -mod=mod
|
1
examples/archive/.gitignore
vendored
Normal file
1
examples/archive/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
archive
|
38
examples/archive/README.md
Normal file
38
examples/archive/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using x264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [x264](https://github.com/pion/mediadevices#x264)
|
||||
|
||||
### Download archive examplee
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
### Run archive example
|
||||
|
||||
Run `cd mediadevices/examples/archive && go build && ./archive recorded.h264`
|
||||
|
||||
To stop recording, press `Ctrl+c` or send a SIGINT signal.
|
||||
|
||||
### Playback recorded video
|
||||
|
||||
Install GStreamer and run:
|
||||
```
|
||||
gst-launch-1.0 playbin uri=file://${PWD}/recorded.h264
|
||||
```
|
||||
|
||||
Or run VLC media plyer:
|
||||
```
|
||||
vlc recorded.h264
|
||||
```
|
||||
|
||||
A video should start playing in your GStreamer or VLC window.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
||||
|
82
examples/archive/main.go
Normal file
82
examples/archive/main.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s <path/to/file.h264>\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
dest := os.Args[1]
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT)
|
||||
|
||||
x264Params, err := x264.NewParams()
|
||||
must(err)
|
||||
x264Params.Preset = x264.PresetMedium
|
||||
x264Params.BitRate = 1_000_000 // 1mbps
|
||||
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
)
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
must(err)
|
||||
|
||||
videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack)
|
||||
defer videoTrack.Close()
|
||||
|
||||
videoTrack.Transform(video.TransformFunc(func(r video.Reader) video.Reader {
|
||||
return video.ReaderFunc(func() (img image.Image, release func(), err error) {
|
||||
// we send io.EOF signal to the encoder reader to stop reading. Therefore, io.Copy
|
||||
// will finish its execution and the program will finish
|
||||
select {
|
||||
case <-sigs:
|
||||
return nil, func() {}, io.EOF
|
||||
default:
|
||||
}
|
||||
|
||||
return r.Read()
|
||||
})
|
||||
}))
|
||||
|
||||
reader, err := videoTrack.NewEncodedIOReader(x264Params.RTPCodec().MimeType)
|
||||
must(err)
|
||||
defer reader.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
must(err)
|
||||
|
||||
fmt.Println("Recording... Press Ctrl+c to stop")
|
||||
_, err = io.Copy(out, reader)
|
||||
must(err)
|
||||
fmt.Println("Your video has been recorded to", dest)
|
||||
}
|
1
examples/facedetection/.gitignore
vendored
Normal file
1
examples/facedetection/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
facedetection
|
@@ -1,29 +1,15 @@
|
||||
## Instructions
|
||||
|
||||
### Download facedetection
|
||||
### Download facedetection example
|
||||
|
||||
```
|
||||
go get github.com/pion/mediadevices/examples/facedetection
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
### Open example page
|
||||
### Compile and Run facedetection
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/video) you should see two text-areas and a 'Start Session' button
|
||||
Run `cd mediadevices/examples/facedetection && go build && ./facedetection`
|
||||
|
||||
### Run facedetection with your browsers SessionDescription as stdin
|
||||
You should be able to see some loggings when it can see faces.
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that and:
|
||||
|
||||
#### Linux
|
||||
|
||||
Run `echo $BROWSER_SDP | facedetection`
|
||||
|
||||
### Input facedetection's SessionDescription into your browser
|
||||
|
||||
Copy the text that `facedetection` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-WebRTC! Now start building something cool
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
||||
|
@@ -1,118 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
pigo "github.com/esimov/pigo/core"
|
||||
)
|
||||
|
||||
var (
|
||||
cascade []byte
|
||||
err error
|
||||
classifier *pigo.Pigo
|
||||
)
|
||||
|
||||
func imgToGrayscale(img image.Image) []uint8 {
|
||||
bounds := img.Bounds()
|
||||
flatten := bounds.Dy() * bounds.Dx()
|
||||
grayImg := make([]uint8, flatten)
|
||||
|
||||
i := 0
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
pix := img.At(x, y)
|
||||
grayPix := color.GrayModel.Convert(pix).(color.Gray)
|
||||
grayImg[i] = grayPix.Y
|
||||
i++
|
||||
}
|
||||
}
|
||||
return grayImg
|
||||
}
|
||||
|
||||
// clusterDetection runs Pigo face detector core methods
|
||||
// and returns a cluster with the detected faces coordinates.
|
||||
func clusterDetection(img image.Image) []pigo.Detection {
|
||||
grayscale := imgToGrayscale(img)
|
||||
bounds := img.Bounds()
|
||||
cParams := pigo.CascadeParams{
|
||||
MinSize: 100,
|
||||
MaxSize: 600,
|
||||
ShiftFactor: 0.15,
|
||||
ScaleFactor: 1.1,
|
||||
ImageParams: pigo.ImageParams{
|
||||
Pixels: grayscale,
|
||||
Rows: bounds.Dy(),
|
||||
Cols: bounds.Dx(),
|
||||
Dim: bounds.Dx(),
|
||||
},
|
||||
}
|
||||
|
||||
if len(cascade) == 0 {
|
||||
cascade, err = ioutil.ReadFile("facefinder")
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading the cascade file: %s", err)
|
||||
}
|
||||
p := pigo.NewPigo()
|
||||
|
||||
// Unpack the binary file. This will return the number of cascade trees,
|
||||
// the tree depth, the threshold and the prediction from tree's leaf nodes.
|
||||
classifier, err = p.Unpack(cascade)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unpacking the cascade file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the classifier over the obtained leaf nodes and return the detection results.
|
||||
// The result contains quadruplets representing the row, column, scale and detection score.
|
||||
dets := classifier.RunCascade(cParams, 0.0)
|
||||
|
||||
// Calculate the intersection over union (IoU) of two clusters.
|
||||
dets = classifier.ClusterDetections(dets, 0)
|
||||
|
||||
return dets
|
||||
}
|
||||
|
||||
func drawCircle(img draw.Image, x0, y0, r int, c color.Color) {
|
||||
x, y, dx, dy := r-1, 0, 1, 1
|
||||
err := dx - (r * 2)
|
||||
|
||||
for x > y {
|
||||
img.Set(x0+x, y0+y, c)
|
||||
img.Set(x0+y, y0+x, c)
|
||||
img.Set(x0-y, y0+x, c)
|
||||
img.Set(x0-x, y0+y, c)
|
||||
img.Set(x0-x, y0-y, c)
|
||||
img.Set(x0-y, y0-x, c)
|
||||
img.Set(x0+y, y0-x, c)
|
||||
img.Set(x0+x, y0-y, c)
|
||||
|
||||
if err <= 0 {
|
||||
y++
|
||||
err += dy
|
||||
dy += 2
|
||||
}
|
||||
if err > 0 {
|
||||
x--
|
||||
dx += 2
|
||||
err += dx - (r * 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markFaces(img image.Image) image.Image {
|
||||
nrgba := imaging.Clone(img)
|
||||
dets := clusterDetection(img)
|
||||
for _, det := range dets {
|
||||
if det.Q < 5.0 {
|
||||
continue
|
||||
}
|
||||
|
||||
drawCircle(nrgba, det.Col, det.Row, det.Scale/2, color.Black)
|
||||
}
|
||||
return nrgba
|
||||
}
|
@@ -1,119 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
pigo "github.com/esimov/pigo/core"
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
func markFacesTransformer(r video.Reader) video.Reader {
|
||||
return video.ReaderFunc(func() (img image.Image, err error) {
|
||||
img, err = r.Read()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
const (
|
||||
confidenceLevel = 5.0
|
||||
)
|
||||
|
||||
img = markFaces(img)
|
||||
return
|
||||
})
|
||||
var (
|
||||
cascade []byte
|
||||
classifier *pigo.Pigo
|
||||
)
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func detectFace(frame *image.YCbCr) bool {
|
||||
bounds := frame.Bounds()
|
||||
cascadeParams := pigo.CascadeParams{
|
||||
MinSize: 100,
|
||||
MaxSize: 600,
|
||||
ShiftFactor: 0.15,
|
||||
ScaleFactor: 1.1,
|
||||
ImageParams: pigo.ImageParams{
|
||||
Pixels: frame.Y, // Y in YCbCr should be enough to detect faces
|
||||
Rows: bounds.Dy(),
|
||||
Cols: bounds.Dx(),
|
||||
Dim: bounds.Dx(),
|
||||
},
|
||||
}
|
||||
|
||||
// Run the classifier over the obtained leaf nodes and return the detection results.
|
||||
// The result contains quadruplets representing the row, column, scale and detection score.
|
||||
dets := classifier.RunCascade(cascadeParams, 0.0)
|
||||
|
||||
// Calculate the intersection over union (IoU) of two clusters.
|
||||
dets = classifier.ClusterDetections(dets, 0)
|
||||
|
||||
for _, det := range dets {
|
||||
if det.Q >= confidenceLevel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
// prepare face detector
|
||||
var err error
|
||||
cascade, err = ioutil.ReadFile("facefinder")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("Error reading the cascade file: %s", err)
|
||||
}
|
||||
p := pigo.NewPigo()
|
||||
|
||||
// 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())
|
||||
})
|
||||
|
||||
md := mediadevices.NewMediaDevices(peerConnection)
|
||||
|
||||
vp8Params, err := vpx.NewVP8Params()
|
||||
// Unpack the binary file. This will return the number of cascade trees,
|
||||
// the tree depth, the threshold and the prediction from tree's leaf nodes.
|
||||
classifier, err = p.Unpack(cascade)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("Error unpacking the cascade file: %s", err)
|
||||
}
|
||||
vp8Params.BitRate = 100000 // 100kbps
|
||||
|
||||
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormatExact(frame.FormatI420) // most of the encoder accepts I420
|
||||
c.Enabled = true
|
||||
c.FrameFormat = prop.FrameFormatOneOf{frame.FormatI420, frame.FormatYUY2}
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
c.VideoTransform = markFacesTransformer
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
must(err)
|
||||
|
||||
for _, tracker := range s.GetTracks() {
|
||||
t := tracker.Track()
|
||||
tracker.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
|
||||
t.ID(), t.Label(), err)
|
||||
})
|
||||
_, err = peerConnection.AddTransceiverFromTrack(t,
|
||||
webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// since we're trying to access the raw data, we need to cast Track to its real type, *mediadevices.VideoTrack
|
||||
videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack)
|
||||
defer videoTrack.Close()
|
||||
|
||||
videoReader := videoTrack.NewReader(false)
|
||||
// To save resources, we can simply use 4 fps to detect faces.
|
||||
ticker := time.NewTicker(time.Millisecond * 250)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
frame, release, err := videoReader.Read()
|
||||
must(err)
|
||||
|
||||
// Since we asked the frame format to be exactly I420/YUY2 in GetUserMedia, we can guarantee that it must be YCbCr
|
||||
if detectFace(frame.(*image.YCbCr)) {
|
||||
log.Println("Detect a face")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote SessionDescription
|
||||
err = peerConnection.SetRemoteDescription(offer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
release()
|
||||
}
|
||||
|
||||
// Create an answer
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Sets the LocalDescription, and starts our UDP listeners
|
||||
err = peerConnection.SetLocalDescription(answer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Output the answer in base64 so we can paste it in browser
|
||||
fmt.Println(signal.Encode(answer))
|
||||
select {}
|
||||
}
|
||||
|
@@ -1,9 +1,37 @@
|
||||
module github.com/pion/mediadevices/examples
|
||||
|
||||
go 1.14
|
||||
go 1.21
|
||||
|
||||
// Please don't commit require entries of examples.
|
||||
// `git checkout master examples/go.mod` to revert this file.
|
||||
require github.com/pion/mediadevices v0.0.0
|
||||
require (
|
||||
github.com/esimov/pigo v1.4.6
|
||||
github.com/pion/mediadevices v0.0.0
|
||||
github.com/pion/webrtc/v4 v4.1.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/blackjack/webcam v0.6.1 // indirect
|
||||
github.com/gen2brain/malgo v0.11.23 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.18 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/pion/mediadevices v0.0.0 => ../
|
||||
|
67
examples/go.sum
Normal file
67
examples/go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
|
||||
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
1
examples/http/.gitignore
vendored
Normal file
1
examples/http/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
http
|
19
examples/http/README.md
Normal file
19
examples/http/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Instructions
|
||||
|
||||
### Download http example
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
### Compile and Run HTTP server
|
||||
|
||||
Run `cd mediadevices/examples/http && go build && ./http :1313`
|
||||
|
||||
|
||||
### Access the camera stream from the browser
|
||||
|
||||
Go to "http://localhost:1313"
|
||||
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
85
examples/http/main.go
Normal file
85
examples/http/main.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// This is an example of using mediadevices to broadcast your camera through http.
|
||||
// The example doesn't aim to be performant, but rather it strives to be simple.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"os"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
|
||||
// Note: If you don't have a camera or microphone or your adapters are not supported,
|
||||
// you can always swap your adapters with our dummy adapters below.
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
)
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s host:port\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
dest := os.Args[1]
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(constraint *mediadevices.MediaTrackConstraints) {
|
||||
constraint.Width = prop.Int(600)
|
||||
constraint.Height = prop.Int(400)
|
||||
},
|
||||
})
|
||||
must(err)
|
||||
|
||||
track := mediaStream.GetVideoTracks()[0]
|
||||
videoTrack := track.(*mediadevices.VideoTrack)
|
||||
defer videoTrack.Close()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
var buf bytes.Buffer
|
||||
videoReader := videoTrack.NewReader(false)
|
||||
mimeWriter := multipart.NewWriter(w)
|
||||
|
||||
contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary())
|
||||
w.Header().Add("Content-Type", contentType)
|
||||
|
||||
partHeader := make(textproto.MIMEHeader)
|
||||
partHeader.Add("Content-Type", "image/jpeg")
|
||||
|
||||
for {
|
||||
frame, release, err := videoReader.Read()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
must(err)
|
||||
|
||||
err = jpeg.Encode(&buf, frame, nil)
|
||||
// Since we're done with img, we need to release img so that that the original owner can reuse
|
||||
// this memory.
|
||||
release()
|
||||
must(err)
|
||||
|
||||
partWriter, err := mimeWriter.CreatePart(partHeader)
|
||||
must(err)
|
||||
|
||||
_, err = partWriter.Write(buf.Bytes())
|
||||
buf.Reset()
|
||||
must(err)
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Printf("listening on %s\n", dest)
|
||||
log.Println(http.ListenAndServe(dest, nil))
|
||||
}
|
1
examples/openh264/.gitignore
vendored
Normal file
1
examples/openh264/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
openh264
|
22
examples/openh264/README.md
Normal file
22
examples/openh264/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using openh264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [openh264](https://github.com/pion/mediadevices#openh264)
|
||||
|
||||
### Download archive examplee
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
### Run openh264 example
|
||||
|
||||
Run `cd mediadevices/examples/openh264 && go build && ./openh264 recorded.h264`
|
||||
set bitrate ,first press `Ctrl+c` or send a SIGINT signal.
|
||||
To stop recording,second press `Ctrl+c` or send a SIGINT signal.
|
||||
|
96
examples/openh264/main.go
Normal file
96
examples/openh264/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/openh264"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s <path/to/file.h264>\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
dest := os.Args[1]
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT)
|
||||
|
||||
params, err := openh264.NewParams()
|
||||
must(err)
|
||||
params.BitRate = 1_000_000 // 1mbps
|
||||
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(¶ms),
|
||||
)
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
must(err)
|
||||
|
||||
videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack)
|
||||
defer videoTrack.Close()
|
||||
|
||||
videoTrack.Transform(video.TransformFunc(func(r video.Reader) video.Reader {
|
||||
return video.ReaderFunc(func() (img image.Image, release func(), err error) {
|
||||
// we send io.EOF signal to the encoder reader to stop reading. Therefore, io.Copy
|
||||
// will finish its execution and the program will finish
|
||||
select {
|
||||
case <-sigs:
|
||||
return nil, func() {}, io.EOF
|
||||
default:
|
||||
}
|
||||
|
||||
return r.Read()
|
||||
})
|
||||
}))
|
||||
|
||||
reader, err := videoTrack.NewEncodedIOReader(params.RTPCodec().MimeType)
|
||||
must(err)
|
||||
defer reader.Close()
|
||||
|
||||
out, err := os.Create(dest)
|
||||
must(err)
|
||||
fmt.Println("Recording... Press Ctrl+c to Set BitRate")
|
||||
go func() {
|
||||
_, err = io.Copy(out, reader)
|
||||
}()
|
||||
<-sigs
|
||||
if control, ok := reader.(codec.Controllable); ok {
|
||||
if ctrl, ok := control.Controller().(codec.KeyFrameController); ok {
|
||||
fmt.Println("Force Key")
|
||||
ctrl.ForceKeyFrame()
|
||||
}
|
||||
if ctrl, ok := control.Controller().(codec.BitRateController); ok {
|
||||
fmt.Println("SetBitRate")
|
||||
ctrl.SetBitRate(200_000)
|
||||
}
|
||||
}
|
||||
fmt.Println("Recording... Press Ctrl+c to stop")
|
||||
<-sigs
|
||||
must(err)
|
||||
fmt.Println("Your video has been recorded to", dest)
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
## Instructions
|
||||
|
||||
### Download rtp-send example
|
||||
|
||||
```
|
||||
go get github.com/pion/mediadevices/examples/rtp-send
|
||||
```
|
||||
|
||||
### Listen RTP
|
||||
|
||||
Install GStreamer and run:
|
||||
```
|
||||
gst-launch-1.0 udpsrc port=5000 caps=application/x-rtp,encode-name=VP8 \
|
||||
! rtpvp8depay ! vp8dec ! videoconvert ! autovideosink
|
||||
```
|
||||
|
||||
Or run VLC media plyer:
|
||||
```
|
||||
vlc ./vp8.sdp
|
||||
```
|
||||
|
||||
### Run rtp-send
|
||||
|
||||
Run `rtp-send localhost:5000`
|
||||
|
||||
A video should start playing in your GStreamer or VLC window.
|
||||
It's not WebRTC, but pure RTP.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
@@ -1,120 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
)
|
||||
|
||||
const (
|
||||
mtu = 1000
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s host:port\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
|
||||
md := mediadevices.NewMediaDevicesFromCodecs(
|
||||
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
||||
webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{
|
||||
webrtc.NewRTPVP8Codec(100, 90000),
|
||||
},
|
||||
},
|
||||
mediadevices.WithTrackGenerator(
|
||||
func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) (
|
||||
mediadevices.LocalTrack, error,
|
||||
) {
|
||||
return newTrack(codec, id, os.Args[1]), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
vp8Params, err := vpx.NewVP8Params()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vp8Params.BitRate = 100000 // 100kbps
|
||||
|
||||
_, err = md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
|
||||
c.Enabled = true
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
type track struct {
|
||||
codec *webrtc.RTPCodec
|
||||
packetizer rtp.Packetizer
|
||||
id string
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func newTrack(codec *webrtc.RTPCodec, id, dest string) *track {
|
||||
addr, err := net.ResolveUDPAddr("udp", dest)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conn, err := net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &track{
|
||||
codec: codec,
|
||||
packetizer: rtp.NewPacketizer(
|
||||
mtu,
|
||||
codec.PayloadType,
|
||||
1,
|
||||
codec.Payloader,
|
||||
rtp.NewRandomSequencer(),
|
||||
codec.ClockRate,
|
||||
),
|
||||
id: id,
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *track) WriteSample(s media.Sample) error {
|
||||
buf := make([]byte, mtu)
|
||||
pkts := t.packetizer.Packetize(s.Data, s.Samples)
|
||||
for _, p := range pkts {
|
||||
n, err := p.MarshalTo(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, _ = t.conn.Write(buf[:n])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *track) Codec() *webrtc.RTPCodec {
|
||||
return t.codec
|
||||
}
|
||||
|
||||
func (t *track) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *track) Kind() webrtc.RTPCodecType {
|
||||
return t.codec.Type
|
||||
}
|
1
examples/rtp/.gitignore
vendored
Normal file
1
examples/rtp/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
rtp
|
38
examples/rtp/README.md
Normal file
38
examples/rtp/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using x264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [x264](https://github.com/pion/mediadevices#x264)
|
||||
|
||||
### Download rtp example
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
### Listen RTP
|
||||
|
||||
Install GStreamer and run:
|
||||
```
|
||||
gst-launch-1.0 udpsrc port=5000 caps=application/x-rtp,encode-name=H264 \
|
||||
! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink
|
||||
```
|
||||
|
||||
Or run VLC media plyer:
|
||||
```
|
||||
vlc ./h264.sdp
|
||||
```
|
||||
|
||||
### Run rtp
|
||||
|
||||
Run `cd mediadevices/examples/archive && go build && ./rtp localhost:5000`
|
||||
|
||||
A video should start playing in your GStreamer or VLC window.
|
||||
It's not WebRTC, but pure RTP.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
||||
|
@@ -6,4 +6,4 @@ c=IN IP4 0.0.0.0
|
||||
t=0 0
|
||||
a=recvonly
|
||||
m=video 5000 RTP/AVP 100
|
||||
a=rtpmap:100 VP8/90000
|
||||
a=rtpmap:100 H264/90000
|
78
examples/rtp/main.go
Normal file
78
examples/rtp/main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
const (
|
||||
mtu = 1000
|
||||
)
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("usage: %s host:port\n", os.Args[0])
|
||||
return
|
||||
}
|
||||
dest := os.Args[1]
|
||||
|
||||
x264Params, err := x264.NewParams()
|
||||
must(err)
|
||||
x264Params.Preset = x264.PresetMedium
|
||||
x264Params.BitRate = 1_000_000 // 1mbps
|
||||
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
)
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
must(err)
|
||||
|
||||
videoTrack := mediaStream.GetVideoTracks()[0]
|
||||
defer videoTrack.Close()
|
||||
|
||||
rtpReader, err := videoTrack.NewRTPReader(x264Params.RTPCodec().MimeType, rand.Uint32(), mtu)
|
||||
must(err)
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", dest)
|
||||
must(err)
|
||||
conn, err := net.DialUDP("udp", nil, addr)
|
||||
must(err)
|
||||
|
||||
buff := make([]byte, mtu)
|
||||
for {
|
||||
pkts, release, err := rtpReader.Read()
|
||||
must(err)
|
||||
|
||||
for _, pkt := range pkts {
|
||||
n, err := pkt.MarshalTo(buff)
|
||||
must(err)
|
||||
|
||||
_, err = conn.Write(buff[:n])
|
||||
must(err)
|
||||
}
|
||||
|
||||
release()
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
## Instructions
|
||||
|
||||
### Download screenshare
|
||||
|
||||
```
|
||||
go get github.com/pion/mediadevices/examples/screenshare
|
||||
```
|
||||
|
||||
### Open example page
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
|
||||
|
||||
### Run screenshare with your browsers SessionDescription as stdin
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that and:
|
||||
|
||||
#### Linux
|
||||
|
||||
Run `echo $BROWSER_SDP | screenshare`
|
||||
|
||||
### Input screenshare's SessionDescription into your browser
|
||||
|
||||
Copy the text that `screenshare` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-WebRTC! Now start building something cool
|
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
_ "github.com/pion/mediadevices/pkg/driver/screen" // This is required to register screen capture adapter
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
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("Connection State has changed %s \n", connectionState.String())
|
||||
})
|
||||
|
||||
md := mediadevices.NewMediaDevices(peerConnection)
|
||||
|
||||
vp8Params, err := vpx.NewVP8Params()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vp8Params.BitRate = 100000 // 100kbps
|
||||
|
||||
s, err := md.GetDisplayMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
c.VideoTransform = video.Scale(-1, 360, nil) // Resize to 360p
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tracker := range s.GetTracks() {
|
||||
t := tracker.Track()
|
||||
tracker.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
|
||||
t.ID(), t.Label(), err)
|
||||
})
|
||||
_, err = peerConnection.AddTransceiverFromTrack(t,
|
||||
webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote SessionDescription
|
||||
err = peerConnection.SetRemoteDescription(offer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create an answer
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Sets the LocalDescription, and starts our UDP listeners
|
||||
err = peerConnection.SetLocalDescription(answer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Output the answer in base64 so we can paste it in browser
|
||||
fmt.Println(signal.Encode(answer))
|
||||
select {}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
## Instructions
|
||||
|
||||
### Download gstreamer-send
|
||||
|
||||
```
|
||||
go get github.com/pion/mediadevices/examples/simple
|
||||
```
|
||||
|
||||
### Open example page
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
|
||||
|
||||
### Run simple with your browsers SessionDescription as stdin
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that and:
|
||||
|
||||
#### Linux
|
||||
|
||||
Run `echo $BROWSER_SDP | simple`
|
||||
|
||||
### Input simple's SessionDescription into your browser
|
||||
|
||||
Copy the text that `simple` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-WebRTC! Now start building something cool
|
1
examples/vnc/.gitignore
vendored
Normal file
1
examples/vnc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
vnc
|
46
examples/vnc/README.md
Normal file
46
examples/vnc/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using x264 and opus as our video and audio codecs. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [x264](https://github.com/pion/mediadevices#x264)
|
||||
|
||||
### Download vnc example
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
#### Compile vnc example
|
||||
|
||||
```
|
||||
cd mediadevices/examples/vnc && go build
|
||||
```
|
||||
|
||||
### Open example page
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
|
||||
|
||||
### Run the webrtc example with your browsers SessionDescription as stdin
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that, and store the session description in an environment variable, `export SDP=<put_the_sdp_here>`
|
||||
|
||||
Run `echo $SDP | ./vnc`
|
||||
|
||||
In Windows
|
||||
|
||||
```powershell
|
||||
type sdp.txt| .\vnc.exe
|
||||
```
|
||||
### Input webrtc's SessionDescription into your browser
|
||||
|
||||
Copy the text that `./webrtc` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
127
examples/vnc/main.go
Normal file
127
examples/vnc/main.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
"github.com/pion/mediadevices/pkg/driver/vncdriver"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
// If you don't like x264, you can also use vpx by importing as below
|
||||
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
// or you can also use openh264 for alternative h264 implementation
|
||||
// "github.com/pion/mediadevices/pkg/codec/openh264"
|
||||
// or if you use a raspberry pi like, you can use mmal for using its hardware encoder
|
||||
// "github.com/pion/mediadevices/pkg/codec/mmal"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder
|
||||
// Note: If you don't have a camera or microphone or your adapters are not supported,
|
||||
// you can always swap your adapters with our dummy adapters below.
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
}
|
||||
driver.GetManager().Register(
|
||||
vncdriver.NewVnc("127.0.0.1:5900"),
|
||||
driver.Info{Label: "VNC", DeviceType: driver.Camera, Priority: driver.PriorityLow},
|
||||
)
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
x264Params, err := x264.NewParams()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
x264Params.BitRate = 500_000 // 500kbps
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
)
|
||||
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
codecSelector.Populate(&mediaEngine)
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
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("Connection State has changed %s \n", connectionState.String())
|
||||
})
|
||||
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, track := range s.GetTracks() {
|
||||
track.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s) ended with error: %v\n",
|
||||
track.ID(), err)
|
||||
})
|
||||
|
||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote SessionDescription
|
||||
err = peerConnection.SetRemoteDescription(offer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create an 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
|
||||
err = peerConnection.SetLocalDescription(answer)
|
||||
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()))
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
1
examples/webrtc/.gitignore
vendored
Normal file
1
examples/webrtc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
webrtc
|
42
examples/webrtc/README.md
Normal file
42
examples/webrtc/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using x264 and opus as our video and audio codecs. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [x264](https://github.com/pion/mediadevices#x264)
|
||||
* [opus](https://github.com/pion/mediadevices#opus)
|
||||
|
||||
### Download webrtc example
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
#### Compile webrtc example
|
||||
|
||||
```
|
||||
cd mediadevices/examples/webrtc && go build
|
||||
```
|
||||
|
||||
### Open example page
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
|
||||
|
||||
### Run the webrtc example with your browsers SessionDescription as stdin
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that, and store the session description in an environment variable, `export SDP=<put_the_sdp_here>`
|
||||
|
||||
Run `echo $SDP | ./webrtc`
|
||||
|
||||
### Input webrtc's SessionDescription into your browser
|
||||
|
||||
Copy the text that `./webrtc` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
@@ -5,19 +5,18 @@ import (
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
// This is required to use opus audio encoder
|
||||
"github.com/pion/mediadevices/pkg/codec/opus"
|
||||
|
||||
// If you don't like vpx, you can also use x264 by importing as below
|
||||
// "github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder
|
||||
// If you don't like x264, you can also use vpx by importing as below
|
||||
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
// or you can also use openh264 for alternative h264 implementation
|
||||
// "github.com/pion/mediadevices/pkg/codec/openh264"
|
||||
"github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
// or if you use a raspberry pi like, you can use mmal for using its hardware encoder
|
||||
// "github.com/pion/mediadevices/pkg/codec/mmal"
|
||||
"github.com/pion/mediadevices/pkg/codec/opus" // This is required to use opus audio encoder
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder
|
||||
|
||||
// Note: If you don't have a camera or microphone or your adapters are not supported,
|
||||
// you can always swap your adapters with our dummy adapters below.
|
||||
@@ -27,10 +26,6 @@ import (
|
||||
_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
|
||||
)
|
||||
|
||||
const (
|
||||
videoCodecName = webrtc.VP8
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
@@ -45,11 +40,24 @@ func main() {
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
if err := mediaEngine.PopulateFromSDP(offer); err != nil {
|
||||
x264Params, err := x264.NewParams()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
|
||||
x264Params.BitRate = 500_000 // 500kbps
|
||||
|
||||
opusParams, err := opus.NewParams()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
mediadevices.WithAudioEncoders(&opusParams),
|
||||
)
|
||||
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
codecSelector.Populate(&mediaEngine)
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -61,45 +69,28 @@ func main() {
|
||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||
})
|
||||
|
||||
md := mediadevices.NewMediaDevices(peerConnection)
|
||||
|
||||
opusParams, err := opus.NewParams()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
opusParams.BitRate = 32000 // 32kbps
|
||||
|
||||
vp8Params, err := vpx.NewVP8Params()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vp8Params.BitRate = 100000 // 100kbps
|
||||
|
||||
s, err := md.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Audio: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{&opusParams}
|
||||
},
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
|
||||
c.Enabled = true
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{&vp8Params}
|
||||
},
|
||||
Audio: func(c *mediadevices.MediaTrackConstraints) {
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tracker := range s.GetTracks() {
|
||||
t := tracker.Track()
|
||||
tracker.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s, Label: %s) ended with error: %v\n",
|
||||
t.ID(), t.Label(), err)
|
||||
for _, track := range s.GetTracks() {
|
||||
track.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s) ended with error: %v\n",
|
||||
track.ID(), err)
|
||||
})
|
||||
_, err = peerConnection.AddTransceiverFromTrack(t,
|
||||
webrtc.RtpTransceiverInit{
|
||||
|
||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
@@ -120,13 +111,23 @@ func main() {
|
||||
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(answer)
|
||||
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(answer))
|
||||
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
46
go.mod
46
go.mod
@@ -1,13 +1,43 @@
|
||||
module github.com/pion/mediadevices
|
||||
|
||||
go 1.13
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
|
||||
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa
|
||||
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39
|
||||
github.com/pion/webrtc/v2 v2.2.26
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a
|
||||
github.com/blackjack/webcam v0.6.1
|
||||
github.com/gen2brain/malgo v0.11.24
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.19
|
||||
github.com/pion/webrtc/v4 v4.1.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/image v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gen2brain/shm v0.1.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
197
go.sum
197
go.sum
@@ -1,139 +1,76 @@
|
||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 h1:1aIqYfg9s9RETAJHGfVKZW4ok0b22p4QTwk8MsdRtPs=
|
||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa h1:qUZIj5+D3UDgfshNe8Cz/9maOxe8ddt43qwQH9vEEC8=
|
||||
github.com/jfreymuth/pulse v0.0.0-20200817093420-a82ccdb5e8aa/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/gen2brain/malgo v0.11.24 h1:hHcIJVfzWcEDHFdPl5Dl/CUSOjzOleY0zzAV8Kx+imE=
|
||||
github.com/gen2brain/malgo v0.11.24/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39 h1:WEYmSwg/uoPVmfmpXWPYplb1UUx/Jr4TXGNrPaI8Cj4=
|
||||
github.com/lherman-cs/opus v0.0.0-20200925065115-26ea9d322d39/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
|
||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
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.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA=
|
||||
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
||||
github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E=
|
||||
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
|
||||
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
|
||||
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
|
||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
|
||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
||||
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
|
||||
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
||||
github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
|
||||
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
||||
github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8=
|
||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||
github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
|
||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
||||
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
|
||||
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.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
||||
github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk=
|
||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||
github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80=
|
||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
||||
github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
|
||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
||||
github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
|
||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
||||
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
|
||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
||||
github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc=
|
||||
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c=
|
||||
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
|
||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a h1:i47hUS795cOydZI4AwJQCKXOr4BvxzvikwDoDtHhP2Y=
|
||||
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
BIN
img/demo.gif
BIN
img/demo.gif
Binary file not shown.
Before Width: | Height: | Size: 9.6 MiB |
@@ -2,12 +2,18 @@ package codec
|
||||
|
||||
import (
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMeasureBitRateStatic(t *testing.T) {
|
||||
// https://github.com/pion/mediadevices/issues/198
|
||||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
const (
|
||||
dataSize = 1000
|
||||
@@ -54,6 +60,11 @@ func TestMeasureBitRateStatic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMeasureBitRateDynamic(t *testing.T) {
|
||||
// https://github.com/pion/mediadevices/issues/198
|
||||
if runtime.GOOS == "darwin" {
|
||||
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
const (
|
||||
dataSize = 1000
|
||||
|
11
internal/logging/logging.go
Normal file
11
internal/logging/logging.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"github.com/pion/logging"
|
||||
)
|
||||
|
||||
var loggerFactory = logging.NewDefaultLoggerFactory()
|
||||
|
||||
func NewLogger(scope string) logging.LeveledLogger {
|
||||
return loggerFactory.NewLogger(scope)
|
||||
}
|
75
ioreader.go
Normal file
75
ioreader.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package mediadevices
|
||||
|
||||
import "github.com/pion/mediadevices/pkg/codec"
|
||||
|
||||
type EncodedBuffer struct {
|
||||
Data []byte
|
||||
Samples uint32
|
||||
}
|
||||
|
||||
type EncodedReadCloser interface {
|
||||
Read() (EncodedBuffer, func(), error)
|
||||
Close() error
|
||||
codec.Controllable
|
||||
}
|
||||
|
||||
type encodedReadCloserImpl struct {
|
||||
readFn func() (EncodedBuffer, func(), error)
|
||||
closeFn func() error
|
||||
controllerFn func() codec.EncoderController
|
||||
}
|
||||
|
||||
func (r *encodedReadCloserImpl) Read() (EncodedBuffer, func(), error) {
|
||||
return r.readFn()
|
||||
}
|
||||
|
||||
func (r *encodedReadCloserImpl) Close() error {
|
||||
return r.closeFn()
|
||||
}
|
||||
|
||||
func (r *encodedReadCloserImpl) Controller() codec.EncoderController {
|
||||
return r.controllerFn()
|
||||
}
|
||||
|
||||
type encodedIOReadCloserImpl struct {
|
||||
readFn func([]byte) (int, error)
|
||||
closeFn func() error
|
||||
controller func() codec.EncoderController
|
||||
}
|
||||
|
||||
func newEncodedIOReadCloserImpl(reader EncodedReadCloser) *encodedIOReadCloserImpl {
|
||||
var encoded EncodedBuffer
|
||||
release := func() {}
|
||||
return &encodedIOReadCloserImpl{
|
||||
readFn: func(b []byte) (int, error) {
|
||||
var err error
|
||||
|
||||
if len(encoded.Data) == 0 {
|
||||
release()
|
||||
encoded, release, err = reader.Read()
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
n := copy(b, encoded.Data)
|
||||
encoded.Data = encoded.Data[n:]
|
||||
return n, nil
|
||||
},
|
||||
closeFn: reader.Close,
|
||||
controller: reader.Controller,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *encodedIOReadCloserImpl) Read(b []byte) (int, error) {
|
||||
return r.readFn(b)
|
||||
}
|
||||
|
||||
func (r *encodedIOReadCloserImpl) Close() error {
|
||||
return r.closeFn()
|
||||
}
|
||||
|
||||
func (r *encodedIOReadCloserImpl) Controller() codec.EncoderController {
|
||||
return r.controller()
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
FROM dockercore/golang-cross
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install -y \
|
||||
g++-mingw-w64 \
|
||||
nasm \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
VOLUME /go/src/github.com/pion/mediadevices
|
||||
WORKDIR /go/src/github.com/pion/mediadevices
|
7
logging.go
Normal file
7
logging.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/internal/logging"
|
||||
)
|
||||
|
||||
var logger = logging.NewLogger("mediadevices")
|
@@ -7,7 +7,7 @@ type MediaDeviceType int
|
||||
|
||||
// MediaDeviceType definitions.
|
||||
const (
|
||||
VideoInput MediaDeviceType = iota
|
||||
VideoInput MediaDeviceType = iota + 1
|
||||
AudioInput
|
||||
AudioOutput
|
||||
)
|
||||
|
140
mediadevices.go
140
mediadevices.go
@@ -7,95 +7,26 @@ import (
|
||||
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
var errNotFound = fmt.Errorf("failed to find the best driver that fits the constraints")
|
||||
|
||||
// MediaDevices is an interface that's defined on https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
|
||||
type MediaDevices interface {
|
||||
GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error)
|
||||
GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error)
|
||||
EnumerateDevices() []MediaDeviceInfo
|
||||
}
|
||||
|
||||
// NewMediaDevices creates MediaDevices interface that provides access to connected media input devices
|
||||
// like cameras and microphones, as well as screen sharing.
|
||||
// In essence, it lets you obtain access to any hardware source of media data.
|
||||
func NewMediaDevices(pc *webrtc.PeerConnection, opts ...MediaDevicesOption) MediaDevices {
|
||||
codecs := make(map[webrtc.RTPCodecType][]*webrtc.RTPCodec)
|
||||
for _, kind := range []webrtc.RTPCodecType{
|
||||
webrtc.RTPCodecTypeAudio,
|
||||
webrtc.RTPCodecTypeVideo,
|
||||
} {
|
||||
codecs[kind] = pc.GetRegisteredRTPCodecs(kind)
|
||||
}
|
||||
return NewMediaDevicesFromCodecs(codecs, opts...)
|
||||
}
|
||||
|
||||
// NewMediaDevicesFromCodecs creates MediaDevices interface from lists of the available codecs
|
||||
// that provides access to connected media input devices like cameras and microphones,
|
||||
// as well as screen sharing.
|
||||
// In essence, it lets you obtain access to any hardware source of media data.
|
||||
func NewMediaDevicesFromCodecs(codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec, opts ...MediaDevicesOption) MediaDevices {
|
||||
mdo := MediaDevicesOptions{
|
||||
codecs: codecs,
|
||||
trackGenerator: defaultTrackGenerator,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&mdo)
|
||||
}
|
||||
return &mediaDevices{
|
||||
MediaDevicesOptions: mdo,
|
||||
}
|
||||
}
|
||||
|
||||
// TrackGenerator is a function to create new track.
|
||||
type TrackGenerator func(payloadType uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error)
|
||||
|
||||
var defaultTrackGenerator = TrackGenerator(func(pt uint8, ssrc uint32, id, label string, codec *webrtc.RTPCodec) (LocalTrack, error) {
|
||||
return webrtc.NewTrack(pt, ssrc, id, label, codec)
|
||||
})
|
||||
|
||||
type mediaDevices struct {
|
||||
MediaDevicesOptions
|
||||
}
|
||||
|
||||
// MediaDevicesOptions stores parameters used by MediaDevices.
|
||||
type MediaDevicesOptions struct {
|
||||
codecs map[webrtc.RTPCodecType][]*webrtc.RTPCodec
|
||||
trackGenerator TrackGenerator
|
||||
}
|
||||
|
||||
// MediaDevicesOption is a type of MediaDevices functional option.
|
||||
type MediaDevicesOption func(*MediaDevicesOptions)
|
||||
|
||||
// WithTrackGenerator specifies a TrackGenerator to use customized track.
|
||||
func WithTrackGenerator(gen TrackGenerator) MediaDevicesOption {
|
||||
return func(o *MediaDevicesOptions) {
|
||||
o.trackGenerator = gen
|
||||
}
|
||||
}
|
||||
|
||||
// GetDisplayMedia prompts the user to select and grant permission to capture the contents
|
||||
// of a display or portion thereof (such as a window) as a MediaStream.
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
|
||||
func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
trackers := make([]Tracker, 0)
|
||||
func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
trackers := make([]Track, 0)
|
||||
|
||||
cleanTrackers := func() {
|
||||
for _, t := range trackers {
|
||||
t.Stop()
|
||||
t.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var videoConstraints MediaTrackConstraints
|
||||
if constraints.Video != nil {
|
||||
constraints.Video(&videoConstraints)
|
||||
}
|
||||
|
||||
if videoConstraints.Enabled {
|
||||
tracker, err := m.selectScreen(videoConstraints)
|
||||
tracker, err := selectScreen(videoConstraints, constraints.Codec)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
return nil, err
|
||||
@@ -116,27 +47,20 @@ func (m *mediaDevices) GetDisplayMedia(constraints MediaStreamConstraints) (Medi
|
||||
// GetUserMedia prompts the user for permission to use a media input which produces a MediaStream
|
||||
// with tracks containing the requested types of media.
|
||||
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) {
|
||||
// TODO: It should return media stream based on constraints
|
||||
trackers := make([]Tracker, 0)
|
||||
trackers := make([]Track, 0)
|
||||
|
||||
cleanTrackers := func() {
|
||||
for _, t := range trackers {
|
||||
t.Stop()
|
||||
t.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var videoConstraints, audioConstraints MediaTrackConstraints
|
||||
if constraints.Video != nil {
|
||||
constraints.Video(&videoConstraints)
|
||||
}
|
||||
|
||||
if constraints.Audio != nil {
|
||||
constraints.Audio(&audioConstraints)
|
||||
}
|
||||
|
||||
if videoConstraints.Enabled {
|
||||
tracker, err := m.selectVideo(videoConstraints)
|
||||
tracker, err := selectVideo(videoConstraints, constraints.Codec)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
return nil, err
|
||||
@@ -145,8 +69,9 @@ func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaSt
|
||||
trackers = append(trackers, tracker)
|
||||
}
|
||||
|
||||
if audioConstraints.Enabled {
|
||||
tracker, err := m.selectAudio(audioConstraints)
|
||||
if constraints.Audio != nil {
|
||||
constraints.Audio(&audioConstraints)
|
||||
tracker, err := selectAudio(audioConstraints, constraints.Codec)
|
||||
if err != nil {
|
||||
cleanTrackers()
|
||||
return nil, err
|
||||
@@ -195,12 +120,15 @@ func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Medi
|
||||
func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) {
|
||||
var bestDriver driver.Driver
|
||||
var bestProp prop.Media
|
||||
var foundPropertiesLog []string
|
||||
minFitnessDist := math.Inf(1)
|
||||
|
||||
foundPropertiesLog = append(foundPropertiesLog, "\n============ Found Properties ============")
|
||||
driverProperties := queryDriverProperties(filter)
|
||||
for d, props := range driverProperties {
|
||||
priority := float64(d.Info().Priority)
|
||||
for _, p := range props {
|
||||
foundPropertiesLog = append(foundPropertiesLog, p.String())
|
||||
fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -214,33 +142,25 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
foundPropertiesLog = append(foundPropertiesLog, "=============== Constraints ==============")
|
||||
foundPropertiesLog = append(foundPropertiesLog, constraints.String())
|
||||
foundPropertiesLog = append(foundPropertiesLog, "================ Best Fit ================")
|
||||
|
||||
if bestDriver == nil {
|
||||
var foundProperties []string
|
||||
for _, props := range driverProperties {
|
||||
for _, p := range props {
|
||||
foundProperties = append(foundProperties, fmt.Sprint(&p))
|
||||
}
|
||||
}
|
||||
|
||||
err := fmt.Errorf(`%w:
|
||||
============ Found Properties ============
|
||||
|
||||
%s
|
||||
|
||||
=============== Constraints ==============
|
||||
|
||||
%s
|
||||
`, errNotFound, strings.Join(foundProperties, "\n\n"), &constraints)
|
||||
return nil, MediaTrackConstraints{}, err
|
||||
foundPropertiesLog = append(foundPropertiesLog, "Not found")
|
||||
logger.Debug(strings.Join(foundPropertiesLog, "\n\n"))
|
||||
return nil, MediaTrackConstraints{}, errNotFound
|
||||
}
|
||||
|
||||
foundPropertiesLog = append(foundPropertiesLog, bestProp.String())
|
||||
logger.Debug(strings.Join(foundPropertiesLog, "\n\n"))
|
||||
constraints.selectedMedia = prop.Media{}
|
||||
constraints.selectedMedia.MergeConstraints(constraints.MediaConstraints)
|
||||
constraints.selectedMedia.Merge(bestProp)
|
||||
return bestDriver, constraints, nil
|
||||
}
|
||||
|
||||
func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker, error) {
|
||||
func selectAudio(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) {
|
||||
typeFilter := driver.FilterAudioRecorder()
|
||||
|
||||
d, c, err := selectBestDriver(typeFilter, constraints)
|
||||
@@ -248,9 +168,9 @@ func (m *mediaDevices) selectAudio(constraints MediaTrackConstraints) (Tracker,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
||||
return newTrackFromDriver(d, c, selector)
|
||||
}
|
||||
func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker, error) {
|
||||
func selectVideo(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) {
|
||||
typeFilter := driver.FilterVideoRecorder()
|
||||
notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen))
|
||||
filter := driver.FilterAnd(typeFilter, notScreenFilter)
|
||||
@@ -260,10 +180,10 @@ func (m *mediaDevices) selectVideo(constraints MediaTrackConstraints) (Tracker,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
||||
return newTrackFromDriver(d, c, selector)
|
||||
}
|
||||
|
||||
func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker, error) {
|
||||
func selectScreen(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) {
|
||||
typeFilter := driver.FilterVideoRecorder()
|
||||
screenFilter := driver.FilterDeviceType(driver.Screen)
|
||||
filter := driver.FilterAnd(typeFilter, screenFilter)
|
||||
@@ -273,10 +193,10 @@ func (m *mediaDevices) selectScreen(constraints MediaTrackConstraints) (Tracker,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTrack(&m.MediaDevicesOptions, d, c)
|
||||
return newTrackFromDriver(d, c, selector)
|
||||
}
|
||||
|
||||
func (m *mediaDevices) EnumerateDevices() []MediaDeviceInfo {
|
||||
func EnumerateDevices() []MediaDeviceInfo {
|
||||
drivers := driver.GetManager().Query(
|
||||
driver.FilterFn(func(driver.Driver) bool { return true }))
|
||||
info := make([]MediaDeviceInfo, 0, len(drivers))
|
||||
|
82
mediadevices_bench_test.go
Normal file
82
mediadevices_bench_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// +build e2e
|
||||
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec/x264"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
)
|
||||
|
||||
type mockVideoSource struct {
|
||||
width, height int
|
||||
pool sync.Pool
|
||||
decoder frame.Decoder
|
||||
}
|
||||
|
||||
func newMockVideoSource(width, height int) *mockVideoSource {
|
||||
decoder, err := frame.NewDecoder(frame.FormatYUY2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &mockVideoSource{
|
||||
width: width,
|
||||
height: height,
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
resolution := width * height
|
||||
return make([]byte, resolution*2)
|
||||
},
|
||||
},
|
||||
decoder: decoder,
|
||||
}
|
||||
}
|
||||
|
||||
func (source *mockVideoSource) ID() string { return "" }
|
||||
func (source *mockVideoSource) Close() error { return nil }
|
||||
func (source *mockVideoSource) Read() (image.Image, func(), error) {
|
||||
raw := source.pool.Get().([]byte)
|
||||
decoded, release, err := source.decoder.Decode(raw, source.width, source.height)
|
||||
source.pool.Put(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return decoded, release, nil
|
||||
}
|
||||
|
||||
func BenchmarkEndToEnd(b *testing.B) {
|
||||
params, err := x264.NewParams()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
params.BitRate = 300_000
|
||||
|
||||
videoSource := newMockVideoSource(1920, 1080)
|
||||
track := NewVideoTrack(videoSource, nil).(*VideoTrack)
|
||||
defer track.Close()
|
||||
|
||||
reader := track.NewReader(false)
|
||||
inputProp, err := detectCurrentVideoProp(track.Broadcaster)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
encodedReader, err := params.BuildVideoEncoder(reader, inputProp)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer encodedReader.Close()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, release, err := encodedReader.Read()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
release()
|
||||
}
|
||||
}
|
@@ -1,91 +1,43 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestGetUserMedia(t *testing.T) {
|
||||
videoParams := mockParams{
|
||||
BaseParams: codec.BaseParams{
|
||||
BitRate: 100000,
|
||||
},
|
||||
name: "MockVideo",
|
||||
}
|
||||
audioParams := mockParams{
|
||||
BaseParams: codec.BaseParams{
|
||||
BitRate: 32000,
|
||||
},
|
||||
name: "MockAudio",
|
||||
}
|
||||
md := NewMediaDevicesFromCodecs(
|
||||
map[webrtc.RTPCodecType][]*webrtc.RTPCodec{
|
||||
webrtc.RTPCodecTypeVideo: {
|
||||
{Type: webrtc.RTPCodecTypeVideo, Name: "MockVideo", PayloadType: 1},
|
||||
},
|
||||
webrtc.RTPCodecTypeAudio: {
|
||||
{Type: webrtc.RTPCodecTypeAudio, Name: "MockAudio", PayloadType: 2},
|
||||
},
|
||||
},
|
||||
WithTrackGenerator(
|
||||
func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) (
|
||||
LocalTrack, error,
|
||||
) {
|
||||
return newMockTrack(codec, id), nil
|
||||
},
|
||||
),
|
||||
)
|
||||
constraints := MediaStreamConstraints{
|
||||
Video: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
params := videoParams
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms}
|
||||
},
|
||||
Audio: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
params := audioParams
|
||||
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms}
|
||||
},
|
||||
}
|
||||
constraintsWrong := MediaStreamConstraints{
|
||||
Video: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
c.Width = prop.Int(640)
|
||||
c.Width = prop.IntExact(10000)
|
||||
c.Height = prop.Int(480)
|
||||
params := videoParams
|
||||
params.BitRate = 0
|
||||
c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms}
|
||||
},
|
||||
Audio: func(c *MediaTrackConstraints) {
|
||||
c.Enabled = true
|
||||
params := audioParams
|
||||
c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms}
|
||||
},
|
||||
}
|
||||
|
||||
// GetUserMedia with broken parameters
|
||||
ms, err := md.GetUserMedia(constraintsWrong)
|
||||
ms, err := GetUserMedia(constraintsWrong)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, but got nil")
|
||||
}
|
||||
|
||||
// GetUserMedia with correct parameters
|
||||
ms, err = md.GetUserMedia(constraints)
|
||||
ms, err = GetUserMedia(constraints)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
@@ -103,11 +55,11 @@ func TestGetUserMedia(t *testing.T) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
for _, track := range tracks {
|
||||
track.Stop()
|
||||
track.Close()
|
||||
}
|
||||
|
||||
// Stop and retry GetUserMedia
|
||||
ms, err = md.GetUserMedia(constraints)
|
||||
ms, err = GetUserMedia(constraints)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to GetUserMedia after the previsous tracks stopped: %v", err)
|
||||
}
|
||||
@@ -124,104 +76,10 @@ func TestGetUserMedia(t *testing.T) {
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
for _, track := range tracks {
|
||||
track.Stop()
|
||||
track.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type mockTrack struct {
|
||||
codec *webrtc.RTPCodec
|
||||
id string
|
||||
}
|
||||
|
||||
func newMockTrack(codec *webrtc.RTPCodec, id string) *mockTrack {
|
||||
return &mockTrack{
|
||||
codec: codec,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *mockTrack) WriteSample(s media.Sample) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *mockTrack) Codec() *webrtc.RTPCodec {
|
||||
return t.codec
|
||||
}
|
||||
|
||||
func (t *mockTrack) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *mockTrack) Kind() webrtc.RTPCodecType {
|
||||
return t.codec.Type
|
||||
}
|
||||
|
||||
type mockParams struct {
|
||||
codec.BaseParams
|
||||
name string
|
||||
}
|
||||
|
||||
func (params *mockParams) Name() string {
|
||||
return params.name
|
||||
}
|
||||
|
||||
func (params *mockParams) BuildVideoEncoder(r video.Reader, p prop.Media) (codec.ReadCloser, error) {
|
||||
if params.BitRate == 0 {
|
||||
// This is a dummy error to test the failure condition.
|
||||
return nil, errors.New("wrong codec parameter")
|
||||
}
|
||||
return &mockVideoCodec{
|
||||
r: r,
|
||||
closed: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (params *mockParams) BuildAudioEncoder(r audio.Reader, p prop.Media) (codec.ReadCloser, error) {
|
||||
return &mockAudioCodec{
|
||||
r: r,
|
||||
closed: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type mockCodec struct{}
|
||||
|
||||
func (e *mockCodec) SetBitRate(b int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *mockCodec) ForceKeyFrame() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockVideoCodec struct {
|
||||
mockCodec
|
||||
r video.Reader
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (m *mockVideoCodec) Read(b []byte) (int, error) {
|
||||
if _, err := m.r.Read(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (m *mockVideoCodec) Close() error { return nil }
|
||||
|
||||
type mockAudioCodec struct {
|
||||
mockCodec
|
||||
r audio.Reader
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (m *mockAudioCodec) Read(b []byte) (int, error) {
|
||||
if _, err := m.r.Read(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
func (m *mockAudioCodec) Close() error { return nil }
|
||||
|
||||
func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) {
|
||||
filterFn := driver.FilterVideoRecorder()
|
||||
drivers := driver.GetManager().Query(filterFn)
|
||||
@@ -240,37 +98,145 @@ func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) {
|
||||
t.Fatal("expect to get at least 1 property")
|
||||
}
|
||||
expectedProp := driver.Properties()[0]
|
||||
// Since this is a continuous value, bestConstraints should be set with the value that user specified
|
||||
expectedProp.FrameRate = 30.0
|
||||
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: prop.VideoConstraints{
|
||||
// By reducing the width from the driver by a tiny amount, this property should be chosen.
|
||||
// At the same time, we'll be able to find out if the return constraints will be properly set
|
||||
// to the best constraints.
|
||||
Width: prop.Int(expectedProp.Width - 1),
|
||||
Height: prop.Int(expectedProp.Width),
|
||||
FrameFormat: prop.FrameFormat(expectedProp.FrameFormat),
|
||||
FrameRate: prop.Float(30.0),
|
||||
},
|
||||
// By reducing the value from the driver by a tiny amount, this property should be chosen.
|
||||
// At the same time, we'll be able to find out if the return constraints will be properly set
|
||||
// to the best constraints.
|
||||
cases := map[string]struct {
|
||||
width, height int
|
||||
frameFormat frame.Format
|
||||
frameRate float32
|
||||
}{
|
||||
"DifferentWidth": {
|
||||
width: expectedProp.Width - 1,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentHeight": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height - 1,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentFrameFormat": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: frame.FormatI420,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
}
|
||||
|
||||
bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
|
||||
for name, c := range cases {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var vc prop.VideoConstraints
|
||||
|
||||
if c.frameRate >= 0 {
|
||||
vc = prop.VideoConstraints{
|
||||
Width: prop.Int(c.width),
|
||||
Height: prop.Int(c.height),
|
||||
FrameFormat: prop.FrameFormat(c.frameFormat),
|
||||
FrameRate: prop.Float(c.frameRate),
|
||||
}
|
||||
} else {
|
||||
// do not specify the framerate
|
||||
vc = prop.VideoConstraints{
|
||||
Width: prop.Int(c.width),
|
||||
Height: prop.Int(c.height),
|
||||
FrameFormat: prop.FrameFormat(c.frameFormat),
|
||||
}
|
||||
}
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: vc,
|
||||
},
|
||||
}
|
||||
|
||||
bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if driver != bestDriver {
|
||||
t.Fatal("best driver is not expected")
|
||||
}
|
||||
|
||||
s := bestConstraints.selectedMedia
|
||||
if s.Width != expectedProp.Width ||
|
||||
s.Height != expectedProp.Height ||
|
||||
s.FrameFormat != expectedProp.FrameFormat ||
|
||||
s.FrameRate != expectedProp.FrameRate {
|
||||
t.Fatalf("failed to return best constraints\nexpected:\n%v\n\ngot:\n%v", expectedProp, bestConstraints.selectedMedia)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSelectBestDriverConstraintsNoFit(t *testing.T) {
|
||||
filterFn := driver.FilterVideoRecorder()
|
||||
drivers := driver.GetManager().Query(filterFn)
|
||||
if len(drivers) == 0 {
|
||||
t.Fatal("expect to get at least 1 driver")
|
||||
}
|
||||
|
||||
driver := drivers[0]
|
||||
err := driver.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatal("expect to open driver successfully")
|
||||
}
|
||||
defer driver.Close()
|
||||
|
||||
if len(driver.Properties()) == 0 {
|
||||
t.Fatal("expect to get at least 1 property")
|
||||
}
|
||||
expectedProp := driver.Properties()[0]
|
||||
|
||||
cases := map[string]struct {
|
||||
width, height int
|
||||
frameFormat frame.Format
|
||||
frameRate float32
|
||||
}{
|
||||
"DifferentWidth": {
|
||||
width: expectedProp.Width - 1,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentHeight": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height - 1,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentFrameFormat": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: frame.FormatI420,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
}
|
||||
|
||||
if driver != bestDriver {
|
||||
t.Fatal("best driver is not expected")
|
||||
}
|
||||
for name, c := range cases {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: prop.VideoConstraints{
|
||||
Width: prop.IntExact(c.width),
|
||||
Height: prop.IntExact(c.height),
|
||||
FrameFormat: prop.FrameFormatExact(c.frameFormat),
|
||||
FrameRate: prop.FloatExact(c.frameRate),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := bestConstraints.selectedMedia
|
||||
if s.Width != expectedProp.Width ||
|
||||
s.Height != expectedProp.Height ||
|
||||
s.FrameFormat != expectedProp.FrameFormat ||
|
||||
s.FrameRate != expectedProp.FrameRate {
|
||||
t.Fatalf("failed to return best constraints\nexpected:\n%v\n\ngot:\n%v", expectedProp, bestConstraints.selectedMedia)
|
||||
_, _, err := selectBestDriver(filterFn, wantConstraints)
|
||||
if err == nil {
|
||||
t.Fatal("expect to not find a driver that fits the constraints")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -3,88 +3,86 @@ package mediadevices
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// MediaStream is an interface that represents a collection of existing tracks.
|
||||
type MediaStream interface {
|
||||
// GetAudioTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks
|
||||
GetAudioTracks() []Tracker
|
||||
GetAudioTracks() []Track
|
||||
// GetVideoTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks
|
||||
GetVideoTracks() []Tracker
|
||||
GetVideoTracks() []Track
|
||||
// GetTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
|
||||
GetTracks() []Tracker
|
||||
GetTracks() []Track
|
||||
// AddTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack
|
||||
AddTrack(t Tracker)
|
||||
AddTrack(t Track)
|
||||
// RemoveTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack
|
||||
RemoveTrack(t Tracker)
|
||||
RemoveTrack(t Track)
|
||||
}
|
||||
|
||||
type mediaStream struct {
|
||||
trackers map[string]Tracker
|
||||
l sync.RWMutex
|
||||
tracks map[Track]struct{}
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
const rtpCodecTypeDefault webrtc.RTPCodecType = 0
|
||||
const trackTypeDefault webrtc.RTPCodecType = 0
|
||||
|
||||
// NewMediaStream creates a MediaStream interface that's defined in
|
||||
// https://w3c.github.io/mediacapture-main/#dom-mediastream
|
||||
func NewMediaStream(trackers ...Tracker) (MediaStream, error) {
|
||||
m := mediaStream{trackers: make(map[string]Tracker)}
|
||||
func NewMediaStream(tracks ...Track) (MediaStream, error) {
|
||||
m := mediaStream{tracks: make(map[Track]struct{})}
|
||||
|
||||
for _, tracker := range trackers {
|
||||
id := tracker.LocalTrack().ID()
|
||||
if _, ok := m.trackers[id]; !ok {
|
||||
m.trackers[id] = tracker
|
||||
for _, track := range tracks {
|
||||
if _, ok := m.tracks[track]; !ok {
|
||||
m.tracks[track] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetAudioTracks() []Tracker {
|
||||
func (m *mediaStream) GetAudioTracks() []Track {
|
||||
return m.queryTracks(webrtc.RTPCodecTypeAudio)
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetVideoTracks() []Tracker {
|
||||
func (m *mediaStream) GetVideoTracks() []Track {
|
||||
return m.queryTracks(webrtc.RTPCodecTypeVideo)
|
||||
}
|
||||
|
||||
func (m *mediaStream) GetTracks() []Tracker {
|
||||
return m.queryTracks(rtpCodecTypeDefault)
|
||||
func (m *mediaStream) GetTracks() []Track {
|
||||
return m.queryTracks(trackTypeDefault)
|
||||
}
|
||||
|
||||
// queryTracks returns all tracks that are the same kind as t.
|
||||
// If t is 0, which is the default, queryTracks will return all the tracks.
|
||||
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Tracker {
|
||||
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Track {
|
||||
m.l.RLock()
|
||||
defer m.l.RUnlock()
|
||||
|
||||
result := make([]Tracker, 0)
|
||||
for _, tracker := range m.trackers {
|
||||
if tracker.LocalTrack().Kind() == t || t == rtpCodecTypeDefault {
|
||||
result = append(result, tracker)
|
||||
result := make([]Track, 0)
|
||||
for track := range m.tracks {
|
||||
if track.Kind() == t || t == trackTypeDefault {
|
||||
result = append(result, track)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *mediaStream) AddTrack(t Tracker) {
|
||||
func (m *mediaStream) AddTrack(t Track) {
|
||||
m.l.Lock()
|
||||
defer m.l.Unlock()
|
||||
|
||||
id := t.LocalTrack().ID()
|
||||
if _, ok := m.trackers[id]; ok {
|
||||
if _, ok := m.tracks[t]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.trackers[id] = t
|
||||
m.tracks[t] = struct{}{}
|
||||
}
|
||||
|
||||
func (m *mediaStream) RemoveTrack(t Tracker) {
|
||||
func (m *mediaStream) RemoveTrack(t Track) {
|
||||
m.l.Lock()
|
||||
defer m.l.Unlock()
|
||||
|
||||
delete(m.trackers, t.LocalTrack().ID())
|
||||
delete(m.tracks, t)
|
||||
}
|
||||
|
116
mediastream_test.go
Normal file
116
mediastream_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"io"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type mockMediaStreamTrack struct {
|
||||
kind MediaDeviceType
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) ID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) StreamID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) RID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) Kind() webrtc.RTPCodecType {
|
||||
switch track.kind {
|
||||
case AudioInput:
|
||||
return webrtc.RTPCodecTypeAudio
|
||||
case VideoInput:
|
||||
return webrtc.RTPCodecTypeVideo
|
||||
default:
|
||||
panic("invalid track kind")
|
||||
}
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) OnEnded(handler func(error)) {
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) Bind(ctx webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {
|
||||
return webrtc.RTPCodecParameters{}, nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) Unbind(ctx webrtc.TrackLocalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) NewRTPReader(codecName string, ssrc uint32, mtu int) (RTPReadCloser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) NewEncodedReader(codecName string) (EncodedReadCloser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) NewEncodedIOReader(codecName string) (io.ReadCloser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) EncoderController() codec.EncoderController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMediaStreamFilters(t *testing.T) {
|
||||
audioTracks := []Track{
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
}
|
||||
|
||||
videoTracks := []Track{
|
||||
&mockMediaStreamTrack{VideoInput},
|
||||
&mockMediaStreamTrack{VideoInput},
|
||||
&mockMediaStreamTrack{VideoInput},
|
||||
}
|
||||
|
||||
tracks := append(audioTracks, videoTracks...)
|
||||
stream, err := NewMediaStream(tracks...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expect := func(t *testing.T, actual, expected []Track) {
|
||||
if len(actual) != len(expected) {
|
||||
t.Fatalf("%s: Expected to get %d trackers, but got %d trackers", t.Name(), len(expected), len(actual))
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
found := slices.Contains(expected, a)
|
||||
|
||||
if !found {
|
||||
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("GetAudioTracks", func(t *testing.T) {
|
||||
expect(t, stream.GetAudioTracks(), audioTracks)
|
||||
})
|
||||
|
||||
t.Run("GetVideoTracks", func(t *testing.T) {
|
||||
expect(t, stream.GetVideoTracks(), videoTracks)
|
||||
})
|
||||
|
||||
t.Run("GetTracks", func(t *testing.T) {
|
||||
expect(t, stream.GetTracks(), tracks)
|
||||
})
|
||||
}
|
@@ -1,40 +1,18 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
type MediaStreamConstraints struct {
|
||||
Audio MediaOption
|
||||
Video MediaOption
|
||||
Codec *CodecSelector
|
||||
}
|
||||
|
||||
// MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints
|
||||
type MediaTrackConstraints struct {
|
||||
prop.MediaConstraints
|
||||
Enabled bool
|
||||
// VideoEncoderBuilders are codec builders that are used for encoding the video
|
||||
// and later being used for sending the appropriate RTP payload type.
|
||||
//
|
||||
// If one encoder builder fails to build the codec, the next builder will be used,
|
||||
// repeating until a codec builds. If no builders build successfully, an error is returned.
|
||||
VideoEncoderBuilders []codec.VideoEncoderBuilder
|
||||
// AudioEncoderBuilders are codec builders that are used for encoding the audio
|
||||
// and later being used for sending the appropriate RTP payload type.
|
||||
//
|
||||
// If one encoder builder fails to build the codec, the next builder will be used,
|
||||
// repeating until a codec builds. If no builders build successfully, an error is returned.
|
||||
AudioEncoderBuilders []codec.AudioEncoderBuilder
|
||||
// VideoTransform will be used to transform the video that's coming from the driver.
|
||||
// So, basically it'll look like following: driver -> VideoTransform -> codec
|
||||
VideoTransform video.TransformFunc
|
||||
// AudioTransform will be used to transform the audio that's coming from the driver.
|
||||
// So, basically it'll look like following: driver -> AudioTransform -> code
|
||||
AudioTransform audio.TransformFunc
|
||||
|
||||
selectedMedia prop.Media
|
||||
}
|
||||
|
||||
|
35
meta.go
Normal file
35
meta.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
// detectCurrentVideoProp is a small helper to get current video property
|
||||
func detectCurrentVideoProp(broadcaster *video.Broadcaster) (prop.Media, error) {
|
||||
var currentProp prop.Media
|
||||
|
||||
// Since broadcaster has a ring buffer internally, a new reader will either read the last
|
||||
// buffered frame or a new frame from the source. This also implies that no frame will be lost
|
||||
// in any case.
|
||||
metaReader := broadcaster.NewReader(false)
|
||||
metaReader = video.DetectChanges(0, 0, func(p prop.Media) { currentProp = p })(metaReader)
|
||||
_, _, err := metaReader.Read()
|
||||
|
||||
return currentProp, err
|
||||
}
|
||||
|
||||
// detectCurrentAudioProp is a small helper to get current audio property
|
||||
func detectCurrentAudioProp(broadcaster *audio.Broadcaster) (prop.Media, error) {
|
||||
var currentProp prop.Media
|
||||
|
||||
// Since broadcaster has a ring buffer internally, a new reader will either read the last
|
||||
// buffered frame or a new frame from the source. This also implies that no frame will be lost
|
||||
// in any case.
|
||||
metaReader := broadcaster.NewReader(false)
|
||||
metaReader = audio.DetectChanges(0, func(p prop.Media) { currentProp = p })(metaReader)
|
||||
_, _, err := metaReader.Read()
|
||||
|
||||
return currentProp, err
|
||||
}
|
98
meta_test.go
Normal file
98
meta_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package mediadevices
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/wave"
|
||||
)
|
||||
|
||||
func TestDetectCurrentVideoProp(t *testing.T) {
|
||||
resolution := image.Rect(0, 0, 4, 4)
|
||||
first := image.NewRGBA(resolution)
|
||||
first.Pix[0] = 1
|
||||
second := image.NewRGBA(resolution)
|
||||
second.Pix[0] = 2
|
||||
|
||||
isFirst := true
|
||||
source := video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
if isFirst {
|
||||
isFirst = true
|
||||
return first, func() {}, nil
|
||||
} else {
|
||||
return second, func() {}, nil
|
||||
}
|
||||
})
|
||||
|
||||
broadcaster := video.NewBroadcaster(source, nil)
|
||||
|
||||
currentProp, err := detectCurrentVideoProp(broadcaster)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if currentProp.Width != resolution.Dx() {
|
||||
t.Fatalf("Expect the actual width to be %d, but got %d", currentProp.Width, resolution.Dx())
|
||||
}
|
||||
|
||||
if currentProp.Height != resolution.Dy() {
|
||||
t.Fatalf("Expect the actual height to be %d, but got %d", currentProp.Height, resolution.Dy())
|
||||
}
|
||||
|
||||
reader := broadcaster.NewReader(false)
|
||||
img, _, err := reader.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rgba := img.(*image.RGBA)
|
||||
if rgba.Pix[0] != 1 {
|
||||
t.Fatal("Expect the frame after reading the current prop is not the first frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectCurrentAudioProp(t *testing.T) {
|
||||
info := wave.ChunkInfo{
|
||||
Len: 4,
|
||||
Channels: 2,
|
||||
SamplingRate: 48000,
|
||||
}
|
||||
first := wave.NewInt16Interleaved(info)
|
||||
first.Data[0] = 1
|
||||
second := wave.NewInt16Interleaved(info)
|
||||
second.Data[0] = 2
|
||||
|
||||
isFirst := true
|
||||
source := audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
if isFirst {
|
||||
isFirst = true
|
||||
return first, func() {}, nil
|
||||
} else {
|
||||
return second, func() {}, nil
|
||||
}
|
||||
})
|
||||
|
||||
broadcaster := audio.NewBroadcaster(source, nil)
|
||||
|
||||
currentProp, err := detectCurrentAudioProp(broadcaster)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if currentProp.ChannelCount != info.Channels {
|
||||
t.Fatalf("Expect the actual channel count to be %d, but got %d", currentProp.ChannelCount, info.Channels)
|
||||
}
|
||||
|
||||
reader := broadcaster.NewReader(false)
|
||||
chunk, _, err := reader.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
realChunk := chunk.(*wave.Int16Interleaved)
|
||||
if realChunk.Data[0] != 1 {
|
||||
t.Fatal("Expect the chunk after reading the current prop is not the first chunk")
|
||||
}
|
||||
}
|
@@ -27,6 +27,7 @@
|
||||
#define MAX_DEVICES 8
|
||||
#define MAX_PROPERTIES 64
|
||||
#define MAX_DEVICE_UID_CHARS 64
|
||||
#define MAX_DEVICE_NAME_CHARS 64
|
||||
|
||||
typedef const char* STATUS;
|
||||
static STATUS STATUS_OK = (STATUS) NULL;
|
||||
@@ -45,7 +46,8 @@ typedef enum AVBindMediaType {
|
||||
typedef enum AVBindFrameFormat {
|
||||
AVBindFrameFormatI420,
|
||||
AVBindFrameFormatNV21,
|
||||
AVBindFrameFormatYUY2,
|
||||
AVBindFrameFormatNV12,
|
||||
AVBindFrameFormatYUYV,
|
||||
AVBindFrameFormatUYVY,
|
||||
} AVBindFrameFormat;
|
||||
|
||||
@@ -64,6 +66,7 @@ typedef struct AVBindSession AVBindSession, *PAVBindSession;
|
||||
|
||||
typedef struct {
|
||||
char uid[MAX_DEVICE_UID_CHARS + 1];
|
||||
char name[MAX_DEVICE_NAME_CHARS + 1];
|
||||
} AVBindDevice, *PAVBindDevice;
|
||||
|
||||
// AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static
|
||||
|
@@ -1,17 +1,17 @@
|
||||
// MIT License
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2019-2020 Pion
|
||||
//
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -46,6 +46,8 @@
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
static NSString *const UnrecognizedMacOSVersionException = @"UnrecognizedMacOSVersionException";
|
||||
|
||||
@interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
|
||||
@property (readonly) AVBindDataCallback mCallback;
|
||||
@@ -74,32 +76,61 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
|
||||
!CMSampleBufferIsValid(sampleBuffer) ||
|
||||
!CMSampleBufferDataIsReady(sampleBuffer)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
if (imageBuffer == NULL) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
imageBuffer = CVBufferRetain(imageBuffer);
|
||||
|
||||
CVBufferRetain(imageBuffer);
|
||||
CVReturn ret =
|
||||
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (ret != kCVReturnSuccess) {
|
||||
return;
|
||||
CVBufferRelease(imageBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t heightY = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
|
||||
size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
|
||||
|
||||
size_t heightUV = CVPixelBufferGetHeightOfPlane(imageBuffer, 1);
|
||||
size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
|
||||
|
||||
int len = (int)((heightY * bytesPerRowY) + (2 * heightUV * bytesPerRowUV));
|
||||
void *buf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
||||
_mCallback(_mPUserData, buf, len);
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
|
||||
// Handle NV12 special case
|
||||
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
|
||||
if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
|
||||
// Get actual dimensions of image (without padding)
|
||||
size_t width = CVPixelBufferGetWidth(imageBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(imageBuffer);
|
||||
size_t totalSize = /*Y plane*/ width * height + /*UV plane*/ width * height / 2;
|
||||
|
||||
size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
|
||||
size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
|
||||
|
||||
void *mergedBuffer = malloc(totalSize);
|
||||
if (!mergedBuffer) {
|
||||
NSLog(@"Failed to allocate memory for merged buffer");
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
CVBufferRelease(imageBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate data where we know it should end to strip padding
|
||||
void *yPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
memcpy(mergedBuffer + row * width, yPlaneBuf + row * bytesPerRowY, width);
|
||||
}
|
||||
|
||||
void *uvPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
|
||||
for (size_t row = 0; row < height / 2; ++row) {
|
||||
memcpy(mergedBuffer + width * height + row * width, uvPlaneBuf + row * bytesPerRowUV, width);
|
||||
}
|
||||
|
||||
_mCallback(_mPUserData, mergedBuffer, (int)totalSize);
|
||||
free(mergedBuffer);
|
||||
} else {
|
||||
void *buf = CVPixelBufferGetBaseAddress(imageBuffer);
|
||||
size_t dataSize = CVPixelBufferGetDataSize(imageBuffer);
|
||||
_mCallback(_mPUserData, buf, (int)dataSize);
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
CVBufferRelease(imageBuffer);
|
||||
}
|
||||
|
||||
@@ -133,13 +164,21 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
|
||||
STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
// Useful mapping reference from ffmpeg:
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/c810a9502cebe32e1dd08ee3d0d17053dde44aa9/libavdevice/avfoundation.m#L53-L80
|
||||
switch (format) {
|
||||
case AVBindFrameFormatNV21:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
|
||||
case AVBindFrameFormatI420:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8Planar;
|
||||
break;
|
||||
case AVBindFrameFormatNV12:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
|
||||
break;
|
||||
case AVBindFrameFormatUYVY:
|
||||
*pFourCC = kCVPixelFormatType_422YpCbCr8;
|
||||
break;
|
||||
case AVBindFrameFormatYUYV:
|
||||
*pFourCC = kCVPixelFormatType_422YpCbCr8_yuvs;
|
||||
break;
|
||||
// TODO: Add the rest of frame formats
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
@@ -150,15 +189,22 @@ STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
|
||||
STATUS frameFormatFromFourCC(FourCharCode fourCC, AVBindFrameFormat *pFormat) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
switch (fourCC) {
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
*pFormat = AVBindFrameFormatNV21;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8:
|
||||
*pFormat = AVBindFrameFormatUYVY;
|
||||
break;
|
||||
case kCVPixelFormatType_420YpCbCr8Planar:
|
||||
*pFormat = AVBindFrameFormatI420;
|
||||
break;
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
||||
*pFormat = AVBindFrameFormatNV12;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8:
|
||||
*pFormat = AVBindFrameFormatUYVY;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8_yuvs:
|
||||
*pFormat = AVBindFrameFormatYUYV;
|
||||
break;
|
||||
// TODO: Add the rest of frame formats
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
}
|
||||
return retStatus;
|
||||
}
|
||||
@@ -170,34 +216,53 @@ STATUS AVBindDevices(AVBindMediaType mediaType, PAVBindDevice *ppDevices, int *p
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(mediaType == AVBindMediaTypeVideo || mediaType == AVBindMediaTypeAudio, STATUS_UNSUPPORTED_MEDIA_TYPE);
|
||||
CHK(ppDevices != NULL && pLen != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
PAVBindDevice pDevice;
|
||||
AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio;
|
||||
NSArray *refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeBuiltInMicrophone,
|
||||
AVCaptureDeviceTypeExternalUnknown
|
||||
];
|
||||
|
||||
NSArray *refAllTypes;
|
||||
#if defined(MAC_OS_VERSION_14_0)
|
||||
if (@available(macOS 14.0, *)) {
|
||||
refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeMicrophone,
|
||||
AVCaptureDeviceTypeExternal,
|
||||
];
|
||||
} else {
|
||||
@throw [NSException exceptionWithName:UnrecognizedMacOSVersionException
|
||||
reason:@"Unrecognized or unsupported macOS version detected."
|
||||
userInfo:nil];
|
||||
}
|
||||
#else
|
||||
refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeBuiltInMicrophone,
|
||||
AVCaptureDeviceTypeExternalUnknown,
|
||||
];
|
||||
#endif
|
||||
|
||||
AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession
|
||||
discoverySessionWithDeviceTypes: refAllTypes
|
||||
mediaType: _mediaType
|
||||
position: AVCaptureDevicePositionUnspecified];
|
||||
|
||||
discoverySessionWithDeviceTypes: refAllTypes
|
||||
mediaType: _mediaType
|
||||
position: AVCaptureDevicePositionUnspecified];
|
||||
|
||||
int i = 0;
|
||||
for (AVCaptureDevice *refDevice in refSession.devices) {
|
||||
if (i >= MAX_DEVICES) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
pDevice = devices + i;
|
||||
strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS);
|
||||
pDevice->uid[MAX_DEVICE_UID_CHARS] = '\0';
|
||||
strncpy(pDevice->name, refDevice.localizedName.UTF8String, MAX_DEVICE_NAME_CHARS);
|
||||
pDevice->name[MAX_DEVICE_NAME_CHARS] = '\0';
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
*ppDevices = devices;
|
||||
*pLen = i;
|
||||
|
||||
|
||||
cleanup:
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
@@ -217,7 +282,7 @@ STATUS AVBindSessionInit(AVBindDevice device, PAVBindSession *ppSessionResult) {
|
||||
pSession->device = device;
|
||||
pSession->refCaptureSession = NULL;
|
||||
*ppSessionResult = pSession;
|
||||
|
||||
|
||||
cleanup:
|
||||
return retStatus;
|
||||
}
|
||||
@@ -244,15 +309,15 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
STATUS retStatus = STATUS_OK;
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(pSession != NULL && dataCallback != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
AVCaptureDeviceInput *refInput;
|
||||
NSError *refErr = NULL;
|
||||
NSString *refUID = [NSString stringWithUTF8String: pSession->device.uid];
|
||||
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refUID];
|
||||
|
||||
|
||||
refInput = [[AVCaptureDeviceInput alloc] initWithDevice: refDevice error: &refErr];
|
||||
CHK(refErr == NULL, STATUS_DEVICE_INIT_FAILED);
|
||||
|
||||
|
||||
AVCaptureSession *refCaptureSession = [[AVCaptureSession alloc] init];
|
||||
refCaptureSession.sessionPreset = AVCaptureSessionPresetMedium;
|
||||
[refCaptureSession addInput: refInput];
|
||||
@@ -261,7 +326,7 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
VideoDataDelegate *pDelegate = [[VideoDataDelegate alloc]
|
||||
init: dataCallback
|
||||
withUserData: pUserData];
|
||||
|
||||
|
||||
AVCaptureVideoDataOutput *pOutput = [[AVCaptureVideoDataOutput alloc] init];
|
||||
FourCharCode fourCC;
|
||||
CHK_STATUS(frameFormatToFourCC(property.frameFormat, &fourCC));
|
||||
@@ -279,10 +344,10 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
} else {
|
||||
// TODO: implement audio pipeline
|
||||
}
|
||||
|
||||
|
||||
pSession->refCaptureSession = [refCaptureSession retain];
|
||||
[refCaptureSession startRunning];
|
||||
|
||||
|
||||
cleanup:
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
@@ -293,20 +358,30 @@ STATUS AVBindSessionClose(PAVBindSession pSession) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
CHK(pSession != NULL, STATUS_NULL_ARG);
|
||||
CHK(pSession->refCaptureSession != NULL, STATUS_OK);
|
||||
|
||||
|
||||
[pSession->refCaptureSession stopRunning];
|
||||
[pSession->refCaptureSession release];
|
||||
pSession->refCaptureSession = NULL;
|
||||
|
||||
|
||||
cleanup:
|
||||
return retStatus;
|
||||
}
|
||||
|
||||
static NSString* FourCCString(FourCharCode code) {
|
||||
NSString *result = [NSString stringWithFormat:@"%c%c%c%c",
|
||||
(code >> 24) & 0xff,
|
||||
(code >> 16) & 0xff,
|
||||
(code >> 8) & 0xff,
|
||||
code & 0xff];
|
||||
NSCharacterSet *characterSet = [NSCharacterSet whitespaceCharacterSet];
|
||||
return [result stringByTrimmingCharactersInSet:characterSet];
|
||||
}
|
||||
|
||||
STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *ppProperties, int *pLen) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(pSession != NULL && ppProperties != NULL && pLen != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
NSString *refDeviceUID = [NSString stringWithUTF8String: pSession->device.uid];
|
||||
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refDeviceUID];
|
||||
FourCharCode fourCC;
|
||||
@@ -319,15 +394,17 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
|
||||
for (AVCaptureDeviceFormat *refFormat in refDevice.formats) {
|
||||
// TODO: Probably gives a warn to the user
|
||||
if (len >= MAX_PROPERTIES) {
|
||||
NSLog(@"[WARNING] skipping the rest of properties due to MAX_PROPERTIES");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if ([refFormat.mediaType isEqual:AVMediaTypeVideo]) {
|
||||
fourCC = CMFormatDescriptionGetMediaSubType(refFormat.formatDescription);
|
||||
if (frameFormatFromFourCC(fourCC, &pProperty->frameFormat) != STATUS_OK) {
|
||||
NSLog(@"[WARNING] skipping %@ %dx%d since it's not supported", FourCCString(fourCC), videoDimensions.width, videoDimensions.height);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
videoFormat = (CMVideoFormatDescriptionRef) refFormat.formatDescription;
|
||||
videoDimensions = CMVideoFormatDescriptionGetDimensions(videoFormat);
|
||||
pProperty->height = videoDimensions.height;
|
||||
@@ -335,16 +412,16 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
|
||||
} else {
|
||||
// TODO: Get audio properties
|
||||
}
|
||||
|
||||
|
||||
pProperty++;
|
||||
len++;
|
||||
}
|
||||
|
||||
|
||||
*ppProperties = pSession->properties;
|
||||
*pLen = len;
|
||||
|
||||
|
||||
cleanup:
|
||||
|
||||
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package avfoundation
|
||||
|
||||
// extern void onData(void*, void*, int);
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
@@ -18,11 +17,10 @@ type handleID int
|
||||
|
||||
//export onData
|
||||
func onData(userData unsafe.Pointer, buf unsafe.Pointer, length C.int) {
|
||||
data := C.GoBytes(buf, length)
|
||||
|
||||
handleNum := (*C.int)(userData)
|
||||
cb, ok := lookup(handleID(*handleNum))
|
||||
if ok {
|
||||
data := C.GoBytes(buf, length)
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
|
@@ -11,8 +11,10 @@ package avfoundation
|
||||
// }
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
@@ -32,6 +34,7 @@ type Device struct {
|
||||
// UID is a unique identifier for a device
|
||||
UID string
|
||||
cDevice C.AVBindDevice
|
||||
Name string
|
||||
}
|
||||
|
||||
func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
||||
@@ -40,8 +43,10 @@ func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
||||
return C.AVBindFrameFormatI420, true
|
||||
case frame.FormatNV21:
|
||||
return C.AVBindFrameFormatNV21, true
|
||||
case frame.FormatYUY2:
|
||||
return C.AVBindFrameFormatYUY2, true
|
||||
case frame.FormatNV12:
|
||||
return C.AVBindFrameFormatNV12, true
|
||||
case frame.FormatYUYV:
|
||||
return C.AVBindFrameFormatYUYV, true
|
||||
case frame.FormatUYVY:
|
||||
return C.AVBindFrameFormatUYVY, true
|
||||
default:
|
||||
@@ -55,8 +60,10 @@ func frameFormatFromAVBind(f C.AVBindFrameFormat) (frame.Format, bool) {
|
||||
return frame.FormatI420, true
|
||||
case C.AVBindFrameFormatNV21:
|
||||
return frame.FormatNV21, true
|
||||
case C.AVBindFrameFormatYUY2:
|
||||
return frame.FormatYUY2, true
|
||||
case C.AVBindFrameFormatNV12:
|
||||
return frame.FormatNV12, true
|
||||
case C.AVBindFrameFormatYUYV:
|
||||
return frame.FormatYUYV, true
|
||||
case C.AVBindFrameFormatUYVY:
|
||||
return frame.FormatUYVY, true
|
||||
default:
|
||||
@@ -81,6 +88,7 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
||||
for i := range devices {
|
||||
devices[i].UID = C.GoString(&cDevices[i].uid[0])
|
||||
devices[i].cDevice = cDevices[i]
|
||||
devices[i].Name = C.GoString(&cDevices[i].name[0])
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
@@ -89,9 +97,13 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
||||
// ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the
|
||||
// the underlying callback can be retrieved by calling Read.
|
||||
type ReadCloser struct {
|
||||
dataChan chan []byte
|
||||
id handleID
|
||||
onClose func()
|
||||
dataChan chan []byte
|
||||
id handleID
|
||||
onClose func()
|
||||
cancelCtx context.Context
|
||||
cancelFunc func()
|
||||
closeWG sync.WaitGroup
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newReadCloser(onClose func()) *ReadCloser {
|
||||
@@ -99,38 +111,62 @@ func newReadCloser(onClose func()) *ReadCloser {
|
||||
rc.dataChan = make(chan []byte, 1)
|
||||
rc.onClose = onClose
|
||||
rc.id = register(rc.dataCb)
|
||||
cancelCtx, cancelFunc := context.WithCancel(context.Background())
|
||||
rc.cancelCtx = cancelCtx
|
||||
rc.cancelFunc = cancelFunc
|
||||
return &rc
|
||||
}
|
||||
|
||||
func (rc *ReadCloser) dataCb(data []byte) {
|
||||
rc.closeWG.Add(1)
|
||||
defer rc.closeWG.Done()
|
||||
|
||||
// TODO: add a policy for slow reader
|
||||
rc.dataChan <- data
|
||||
if rc.cancelCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
// Use the Done channel to avoid waiting for new data from closed camera
|
||||
case <-rc.cancelCtx.Done():
|
||||
case rc.dataChan <- data:
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads raw data, the format is determined by the media type and property:
|
||||
// - For video, each call will return a frame.
|
||||
// - For audio, each call will return a chunk which its size configured by Latency
|
||||
func (rc *ReadCloser) Read() ([]byte, error) {
|
||||
func (rc *ReadCloser) Read() ([]byte, func(), error) {
|
||||
data, ok := <-rc.dataChan
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
return data, nil
|
||||
return data, func() {}, nil
|
||||
}
|
||||
|
||||
// Close closes the capturing session, and no data will flow anymore
|
||||
func (rc *ReadCloser) Close() {
|
||||
rc.lock.Lock()
|
||||
defer rc.lock.Unlock()
|
||||
|
||||
if rc.cancelCtx.Err() != nil {
|
||||
return // already closed
|
||||
}
|
||||
|
||||
if rc.onClose != nil {
|
||||
rc.onClose()
|
||||
}
|
||||
close(rc.dataChan)
|
||||
rc.cancelFunc()
|
||||
unregister(rc.id)
|
||||
rc.closeWG.Wait()
|
||||
close(rc.dataChan)
|
||||
}
|
||||
|
||||
// Session represents a capturing session.
|
||||
type Session struct {
|
||||
device Device
|
||||
cSession C.PAVBindSession
|
||||
lock sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewSession creates a new capturing session
|
||||
@@ -148,6 +184,13 @@ func NewSession(device Device) (*Session, error) {
|
||||
|
||||
// Close stops capturing session and frees up resources
|
||||
func (session *Session) Close() error {
|
||||
session.lock.Lock()
|
||||
defer session.lock.Unlock()
|
||||
if session.closed {
|
||||
return nil
|
||||
}
|
||||
session.closed = true
|
||||
|
||||
if session.cSession == nil {
|
||||
return nil
|
||||
}
|
||||
|
48
pkg/codec/bitrate_tracker.go
Normal file
48
pkg/codec/bitrate_tracker.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BitrateTracker struct {
|
||||
windowSize time.Duration
|
||||
buffer []int
|
||||
times []time.Time
|
||||
}
|
||||
|
||||
func NewBitrateTracker(windowSize time.Duration) *BitrateTracker {
|
||||
return &BitrateTracker{
|
||||
windowSize: windowSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (bt *BitrateTracker) AddFrame(sizeBytes int, timestamp time.Time) {
|
||||
bt.buffer = append(bt.buffer, sizeBytes)
|
||||
bt.times = append(bt.times, timestamp)
|
||||
|
||||
// Remove old entries outside the window
|
||||
cutoff := timestamp.Add(-bt.windowSize)
|
||||
i := 0
|
||||
for ; i < len(bt.times); i++ {
|
||||
if bt.times[i].After(cutoff) {
|
||||
break
|
||||
}
|
||||
}
|
||||
bt.buffer = bt.buffer[i:]
|
||||
bt.times = bt.times[i:]
|
||||
}
|
||||
|
||||
func (bt *BitrateTracker) GetBitrate() float64 {
|
||||
if len(bt.times) < 2 {
|
||||
return 0
|
||||
}
|
||||
totalBytes := 0
|
||||
for _, b := range bt.buffer {
|
||||
totalBytes += b
|
||||
}
|
||||
duration := bt.times[len(bt.times)-1].Sub(bt.times[0]).Seconds()
|
||||
if duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(totalBytes*8) / duration // bits per second
|
||||
}
|
19
pkg/codec/bitrate_tracker_test.go
Normal file
19
pkg/codec/bitrate_tracker_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBitrateTracker(t *testing.T) {
|
||||
packetSize := 1000
|
||||
bt := NewBitrateTracker(time.Second)
|
||||
bt.AddFrame(packetSize, time.Now())
|
||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*100))
|
||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*999))
|
||||
eps := float64(packetSize*8) / 10
|
||||
if got, want := bt.GetBitrate(), float64(packetSize*8)*3; math.Abs(got-want) > eps {
|
||||
t.Fatalf("GetBitrate() = %v, want %v (|diff| <= %v)", got, want, eps)
|
||||
}
|
||||
}
|
@@ -1,21 +1,137 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future.
|
||||
type RTPCodec struct {
|
||||
webrtc.RTPCodecParameters
|
||||
rtp.Payloader
|
||||
|
||||
// Latency of static frame size codec.
|
||||
Latency time.Duration
|
||||
}
|
||||
|
||||
// NewRTPH264Codec is a helper to create an H264 codec
|
||||
func NewRTPH264Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH264,
|
||||
ClockRate: 90000,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 125,
|
||||
},
|
||||
Payloader: &codecs.H264Payloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRTPH265Codec is a helper to create an H265 codec
|
||||
func NewRTPH265Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeH265,
|
||||
ClockRate: 90000,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: "",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 116,
|
||||
},
|
||||
Payloader: &codecs.H265Payloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRTPVP8Codec is a helper to create an VP8 codec
|
||||
func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeVP8,
|
||||
ClockRate: 90000,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: "",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 96,
|
||||
},
|
||||
Payloader: &codecs.VP8Payloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRTPVP9Codec is a helper to create an VP9 codec
|
||||
func NewRTPVP9Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeVP9,
|
||||
ClockRate: 90000,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: "",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 98,
|
||||
},
|
||||
Payloader: &codecs.VP9Payloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRTPAV1Codec is a helper to create an AV1 codec
|
||||
func NewRTPAV1Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeAV1,
|
||||
ClockRate: 90000,
|
||||
Channels: 0,
|
||||
SDPFmtpLine: "level-idx=5;profile=0;tier=0",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 99,
|
||||
},
|
||||
Payloader: &codecs.AV1Payloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewRTPOpusCodec is a helper to create an Opus codec
|
||||
func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
RTPCodecParameters: webrtc.RTPCodecParameters{
|
||||
RTPCodecCapability: webrtc.RTPCodecCapability{
|
||||
MimeType: webrtc.MimeTypeOpus,
|
||||
ClockRate: 48000,
|
||||
Channels: 2,
|
||||
SDPFmtpLine: "minptime=10;useinbandfec=1",
|
||||
RTCPFeedback: nil,
|
||||
},
|
||||
PayloadType: 111,
|
||||
},
|
||||
Payloader: &codecs.OpusPayloader{},
|
||||
}
|
||||
}
|
||||
|
||||
// AudioEncoderBuilder is the interface that wraps basic operations that are
|
||||
// necessary to build the audio encoder.
|
||||
//
|
||||
// This interface is for codec implementors to provide codec specific params,
|
||||
// but still giving generality for the users.
|
||||
type AudioEncoderBuilder interface {
|
||||
// Name represents the codec name
|
||||
Name() string
|
||||
// RTPCodec represents the codec metadata
|
||||
RTPCodec() *RTPCodec
|
||||
// BuildAudioEncoder builds audio encoder by given media params and audio input
|
||||
BuildAudioEncoder(r audio.Reader, p prop.Media) (ReadCloser, error)
|
||||
}
|
||||
@@ -26,20 +142,58 @@ type AudioEncoderBuilder interface {
|
||||
// This interface is for codec implementors to provide codec specific params,
|
||||
// but still giving generality for the users.
|
||||
type VideoEncoderBuilder interface {
|
||||
// Name represents the codec name
|
||||
Name() string
|
||||
// RTPCodec represents the codec metadata
|
||||
RTPCodec() *RTPCodec
|
||||
// BuildVideoEncoder builds video encoder by given media params and video input
|
||||
BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error)
|
||||
}
|
||||
|
||||
// ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame
|
||||
// ReadCloser is an io.ReadCloser with a controller
|
||||
type ReadCloser interface {
|
||||
io.ReadCloser
|
||||
Read() (b []byte, release func(), err error)
|
||||
Close() error
|
||||
Controllable
|
||||
}
|
||||
|
||||
type VideoDecoderBuilder interface {
|
||||
BuildVideoDecoder(r io.Reader, p prop.Media) (VideoDecoder, error)
|
||||
}
|
||||
|
||||
type VideoDecoder interface {
|
||||
Read() (image.Image, func(), error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// EncoderController is the interface allowing to control the encoder behaviour after it's initialisation.
|
||||
// It will possibly have common control method in the future.
|
||||
// A controller can have optional methods represented by *Controller interfaces
|
||||
type EncoderController any
|
||||
|
||||
// Controllable is a interface representing a encoder which can be controlled
|
||||
// after it's initialisation with an EncoderController
|
||||
type Controllable interface {
|
||||
Controller() EncoderController
|
||||
}
|
||||
|
||||
// KeyFrameController is a interface representing an encoder that can be forced to produce key frame on demand
|
||||
type KeyFrameController interface {
|
||||
EncoderController
|
||||
// ForceKeyFrame forces the next frame to be a keyframe, aka intra-frame.
|
||||
ForceKeyFrame() error
|
||||
}
|
||||
|
||||
// BitRateController is a interface representing an encoder which can have a variable bit rate
|
||||
type BitRateController interface {
|
||||
EncoderController
|
||||
// SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted
|
||||
// but this also means that the quality will also be lower.
|
||||
SetBitRate(int) error
|
||||
// ForceKeyFrame forces the next frame to be a keyframe, aka intra-frame.
|
||||
ForceKeyFrame() error
|
||||
}
|
||||
|
||||
type QPController interface {
|
||||
EncoderController
|
||||
// DynamicQPControl adjusts the QP of the encoder based on the current and target bitrate
|
||||
DynamicQPControl(currentBitrate int, targetBitrate int) error
|
||||
}
|
||||
|
||||
// BaseParams represents an codec's encoding properties
|
||||
|
159
pkg/codec/internal/codectest/codectest.go
Normal file
159
pkg/codec/internal/codectest/codectest.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Package codectest provides shared test for codec implementations.
|
||||
package codectest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices/pkg/wave"
|
||||
)
|
||||
|
||||
func assertNoPanic(t *testing.T, fn func() error, msg string) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("panic: %v: %s", r, msg)
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
}
|
||||
|
||||
func AudioEncoderSimpleReadTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media, w wave.Audio) {
|
||||
var eof bool
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
if eof {
|
||||
return nil, nil, io.EOF
|
||||
}
|
||||
return w, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 16; i++ {
|
||||
b, release, err := enc.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
t.Fatal("Encoded frame is empty")
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
eof = true
|
||||
if _, _, err := enc.Read(); err != io.EOF {
|
||||
t.Fatalf("Expected EOF, got %v", err)
|
||||
}
|
||||
|
||||
if err := enc.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderSimpleReadTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media, img image.Image) {
|
||||
var eof bool
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
if eof {
|
||||
return nil, nil, io.EOF
|
||||
}
|
||||
return img, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 16; i++ {
|
||||
b, release, err := enc.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
t.Errorf("Encoded frame is empty (%d)", i)
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
eof = true
|
||||
if _, _, err := enc.Read(); err != io.EOF {
|
||||
t.Fatalf("Expected EOF, got %v", err)
|
||||
}
|
||||
|
||||
if err := enc.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AudioEncoderCloseTwiceTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media) {
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
return nil, nil, io.EOF
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on first Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, enc.Close, "on second Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderCloseTwiceTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media) {
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
return nil, nil, io.EOF
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on first Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, enc.Close, "on second Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AudioEncoderReadAfterCloseTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media, w wave.Audio) {
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
return w, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, func() error {
|
||||
_, _, err := enc.Read()
|
||||
return err
|
||||
}, "on Read()"); err != io.EOF {
|
||||
t.Fatalf("Expected: %v, got: %v", io.EOF, err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderReadAfterCloseTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media, img image.Image) {
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
return img, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, func() error {
|
||||
_, _, err := enc.Read()
|
||||
return err
|
||||
}, "on Read()"); err != io.EOF {
|
||||
t.Fatalf("Expected: %v, got: %v", io.EOF, err)
|
||||
}
|
||||
}
|
196
pkg/codec/mmal/bridge.h
Normal file
196
pkg/codec/mmal/bridge.h
Normal file
@@ -0,0 +1,196 @@
|
||||
#include <interface/mmal/mmal.h>
|
||||
#include <interface/mmal/util/mmal_default_components.h>
|
||||
#include <interface/mmal/util/mmal_util_params.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define CHK(__status, __msg) \
|
||||
do { \
|
||||
status.code = __status; \
|
||||
if (status.code != MMAL_SUCCESS) { \
|
||||
status.msg = __msg; \
|
||||
goto CleanUp; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
typedef struct Status {
|
||||
MMAL_STATUS_T code;
|
||||
const char *msg;
|
||||
} Status;
|
||||
|
||||
typedef struct Slice {
|
||||
uint8_t *data;
|
||||
int len;
|
||||
} Slice;
|
||||
|
||||
typedef struct Params {
|
||||
int width, height;
|
||||
uint32_t bitrate;
|
||||
uint32_t key_frame_interval;
|
||||
} Params;
|
||||
|
||||
typedef struct Encoder {
|
||||
MMAL_COMPONENT_T *component;
|
||||
MMAL_PORT_T *port_in, *port_out;
|
||||
MMAL_QUEUE_T *queue_out;
|
||||
MMAL_POOL_T *pool_in, *pool_out;
|
||||
} Encoder;
|
||||
|
||||
Status enc_new(Params, Encoder *);
|
||||
Status enc_encode(Encoder *, Slice y, Slice cb, Slice cr, MMAL_BUFFER_HEADER_T **);
|
||||
Status enc_close(Encoder *);
|
||||
|
||||
static void encoder_in_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer) { mmal_buffer_header_release(buffer); }
|
||||
|
||||
static void encoder_out_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer) {
|
||||
MMAL_QUEUE_T *queue = (MMAL_QUEUE_T *)port->userdata;
|
||||
mmal_queue_put(queue, buffer);
|
||||
}
|
||||
|
||||
Status enc_new(Params params, Encoder *encoder) {
|
||||
Status status = {0};
|
||||
bool created = false;
|
||||
|
||||
memset(encoder, 0, sizeof(Encoder));
|
||||
|
||||
CHK(mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_ENCODER, &encoder->component),
|
||||
"Failed to create video encoder component");
|
||||
created = true;
|
||||
|
||||
encoder->port_in = encoder->component->input[0];
|
||||
encoder->port_in->format->type = MMAL_ES_TYPE_VIDEO;
|
||||
encoder->port_in->format->encoding = MMAL_ENCODING_I420;
|
||||
encoder->port_in->format->es->video.width = params.width;
|
||||
encoder->port_in->format->es->video.height = params.height;
|
||||
encoder->port_in->format->es->video.par.num = 1;
|
||||
encoder->port_in->format->es->video.par.den = 1;
|
||||
encoder->port_in->format->es->video.crop.x = 0;
|
||||
encoder->port_in->format->es->video.crop.y = 0;
|
||||
encoder->port_in->format->es->video.crop.width = params.width;
|
||||
encoder->port_in->format->es->video.crop.height = params.height;
|
||||
CHK(mmal_port_format_commit(encoder->port_in), "Failed to commit input port format");
|
||||
|
||||
encoder->port_out = encoder->component->output[0];
|
||||
encoder->port_out->format->type = MMAL_ES_TYPE_VIDEO;
|
||||
encoder->port_out->format->encoding = MMAL_ENCODING_H264;
|
||||
encoder->port_out->format->bitrate = params.bitrate;
|
||||
CHK(mmal_port_format_commit(encoder->port_out), "Failed to commit output port format");
|
||||
|
||||
MMAL_PARAMETER_VIDEO_PROFILE_T encoder_param_profile = {0};
|
||||
encoder_param_profile.hdr.id = MMAL_PARAMETER_PROFILE;
|
||||
encoder_param_profile.hdr.size = sizeof(encoder_param_profile);
|
||||
encoder_param_profile.profile[0].profile = MMAL_VIDEO_PROFILE_H264_BASELINE;
|
||||
encoder_param_profile.profile[0].level = MMAL_VIDEO_LEVEL_H264_42;
|
||||
CHK(mmal_port_parameter_set(encoder->port_out, &encoder_param_profile.hdr), "Failed to set encoder profile param");
|
||||
|
||||
CHK(mmal_port_parameter_set_uint32(encoder->port_out, MMAL_PARAMETER_INTRAPERIOD, params.key_frame_interval),
|
||||
"Failed to set intra period param");
|
||||
|
||||
MMAL_PARAMETER_VIDEO_RATECONTROL_T encoder_param_rate_control = {0};
|
||||
encoder_param_rate_control.hdr.id = MMAL_PARAMETER_RATECONTROL;
|
||||
encoder_param_rate_control.hdr.size = sizeof(encoder_param_rate_control);
|
||||
encoder_param_rate_control.control = MMAL_VIDEO_RATECONTROL_VARIABLE;
|
||||
CHK(mmal_port_parameter_set(encoder->port_out, &encoder_param_rate_control.hdr), "Failed to set rate control param");
|
||||
|
||||
// Some decoders expect SPS/PPS headers to be added to every frame
|
||||
CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_INLINE_HEADER, MMAL_TRUE),
|
||||
"Failed to set inline header param");
|
||||
|
||||
CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_HEADERS_WITH_FRAME, MMAL_TRUE),
|
||||
"Failed to set headers with frame param");
|
||||
|
||||
/* FIXME: Somehow this flag is broken? When this flag is on, the encoder will get stuck.
|
||||
// Since our use case is mainly for real time streaming, the encoder should optimized for low latency
|
||||
CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_H264_LOW_LATENCY, MMAL_TRUE),
|
||||
"Failed to set low latency param");
|
||||
*/
|
||||
|
||||
// Now we know the format of both ports and the requirements of the encoder, we can create
|
||||
// our buffer headers and their associated memory buffers. We use the buffer pool API for this.
|
||||
encoder->port_in->buffer_num = encoder->port_in->buffer_num_min;
|
||||
// mmal calculates recommended size that's big enough to store all of the pixels
|
||||
encoder->port_in->buffer_size = encoder->port_in->buffer_size_recommended;
|
||||
encoder->pool_in = mmal_pool_create(encoder->port_in->buffer_num, encoder->port_in->buffer_size);
|
||||
encoder->port_out->buffer_num = encoder->port_out->buffer_num_min;
|
||||
encoder->port_out->buffer_size = encoder->port_out->buffer_size_recommended;
|
||||
encoder->pool_out = mmal_pool_create(encoder->port_out->buffer_num, encoder->port_out->buffer_size);
|
||||
|
||||
// Create a queue to store our encoded video frames. The callback we will get when
|
||||
// a frame has been encoded will put the frame into this queue.
|
||||
encoder->queue_out = mmal_queue_create();
|
||||
encoder->port_out->userdata = (void *)encoder->queue_out;
|
||||
|
||||
// Enable all the input port and the output port.
|
||||
// The callback specified here is the function which will be called when the buffer header
|
||||
// we sent to the component has been processed.
|
||||
CHK(mmal_port_enable(encoder->port_in, encoder_in_cb), "Failed to enable input port");
|
||||
CHK(mmal_port_enable(encoder->port_out, encoder_out_cb), "Failed to enable output port");
|
||||
|
||||
// Enable the component. Components will only process data when they are enabled.
|
||||
CHK(mmal_component_enable(encoder->component), "Failed to enable component");
|
||||
|
||||
CleanUp:
|
||||
|
||||
if (status.code != MMAL_SUCCESS) {
|
||||
if (created) {
|
||||
enc_close(encoder);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// enc_encode encodes y, cb, cr. The encoded frame is going to be stored in encoded_buffer.
|
||||
// IMPORTANT: the caller is responsible to release the ownership of encoded_buffer
|
||||
Status enc_encode(Encoder *encoder, Slice y, Slice cb, Slice cr, MMAL_BUFFER_HEADER_T **encoded_buffer) {
|
||||
Status status = {0};
|
||||
MMAL_BUFFER_HEADER_T *buffer;
|
||||
uint32_t required_size;
|
||||
|
||||
// buffer should always be available since the encoding process is blocking
|
||||
buffer = mmal_queue_get(encoder->pool_in->queue);
|
||||
assert(buffer != NULL);
|
||||
// buffer->data should've been allocated with enough memory to contain a frame by pool_in
|
||||
required_size = y.len + cb.len + cr.len;
|
||||
assert(buffer->alloc_size >= required_size);
|
||||
memcpy(buffer->data, y.data, y.len);
|
||||
memcpy(buffer->data + y.len, cb.data, cb.len);
|
||||
memcpy(buffer->data + y.len + cb.len, cr.data, cr.len);
|
||||
buffer->length = required_size;
|
||||
CHK(mmal_port_send_buffer(encoder->port_in, buffer), "Failed to send filled buffer to input port");
|
||||
|
||||
while (1) {
|
||||
// Send empty buffers to the output port to allow the encoder to start
|
||||
// producing frames as soon as it gets input data
|
||||
while ((buffer = mmal_queue_get(encoder->pool_out->queue)) != NULL) {
|
||||
CHK(mmal_port_send_buffer(encoder->port_out, buffer), "Failed to send empty buffers to output port");
|
||||
}
|
||||
|
||||
while ((buffer = mmal_queue_wait(encoder->queue_out)) != NULL) {
|
||||
if ((buffer->flags & MMAL_BUFFER_HEADER_FLAG_FRAME_END) != 0) {
|
||||
*encoded_buffer = buffer;
|
||||
goto CleanUp;
|
||||
}
|
||||
|
||||
mmal_buffer_header_release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp:
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
Status enc_close(Encoder *encoder) {
|
||||
Status status = {0};
|
||||
|
||||
mmal_pool_destroy(encoder->pool_out);
|
||||
mmal_pool_destroy(encoder->pool_in);
|
||||
mmal_queue_destroy(encoder->queue_out);
|
||||
mmal_component_destroy(encoder->component);
|
||||
|
||||
CleanUp:
|
||||
|
||||
return status;
|
||||
}
|
110
pkg/codec/mmal/mmal.go
Normal file
110
pkg/codec/mmal/mmal.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Package mmal implements a hardware accelerated H264 encoder for raspberry pi.
|
||||
// This package requires libmmal headers and libraries to be built.
|
||||
// Reference: https://github.com/raspberrypi/userland/tree/master/interface/mmal
|
||||
package mmal
|
||||
|
||||
// #cgo CFLAGS: -I/opt/vc/include
|
||||
// #cgo LDFLAGS: -L/opt/vc/lib -lmmal -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host -lvcsm -lvcos
|
||||
// #include "bridge.h"
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
engine C.Encoder
|
||||
r video.Reader
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
cntr int
|
||||
}
|
||||
|
||||
func statusToErr(status *C.Status) error {
|
||||
return fmt.Errorf("(status = %d) %s", int(status.code), C.GoString(status.msg))
|
||||
}
|
||||
|
||||
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
||||
if params.KeyFrameInterval == 0 {
|
||||
params.KeyFrameInterval = 60
|
||||
}
|
||||
|
||||
if params.BitRate == 0 {
|
||||
params.BitRate = 300000
|
||||
}
|
||||
|
||||
e := encoder{
|
||||
r: video.ToI420(r),
|
||||
}
|
||||
status := C.enc_new(C.Params{
|
||||
width: C.int(p.Width),
|
||||
height: C.int(p.Height),
|
||||
bitrate: C.uint(params.BitRate),
|
||||
key_frame_interval: C.uint(params.KeyFrameInterval),
|
||||
}, &e.engine)
|
||||
if status.code != 0 {
|
||||
return nil, statusToErr(&status)
|
||||
}
|
||||
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (e *encoder) Read() ([]byte, func(), error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
imgReal := img.(*image.YCbCr)
|
||||
var y, cb, cr C.Slice
|
||||
y.data = (*C.uchar)(&imgReal.Y[0])
|
||||
y.len = C.int(len(imgReal.Y))
|
||||
cb.data = (*C.uchar)(&imgReal.Cb[0])
|
||||
cb.len = C.int(len(imgReal.Cb))
|
||||
cr.data = (*C.uchar)(&imgReal.Cr[0])
|
||||
cr.len = C.int(len(imgReal.Cr))
|
||||
|
||||
var encodedBuffer *C.MMAL_BUFFER_HEADER_T
|
||||
status := C.enc_encode(&e.engine, y, cb, cr, &encodedBuffer)
|
||||
if status.code != 0 {
|
||||
return nil, func() {}, statusToErr(&status)
|
||||
}
|
||||
|
||||
// GoBytes copies the C array to Go slice. After this, it's safe to release the C array
|
||||
encoded := C.GoBytes(unsafe.Pointer(encodedBuffer.data), C.int(encodedBuffer.length))
|
||||
// Release the buffer so that mmal can reuse this memory
|
||||
C.mmal_buffer_header_release(encodedBuffer)
|
||||
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoder) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.closed = true
|
||||
C.enc_close(&e.engine)
|
||||
return nil
|
||||
}
|
84
pkg/codec/mmal/mmal_test.go
Normal file
84
pkg/codec/mmal/mmal_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package mmal
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderReadAfterCloseTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement bit rate control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement key frame control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
31
pkg/codec/mmal/params.go
Normal file
31
pkg/codec/mmal/params.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package mmal
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
// Params stores libmmal specific encoding parameters.
|
||||
type Params struct {
|
||||
codec.BaseParams
|
||||
}
|
||||
|
||||
// NewParams returns default mmal codec specific parameters.
|
||||
func NewParams() (Params, error) {
|
||||
return Params{
|
||||
BaseParams: codec.BaseParams{
|
||||
KeyFrameInterval: 60,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RTPCodec represents the codec metadata
|
||||
func (p *Params) RTPCodec() *codec.RTPCodec {
|
||||
return codec.NewRTPH264Codec(90000)
|
||||
}
|
||||
|
||||
// BuildVideoEncoder builds mmal encoder with given params
|
||||
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
|
||||
return newEncoder(r, property, *p)
|
||||
}
|
1
pkg/codec/openh264/.gitignore
vendored
Normal file
1
pkg/codec/openh264/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src
|
23
pkg/codec/openh264/LICENSE
Normal file
23
pkg/codec/openh264/LICENSE
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2013, Cisco Systems
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
47
pkg/codec/openh264/Makefile
Normal file
47
pkg/codec/openh264/Makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
git_url := https://github.com/cisco/openh264.git
|
||||
version := v2.1.1
|
||||
src_root_dir := src
|
||||
lib_dir := lib
|
||||
include_dir := include/openh264
|
||||
lib_prefix := libopenh264
|
||||
src_dir := $(src_root_dir)/$(MEDIADEVICES_TARGET_PLATFORM)
|
||||
output_path := $(lib_dir)/$(lib_prefix)-$(MEDIADEVICES_TARGET_PLATFORM).a
|
||||
|
||||
# OS and Arch mapping to OpenH264 parameters
|
||||
ifeq (windows,$(MEDIADEVICES_TARGET_OS))
|
||||
os := mingw_nt
|
||||
else
|
||||
os := $(MEDIADEVICES_TARGET_OS)
|
||||
endif
|
||||
|
||||
ifneq (,$(findstring $(MEDIADEVICES_TARGET_ARCH),armv6 armv7 armv8))
|
||||
arch := arm
|
||||
else ifeq (x64,$(MEDIADEVICES_TARGET_ARCH))
|
||||
arch := x86_64
|
||||
else
|
||||
arch := $(MEDIADEVICES_TARGET_ARCH)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: guard-MEDIADEVICES_TARGET_PLATFORM guard-MEDIADEVICES_TARGET_PLATFORM \
|
||||
guard-MEDIADEVICES_TARGET_OS guard-MEDIADEVICES_TARGET_ARCH \
|
||||
$(output_path) headers
|
||||
|
||||
headers: | $(src_dir) $(include_dir)
|
||||
@cp $(src_dir)/codec/api/svc/*.h $(include_dir)
|
||||
|
||||
$(output_path): $(src_dir)/$(lib_prefix).a | $(lib_dir)
|
||||
@cp $< $@
|
||||
|
||||
$(src_dir)/$(lib_prefix).a: | $(src_dir)
|
||||
$(MEDIADEVICES_TOOLCHAIN_BIN) make --directory=$(src_dir) $(lib_prefix).a \
|
||||
OS=$(os) ARCH=$(arch)
|
||||
|
||||
$(src_dir): | $(src_root_dir)
|
||||
git clone --depth=1 --branch=$(version) $(git_url) $@
|
||||
|
||||
$(src_root_dir) $(lib_dir) $(include_dir):
|
||||
@mkdir -p $@
|
||||
|
||||
guard-%:
|
||||
@if [ -z ${$*} ]; then echo "$* is a required environment variable"; exit 1; fi
|
@@ -21,29 +21,26 @@ Encoder *enc_new(const EncoderOptions opts, int *eresult) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// TODO: Remove hardcoded values
|
||||
params.iUsageType = CAMERA_VIDEO_REAL_TIME;
|
||||
params.iUsageType = opts.usage_type;
|
||||
params.iPicWidth = opts.width;
|
||||
params.iPicHeight = opts.height;
|
||||
params.iTargetBitrate = opts.target_bitrate;
|
||||
params.iMaxBitrate = opts.target_bitrate;
|
||||
params.iRCMode = RC_BITRATE_MODE;
|
||||
params.iRCMode = opts.rc_mode;
|
||||
params.fMaxFrameRate = opts.max_fps;
|
||||
params.bEnableFrameSkip = true;
|
||||
params.uiMaxNalSize = 0;
|
||||
params.uiIntraPeriod = 30;
|
||||
// set to 0, so that it'll automatically use multi threads when needed
|
||||
params.iMultipleThreadIdc = 0;
|
||||
params.bEnableFrameSkip = opts.enable_frame_skip;
|
||||
params.uiMaxNalSize = opts.max_nal_size;
|
||||
params.uiIntraPeriod = opts.intra_period;
|
||||
params.iMultipleThreadIdc = opts.multiple_thread_idc;
|
||||
// The base spatial layer 0 is the only one we use.
|
||||
params.sSpatialLayers[0].iVideoWidth = params.iPicWidth;
|
||||
params.sSpatialLayers[0].iVideoHeight = params.iPicHeight;
|
||||
params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate;
|
||||
params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate;
|
||||
params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate;
|
||||
// Single NAL unit mode
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = 1;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SIZELIMITED_SLICE;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = 12800;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = opts.slice_num;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = opts.slice_mode;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = opts.slice_size_constraint;
|
||||
|
||||
rv = engine->InitializeExt(¶ms);
|
||||
if (rv != 0) {
|
||||
@@ -72,6 +69,16 @@ void enc_free(Encoder *e, int *eresult) {
|
||||
free(e);
|
||||
}
|
||||
|
||||
void enc_set_bitrate(Encoder *e, int bitrate) {
|
||||
SEncParamExt encParamExt;
|
||||
e->engine->GetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
|
||||
encParamExt.iTargetBitrate=bitrate;
|
||||
encParamExt.iMaxBitrate=bitrate;
|
||||
encParamExt.sSpatialLayers[0].iSpatialBitrate = bitrate;
|
||||
encParamExt.sSpatialLayers[0].iMaxSpatialBitrate = bitrate;
|
||||
e->engine->SetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
|
||||
}
|
||||
|
||||
// There's a good reference from ffmpeg in using the encode_frame
|
||||
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
|
||||
Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
||||
@@ -80,12 +87,16 @@ Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
||||
SFrameBSInfo info = {0};
|
||||
Slice payload = {0};
|
||||
|
||||
if(e->force_key_frame == 1) {
|
||||
e->engine->ForceIntraFrame(true);
|
||||
e->force_key_frame = 0;
|
||||
}
|
||||
|
||||
pic.iPicWidth = f.width;
|
||||
pic.iPicHeight = f.height;
|
||||
pic.iColorFormat = videoFormatI420;
|
||||
// We always received I420 format
|
||||
pic.iStride[0] = pic.iPicWidth;
|
||||
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth / 2;
|
||||
pic.iStride[0] = f.ystride;
|
||||
pic.iStride[1] = pic.iStride[2] = f.cstride;
|
||||
pic.pData[0] = (unsigned char *)f.y;
|
||||
pic.pData[1] = (unsigned char *)f.u;
|
||||
pic.pData[2] = (unsigned char *)f.v;
|
||||
|
@@ -12,6 +12,8 @@ typedef struct Slice {
|
||||
|
||||
typedef struct Frame {
|
||||
void *y, *u, *v;
|
||||
int ystride;
|
||||
int cstride;
|
||||
int height;
|
||||
int width;
|
||||
} Frame;
|
||||
@@ -20,6 +22,15 @@ typedef struct EncoderOptions {
|
||||
int width, height;
|
||||
int target_bitrate;
|
||||
float max_fps;
|
||||
EUsageType usage_type;
|
||||
RC_MODES rc_mode;
|
||||
bool enable_frame_skip;
|
||||
unsigned int max_nal_size;
|
||||
unsigned int intra_period;
|
||||
int multiple_thread_idc;
|
||||
unsigned int slice_num;
|
||||
SliceModeEnum slice_mode;
|
||||
unsigned int slice_size_constraint;
|
||||
} EncoderOptions;
|
||||
|
||||
typedef struct Encoder {
|
||||
@@ -27,11 +38,13 @@ typedef struct Encoder {
|
||||
ISVCEncoder *engine;
|
||||
unsigned char *buff;
|
||||
int buff_size;
|
||||
int force_key_frame;
|
||||
} Encoder;
|
||||
|
||||
Encoder *enc_new(const EncoderOptions params, int *eresult);
|
||||
void enc_free(Encoder *e, int *eresult);
|
||||
Slice enc_encode(Encoder *e, Frame f, int *eresult);
|
||||
void enc_set_bitrate(Encoder *e, int bitrate);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@@ -167,8 +167,8 @@ typedef enum {
|
||||
DECODER_OPTION_LEVEL, ///< get current AU level info,only is used in GetOption
|
||||
DECODER_OPTION_STATISTICS_LOG_INTERVAL,///< set log output interval
|
||||
DECODER_OPTION_IS_REF_PIC, ///< feedback current frame is ref pic or not
|
||||
DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER ///< number of frames remaining in decoder buffer when pictures are required to re-ordered into display-order.
|
||||
|
||||
DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, ///< number of frames remaining in decoder buffer when pictures are required to re-ordered into display-order.
|
||||
DECODER_OPTION_NUM_OF_THREADS, ///< number of decoding threads. The maximum thread count is equal or less than lesser of (cpu core counts and 16).
|
||||
} DECODER_OPTION;
|
||||
|
||||
/**
|
@@ -201,6 +201,7 @@ typedef struct TagBufferInfo {
|
||||
union {
|
||||
SSysMEMBuffer sSystemBuffer; ///< memory info for one picture
|
||||
} UsrData; ///< output buffer info
|
||||
unsigned char* pDst[3]; //point to picture YUV data
|
||||
} SBufferInfo;
|
||||
|
||||
|
15
pkg/codec/openh264/include/openh264/codec_ver.h
Normal file
15
pkg/codec/openh264/include/openh264/codec_ver.h
Normal file
@@ -0,0 +1,15 @@
|
||||
//The current file is auto-generated by script: generate_codec_ver.sh
|
||||
#ifndef CODEC_VER_H
|
||||
#define CODEC_VER_H
|
||||
|
||||
#include "codec_app_def.h"
|
||||
|
||||
static const OpenH264Version g_stCodecVersion = {2, 1, 1, 2005};
|
||||
static const char* const g_strCodecVer = "OpenH264 version:2.1.1.2005";
|
||||
|
||||
#define OPENH264_MAJOR (2)
|
||||
#define OPENH264_MINOR (1)
|
||||
#define OPENH264_REVISION (1)
|
||||
#define OPENH264_RESERVED (2005)
|
||||
|
||||
#endif // CODEC_VER_H
|
BIN
pkg/codec/openh264/lib/libopenh264-darwin-arm64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-darwin-arm64.a
Normal file
Binary file not shown.
BIN
pkg/codec/openh264/lib/libopenh264-darwin-x64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-darwin-x64.a
Normal file
Binary file not shown.
BIN
pkg/codec/openh264/lib/libopenh264-linux-arm64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-linux-arm64.a
Normal file
Binary file not shown.
BIN
pkg/codec/openh264/lib/libopenh264-linux-armv7.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-linux-armv7.a
Normal file
Binary file not shown.
BIN
pkg/codec/openh264/lib/libopenh264-linux-x64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-linux-x64.a
Normal file
Binary file not shown.
BIN
pkg/codec/openh264/lib/libopenh264-windows-x64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-windows-x64.a
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user