mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-27 21:02:17 +08:00
Compare commits
210 Commits
fork-merge
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
40
.github/workflows/ci.yaml
vendored
40
.github/workflows/ci.yaml
vendored
@@ -13,13 +13,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: [ '1.16', '1.15' ]
|
go:
|
||||||
|
- '1.21' # oldest version this package supports
|
||||||
|
- '1.22' # oldstable Go version
|
||||||
|
- '1.23' # stable Go version
|
||||||
name: Linux Go ${{ matrix.go }}
|
name: Linux Go ${{ matrix.go }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -29,27 +32,33 @@ jobs:
|
|||||||
libopus-dev \
|
libopus-dev \
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libvpx-dev \
|
libvpx-dev \
|
||||||
libx264-dev
|
libx11-dev \
|
||||||
|
libx264-dev \
|
||||||
|
libxext-dev
|
||||||
- name: Run Test Suite
|
- name: Run Test Suite
|
||||||
run: make test
|
run: make test
|
||||||
- uses: codecov/codecov-action@v2
|
- uses: codecov/codecov-action@v5
|
||||||
if: matrix.go == '1.16'
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
build-darwin:
|
build-darwin:
|
||||||
runs-on: macos-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: [ '1.16', '1.15' ]
|
go:
|
||||||
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
|
runs-on: macos-latest
|
||||||
name: Darwin Go ${{ matrix.go }}
|
name: Darwin Go ${{ matrix.go }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
which brew
|
||||||
brew install \
|
brew install \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
opus \
|
opus \
|
||||||
@@ -57,17 +66,20 @@ jobs:
|
|||||||
x264
|
x264
|
||||||
- name: Run Test Suite
|
- name: Run Test Suite
|
||||||
run: make test
|
run: make test
|
||||||
|
- uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
check-licenses:
|
check-licenses:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Check Licenses
|
name: Check Licenses
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.16'
|
go-version: stable
|
||||||
- name: Installing go-licenses
|
- name: Installing go-licenses
|
||||||
run: go get github.com/google/go-licenses
|
run: go install github.com/google/go-licenses@latest
|
||||||
- name: Checking licenses
|
- name: Checking licenses
|
||||||
run: go-licenses check ./...
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: fix
|
- name: fix
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@
|
|||||||
|
|
||||||
scripts/cross
|
scripts/cross
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
|
||||||
|
.idea
|
||||||
|
10
.idea/mediadevices.iml
generated
10
.idea/mediadevices.iml
generated
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="Go" enabled="true" />
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/mediadevices.iml" filepath="$PROJECT_DIR$/.idea/mediadevices.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
63
.idea/workspace.xml
generated
63
.idea/workspace.xml
generated
@@ -1,63 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="8804a8cb-7b92-421b-8786-b7715667b867" name="Changes" comment="">
|
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/mediadevices.iml" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="GOROOT" url="file:///usr/local/opt/go/libexec" />
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
</component>
|
|
||||||
<component name="GoLibraries">
|
|
||||||
<option name="indexEntireGoPath" value="false" />
|
|
||||||
</component>
|
|
||||||
<component name="KubernetesApiPersistence">
|
|
||||||
<option name="context" value="crane-nts-0" />
|
|
||||||
<option name="namespace" value="default" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectId" id="25msmAX4e3Virjg5KBrchpLrlbl" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="autoscrollFromSource" value="true" />
|
|
||||||
<option name="autoscrollToSource" value="true" />
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent">
|
|
||||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
|
||||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
|
||||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
|
||||||
<property name="go.formatter.settings.were.checked" value="true" />
|
|
||||||
<property name="go.import.settings.migrated" value="true" />
|
|
||||||
<property name="go.sdk.automatically.set" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="8804a8cb-7b92-421b-8786-b7715667b867" name="Changes" comment="" />
|
|
||||||
<created>1646143753802</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1646143753802</updated>
|
|
||||||
<workItem from="1646143757180" duration="56000" />
|
|
||||||
</task>
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
<component name="VgoProject">
|
|
||||||
<integration-enabled>true</integration-enabled>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
13
Makefile
13
Makefile
@@ -16,7 +16,8 @@ supported_platforms := \
|
|||||||
linux-arm64 \
|
linux-arm64 \
|
||||||
linux-x64 \
|
linux-x64 \
|
||||||
windows-x64 \
|
windows-x64 \
|
||||||
darwin-x64
|
darwin-x64 \
|
||||||
|
darwin-arm64
|
||||||
cmd_build := build
|
cmd_build := build
|
||||||
cmd_test := test
|
cmd_test := test
|
||||||
examples_dir := examples
|
examples_dir := examples
|
||||||
@@ -24,7 +25,7 @@ codec_dir := pkg/codec
|
|||||||
codec_list := $(shell ls $(codec_dir)/*/Makefile)
|
codec_list := $(shell ls $(codec_dir)/*/Makefile)
|
||||||
codec_list := $(codec_list:$(codec_dir)/%/Makefile=%)
|
codec_list := $(codec_list:$(codec_dir)/%/Makefile=%)
|
||||||
targets := $(foreach codec, $(codec_list), $(addprefix $(cmd_build)-$(codec)-, $(supported_platforms)))
|
targets := $(foreach codec, $(codec_list), $(addprefix $(cmd_build)-$(codec)-, $(supported_platforms)))
|
||||||
pkgs_without_mmal := $(shell go list ./... | grep -v mmal)
|
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)
|
pkgs_without_cgo := $(shell go list ./... | grep -v pkg/codec | grep -v pkg/driver | grep -v pkg/avfoundation)
|
||||||
|
|
||||||
define BUILD_TEMPLATE
|
define BUILD_TEMPLATE
|
||||||
@@ -72,11 +73,11 @@ $(foreach codec, $(codec_list), \
|
|||||||
# Description:
|
# Description:
|
||||||
# Run a series of tests
|
# Run a series of tests
|
||||||
$(cmd_test):
|
$(cmd_test):
|
||||||
go vet $(pkgs_without_mmal)
|
go vet $(pkgs_without_ext_device)
|
||||||
go build $(pkgs_without_mmal)
|
go build $(pkgs_without_ext_device)
|
||||||
# go build without CGO
|
# go build without CGO
|
||||||
CGO_ENABLED=0 go build $(pkgs_without_cgo)
|
CGO_ENABLED=0 go build $(pkgs_without_cgo)
|
||||||
# go build with CGO
|
# go build with CGO
|
||||||
CGO_ENABLED=1 go build $(pkgs_without_mmal)
|
CGO_ENABLED=1 go build $(pkgs_without_ext_device)
|
||||||
$(MAKE) --directory=$(examples_dir)
|
$(MAKE) --directory=$(examples_dir)
|
||||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(pkgs_without_mmal)
|
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(pkgs_without_ext_device)
|
||||||
|
81
README.md
81
README.md
@@ -5,9 +5,9 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4>
|
<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">
|
<p align="center">
|
||||||
<a href="https://pion.ly/slack"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
|
<a href="https://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>
|
||||||
<a href="https://github.com/pion/mediadevices/actions"><img src="https://github.com/pion/mediadevices/workflows/CI/badge.svg?branch=master" alt="Build status"></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://godoc.org/github.com/pion/mediadevices?status.svg" alt="GoDoc"></a>
|
<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="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>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -15,11 +15,13 @@
|
|||||||
|
|
||||||
`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!
|
`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
|
### Install
|
||||||
|
|
||||||
`go get -u github.com/pion/mediadevices`
|
```bash
|
||||||
|
go get -u github.com/pion/mediadevices
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
### Usage
|
||||||
|
|
||||||
The following snippet shows how to capture a camera stream and store a frame as a jpeg image:
|
The following snippet shows how to capture a camera stream and store a frame as a jpeg image:
|
||||||
|
|
||||||
@@ -67,25 +69,21 @@ func main() {
|
|||||||
output, _ := os.Create("frame.jpg")
|
output, _ := os.Create("frame.jpg")
|
||||||
jpeg.Encode(output, frame, nil)
|
jpeg.Encode(output, frame, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## More Examples
|
### More Examples
|
||||||
|
|
||||||
* [Webrtc](/examples/webrtc) - Use Webrtc to create a realtime peer-to-peer video call
|
* [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
|
* [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
|
* [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
|
* [HTTP Broadcast](/examples/http) - Broadcast camera stream through HTTP with MJPEG
|
||||||
* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera
|
* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera
|
||||||
|
|
||||||
## Available Media Inputs
|
### Available Media Inputs
|
||||||
|
| Input | Linux | Mac | Windows |
|
||||||
| Input | Linux | Mac | Windows |
|
|
||||||
| :--------: | :---: | :-: | :-----: |
|
| :--------: | :---: | :-: | :-----: |
|
||||||
| Camera | ✔️ | ✔️ | ✔️ |
|
| Camera | ✔️ | ✔️ | ✔️ |
|
||||||
| Microphone | ✔️ | ✔️ | ✔️ |
|
| Microphone | ✔️ | ✔️ | ✔️ |
|
||||||
| Screen | ✔️ | ✔️ | ✔️ |
|
| Screen | ✔️ | ✔️ | ✔️ |
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
@@ -96,8 +94,7 @@ import (
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Available Codecs
|
### 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`:
|
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`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -130,9 +127,9 @@ Since `mediadevices` doesn't implement the video/audio codecs, it needs to call
|
|||||||
|
|
||||||
Note: we do not provide recommendations on choosing one codec or another as it is very complex and can be subjective.
|
Note: we do not provide recommendations on choosing one codec or another as it is very complex and can be subjective.
|
||||||
|
|
||||||
### Video Codecs
|
#### Video Codecs
|
||||||
|
|
||||||
#### x264
|
##### x264
|
||||||
A free software library and application for encoding video streams into the H.264/MPEG-4 AVC compression format.
|
A free software library and application for encoding video streams into the H.264/MPEG-4 AVC compression format.
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/x264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/x264)
|
* Package: [github.com/pion/mediadevices/pkg/codec/x264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/x264)
|
||||||
@@ -140,19 +137,19 @@ A free software library and application for encoding video streams into the H.26
|
|||||||
* Mac: `brew install x264`
|
* Mac: `brew install x264`
|
||||||
* Ubuntu: `apt install libx264-dev`
|
* Ubuntu: `apt install libx264-dev`
|
||||||
|
|
||||||
#### mmal
|
##### mmal
|
||||||
A framework to enable H264 hardware encoding for Raspberry Pi or boards that use VideoCore GPUs.
|
A framework to enable H264 hardware encoding for Raspberry Pi or boards that use VideoCore GPUs.
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/mmal](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/mmal)
|
* 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
|
* Installation: no installation needed, mmal should come built in Raspberry Pi devices
|
||||||
|
|
||||||
#### openh264
|
##### openh264
|
||||||
A codec library which supports H.264 encoding and decoding. It is suitable for use in real time applications.
|
A codec library which supports H.264 encoding and decoding. It is suitable for use in real time applications.
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/openh264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/openh264)
|
* 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
|
* Installation: no installation needed, included as a static binary
|
||||||
|
|
||||||
#### vpx
|
##### vpx
|
||||||
A free software video codec library from Google and the Alliance for Open Media that implements VP8/VP9 video coding formats.
|
A free software video codec library from Google and the Alliance for Open Media that implements VP8/VP9 video coding formats.
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/vpx](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vpx)
|
* Package: [github.com/pion/mediadevices/pkg/codec/vpx](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vpx)
|
||||||
@@ -160,7 +157,7 @@ A free software video codec library from Google and the Alliance for Open Media
|
|||||||
* Mac: `brew install libvpx`
|
* Mac: `brew install libvpx`
|
||||||
* Ubuntu: `apt install libvpx-dev`
|
* Ubuntu: `apt install libvpx-dev`
|
||||||
|
|
||||||
#### vaapi
|
##### 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).
|
An open source API that allows applications such as VLC media player or GStreamer to use hardware video acceleration capabilities (currently support VP8/VP9).
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/vaapi](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vaapi)
|
* Package: [github.com/pion/mediadevices/pkg/codec/vaapi](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vaapi)
|
||||||
@@ -168,9 +165,9 @@ An open source API that allows applications such as VLC media player or GStreame
|
|||||||
* Ubuntu: `apt install libva-dev`
|
* Ubuntu: `apt install libva-dev`
|
||||||
|
|
||||||
|
|
||||||
### Audio Codecs
|
#### Audio Codecs
|
||||||
|
|
||||||
#### opus
|
##### opus
|
||||||
A totally open, royalty-free, highly versatile audio codec.
|
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)
|
* Package: [github.com/pion/mediadevices/pkg/codec/opus](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/opus)
|
||||||
@@ -178,16 +175,14 @@ A totally open, royalty-free, highly versatile audio codec.
|
|||||||
* Mac: `brew install opus`
|
* Mac: `brew install opus`
|
||||||
* Ubuntu: `apt install libopus-dev`
|
* Ubuntu: `apt install libopus-dev`
|
||||||
|
|
||||||
## Benchmark
|
### 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**.
|
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.
|
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
|
### FAQ
|
||||||
|
|
||||||
### Failed to find the best driver that fits the constraints
|
|
||||||
|
|
||||||
|
#### 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:
|
`mediadevices` provides an automated driver discovery through `GetUserMedia` and `GetDisplayMedia`. The driver discover algorithm works something like:
|
||||||
|
|
||||||
1. Open all registered drivers
|
1. Open all registered drivers
|
||||||
@@ -198,13 +193,14 @@ So, when `mediadevices` returns `failed to find the best driver that fits the co
|
|||||||
* Driver was not imported as a side effect in your program, e.g. `import _ github.com/pion/mediadevices/pkg/driver/camera`
|
* 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 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`
|
* 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
|
#### 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.
|
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:
|
If you see the following error message at compile time:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
# pkg-config --cflags -- vpx
|
# pkg-config --cflags -- vpx
|
||||||
Package vpx was not found in the pkg-config search path.
|
Package vpx was not found in the pkg-config search path.
|
||||||
Perhaps you should add the directory containing `vpx.pc'
|
Perhaps you should add the directory containing `vpx.pc'
|
||||||
@@ -218,20 +214,19 @@ 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).
|
* 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`.
|
* 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
|
### Community
|
||||||
Pion has an active community on the [Slack](https://pion.ly/slack).
|
Pion has an active community on [Discord](https://discord.gg/PngbdqpFbt).
|
||||||
|
|
||||||
Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news.
|
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!
|
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)
|
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
|
### Contributing
|
||||||
Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
|
Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible
|
||||||
|
|
||||||
* [Lukas Herman](https://github.com/lherman-cs) - _Original Author_
|
### License
|
||||||
* [Atsushi Watanabe](https://github.com/at-wat) - _VP8, Screencast, etc._
|
|
||||||
|
|
||||||
## License
|
|
||||||
MIT License - see [LICENSE](LICENSE) for full text
|
MIT License - see [LICENSE](LICENSE) for full text
|
||||||
|
2
codec.go
2
codec.go
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CodecSelector is a container of video and audio encoder builders, which later will be used
|
// CodecSelector is a container of video and audio encoder builders, which later will be used
|
||||||
|
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})
|
@@ -1,11 +1,37 @@
|
|||||||
module github.com/pion/mediadevices/examples
|
module github.com/pion/mediadevices/examples
|
||||||
|
|
||||||
go 1.14
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/esimov/pigo v1.4.3
|
github.com/esimov/pigo v1.4.6
|
||||||
github.com/pion/mediadevices v0.0.0
|
github.com/pion/mediadevices v0.0.0
|
||||||
github.com/pion/webrtc/v3 v3.0.20
|
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 => ../
|
replace github.com/pion/mediadevices v0.0.0 => ../
|
||||||
|
213
examples/go.sum
213
examples/go.sum
@@ -1,166 +1,67 @@
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 h1:1aIqYfg9s9RETAJHGfVKZW4ok0b22p4QTwk8MsdRtPs=
|
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/disintegration/imaging v1.6.1/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/esimov/pigo v1.4.3 h1:xl098Z9CHmouywvyRZepuKx8aSWHBs/0lZtp7Yt5g28=
|
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
|
||||||
github.com/esimov/pigo v1.4.3/go.mod h1:aOTYpOWsqniACzXKdSOGkqI6CnWQpP8tFjgtUOARoEs=
|
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
|
||||||
github.com/fogleman/gg v1.0.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||||
github.com/gen2brain/malgo v0.10.29 h1:bTYiUTUKJsEomNby+W0hgyLrOttUXIk4lTEnKA54iqM=
|
|
||||||
github.com/gen2brain/malgo v0.10.29/go.mod h1:zHSUNZAXfCeNsZou0RtQ6Zk7gDYLIcKOrUWtAdksnEs=
|
|
||||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||||
github.com/kbinani/screenshot v0.0.0-20210326165202-b96eb3309bb0/go.mod h1:ZceVWGtzUZmxyN+/1I+oG31oOm1dOA2QUNbua9TLVdE=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
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/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
|
|
||||||
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
|
|
||||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
|
||||||
github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8=
|
|
||||||
github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho=
|
|
||||||
github.com/pion/ice/v2 v2.1.7 h1:FjgDfUNrVYTxQabJrkBX6ld12tvYbgzHenqPh3PJF6E=
|
|
||||||
github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0=
|
|
||||||
github.com/pion/interceptor v0.0.12 h1:eC1iVneBIAQJEfaNAfDqAncJWhMDAnaXPRCJsltdokE=
|
|
||||||
github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4=
|
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
|
||||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
|
||||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U=
|
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||||
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/rtp v1.6.5 h1:o2cZf8OascA5HF/b0PAbTxRKvOWxTQxWYt7SlToxFGI=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
|
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||||
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/srtp/v2 v2.0.2 h1:664iGzVmaY7KYS5M0gleY0DscRo9ReDfTxQrq4UgGoU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||||
github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
|
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
|
||||||
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
|
|
||||||
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
|
|
||||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
|
||||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
|
||||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
|
|
||||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210420210106-798c2154c571 h1:Q6Bg8xzKzpFPU4Oi1sBnBTHBwlMsLeEXpu4hYBY8rAg=
|
|
||||||
golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ=
|
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
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=
|
|
||||||
|
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
examples/vnc/.gitignore
vendored
Normal file
1
examples/vnc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
vnc
|
@@ -2,12 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
"github.com/pion/mediadevices/pkg/driver/vncdriver"
|
"github.com/pion/mediadevices/pkg/driver/vncdriver"
|
||||||
|
|
||||||
"github.com/pion/mediadevices"
|
"github.com/pion/mediadevices"
|
||||||
"github.com/pion/mediadevices/examples/internal/signal"
|
"github.com/pion/mediadevices/examples/internal/signal"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v4"
|
||||||
|
|
||||||
// If you don't like x264, you can also use vpx by importing as below
|
// 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
|
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||||
@@ -16,7 +17,6 @@ import (
|
|||||||
// or if you use a raspberry pi like, you can use mmal for using its hardware 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/mmal"
|
||||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video 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,
|
// 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.
|
// you can always swap your adapters with our dummy adapters below.
|
||||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||||
@@ -84,7 +84,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||||
webrtc.RtpTransceiverInit{
|
webrtc.RTPTransceiverInit{
|
||||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/pion/mediadevices/examples/internal/signal"
|
"github.com/pion/mediadevices/examples/internal/signal"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v4"
|
||||||
|
|
||||||
// If you don't like x264, you can also use vpx by importing as below
|
// 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
|
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||||
@@ -90,7 +90,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||||
webrtc.RtpTransceiverInit{
|
webrtc.RTPTransceiverInit{
|
||||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
51
go.mod
51
go.mod
@@ -1,18 +1,43 @@
|
|||||||
module github.com/pion/mediadevices
|
module github.com/pion/mediadevices
|
||||||
|
|
||||||
go 1.13
|
go 1.21
|
||||||
|
|
||||||
replace github.com/pion/webrtc/v3 => github.com/EmrysMyrddin/webrtc/v3 v3.1.25-0.20220301142221-d92d68ff068f
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
|
github.com/blackjack/webcam v0.6.1
|
||||||
github.com/gen2brain/malgo v0.10.35
|
github.com/gen2brain/malgo v0.11.23
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329
|
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/interceptor v0.1.40
|
||||||
github.com/pion/rtcp v1.2.9
|
github.com/pion/logging v0.2.4
|
||||||
github.com/pion/rtp v1.7.4
|
github.com/pion/rtcp v1.2.15
|
||||||
github.com/pion/webrtc/v3 v3.1.10
|
github.com/pion/rtp v1.8.19
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
github.com/pion/webrtc/v4 v4.1.2
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
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
|
||||||
)
|
)
|
||||||
|
214
go.sum
214
go.sum
@@ -1,170 +1,76 @@
|
|||||||
github.com/EmrysMyrddin/webrtc/v3 v3.1.25-0.20220301142221-d92d68ff068f h1:o7MCxR85nZxyOgjkmjtnXHsmPmchX3AEbWb/Bgpy3aI=
|
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||||
github.com/EmrysMyrddin/webrtc/v3 v3.1.25-0.20220301142221-d92d68ff068f/go.mod h1:mO/yv7fBN3Lp7YNlnYcTj1jtpvNvssJG+7eh6itZ4xM=
|
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||||
github.com/gen2brain/malgo v0.10.35 h1:D6aNo/Q0SnzQLHomTydTXxj4AJFdGJcVoE7I8JxPoUo=
|
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||||
github.com/gen2brain/malgo v0.10.35/go.mod h1:zHSUNZAXfCeNsZou0RtQ6Zk7gDYLIcKOrUWtAdksnEs=
|
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
|
|
||||||
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
|
|
||||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329 h1:qq2nCpSrXrmvDGRxW0ruW9BVEV1CN2a9YDOExdt+U0o=
|
|
||||||
github.com/kbinani/screenshot v0.0.0-20210720154843-7d3a670d8329/go.mod h1:2VPVQDR4wO7KXHwP+DAypEy67rXf+okUx2zjgpCxZw4=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
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 h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||||
github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
|
|
||||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
|
||||||
github.com/pion/ice/v2 v2.2.1 h1:R3MeuJZpU1ty3diPqpD5OxaxcZ15eprAc+EtUiSoFxg=
|
|
||||||
github.com/pion/ice/v2 v2.2.1/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ=
|
|
||||||
github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
|
|
||||||
github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
|
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
|
||||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
|
||||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c=
|
||||||
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
|
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||||
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||||
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
|
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
|
||||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
|
||||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
|
||||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
|
||||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
|
||||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
|
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
|
||||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
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=
|
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
_ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
_ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
||||||
_ "github.com/pion/mediadevices/pkg/driver/videotest"
|
_ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||||
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,37 +98,145 @@ func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) {
|
|||||||
t.Fatal("expect to get at least 1 property")
|
t.Fatal("expect to get at least 1 property")
|
||||||
}
|
}
|
||||||
expectedProp := driver.Properties()[0]
|
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{
|
// By reducing the value from the driver by a tiny amount, this property should be chosen.
|
||||||
MediaConstraints: prop.MediaConstraints{
|
// At the same time, we'll be able to find out if the return constraints will be properly set
|
||||||
VideoConstraints: prop.VideoConstraints{
|
// to the best constraints.
|
||||||
// By reducing the width from the driver by a tiny amount, this property should be chosen.
|
cases := map[string]struct {
|
||||||
// At the same time, we'll be able to find out if the return constraints will be properly set
|
width, height int
|
||||||
// to the best constraints.
|
frameFormat frame.Format
|
||||||
Width: prop.Int(expectedProp.Width - 1),
|
frameRate float32
|
||||||
Height: prop.Int(expectedProp.Width),
|
}{
|
||||||
FrameFormat: prop.FrameFormat(expectedProp.FrameFormat),
|
"DifferentWidth": {
|
||||||
FrameRate: prop.Float(30.0),
|
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 {
|
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 {
|
for name, c := range cases {
|
||||||
t.Fatal("best driver is not expected")
|
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
|
_, _, err := selectBestDriver(filterFn, wantConstraints)
|
||||||
if s.Width != expectedProp.Width ||
|
if err == nil {
|
||||||
s.Height != expectedProp.Height ||
|
t.Fatal("expect to not find a driver that fits the constraints")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ package mediadevices
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MediaStream is an interface that represents a collection of existing tracks.
|
// MediaStream is an interface that represents a collection of existing tracks.
|
||||||
|
@@ -2,9 +2,11 @@ package mediadevices
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockMediaStreamTrack struct {
|
type mockMediaStreamTrack struct {
|
||||||
@@ -61,6 +63,10 @@ func (track *mockMediaStreamTrack) NewEncodedIOReader(codecName string) (io.Read
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (track *mockMediaStreamTrack) EncoderController() codec.EncoderController {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestMediaStreamFilters(t *testing.T) {
|
func TestMediaStreamFilters(t *testing.T) {
|
||||||
audioTracks := []Track{
|
audioTracks := []Track{
|
||||||
&mockMediaStreamTrack{AudioInput},
|
&mockMediaStreamTrack{AudioInput},
|
||||||
@@ -88,13 +94,7 @@ func TestMediaStreamFilters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range actual {
|
for _, a := range actual {
|
||||||
found := false
|
found := slices.Contains(expected, a)
|
||||||
for _, e := range expected {
|
|
||||||
if e == a {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
||||||
|
@@ -27,6 +27,7 @@
|
|||||||
#define MAX_DEVICES 8
|
#define MAX_DEVICES 8
|
||||||
#define MAX_PROPERTIES 64
|
#define MAX_PROPERTIES 64
|
||||||
#define MAX_DEVICE_UID_CHARS 64
|
#define MAX_DEVICE_UID_CHARS 64
|
||||||
|
#define MAX_DEVICE_NAME_CHARS 64
|
||||||
|
|
||||||
typedef const char* STATUS;
|
typedef const char* STATUS;
|
||||||
static STATUS STATUS_OK = (STATUS) NULL;
|
static STATUS STATUS_OK = (STATUS) NULL;
|
||||||
@@ -46,7 +47,7 @@ typedef enum AVBindFrameFormat {
|
|||||||
AVBindFrameFormatI420,
|
AVBindFrameFormatI420,
|
||||||
AVBindFrameFormatNV21,
|
AVBindFrameFormatNV21,
|
||||||
AVBindFrameFormatNV12,
|
AVBindFrameFormatNV12,
|
||||||
AVBindFrameFormatYUY2,
|
AVBindFrameFormatYUYV,
|
||||||
AVBindFrameFormatUYVY,
|
AVBindFrameFormatUYVY,
|
||||||
} AVBindFrameFormat;
|
} AVBindFrameFormat;
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ typedef struct AVBindSession AVBindSession, *PAVBindSession;
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char uid[MAX_DEVICE_UID_CHARS + 1];
|
char uid[MAX_DEVICE_UID_CHARS + 1];
|
||||||
|
char name[MAX_DEVICE_NAME_CHARS + 1];
|
||||||
} AVBindDevice, *PAVBindDevice;
|
} AVBindDevice, *PAVBindDevice;
|
||||||
|
|
||||||
// AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static
|
// AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static
|
||||||
|
@@ -46,6 +46,8 @@
|
|||||||
} \
|
} \
|
||||||
} while(0)
|
} while(0)
|
||||||
|
|
||||||
|
static NSString *const UnrecognizedMacOSVersionException = @"UnrecognizedMacOSVersionException";
|
||||||
|
|
||||||
@interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
|
@interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||||
|
|
||||||
@property (readonly) AVBindDataCallback mCallback;
|
@property (readonly) AVBindDataCallback mCallback;
|
||||||
@@ -74,26 +76,61 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
|||||||
if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
|
if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
|
||||||
!CMSampleBufferIsValid(sampleBuffer) ||
|
!CMSampleBufferIsValid(sampleBuffer) ||
|
||||||
!CMSampleBufferDataIsReady(sampleBuffer)) {
|
!CMSampleBufferDataIsReady(sampleBuffer)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||||
if (imageBuffer == NULL) {
|
if (imageBuffer == NULL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
imageBuffer = CVBufferRetain(imageBuffer);
|
CVBufferRetain(imageBuffer);
|
||||||
CVReturn ret =
|
CVReturn ret =
|
||||||
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||||
if (ret != kCVReturnSuccess) {
|
if (ret != kCVReturnSuccess) {
|
||||||
return;
|
CVBufferRelease(imageBuffer);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void *buf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
// Handle NV12 special case
|
||||||
size_t dataSize = CVPixelBufferGetDataSize(imageBuffer);
|
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
|
||||||
_mCallback(_mPUserData, buf, (int)dataSize);
|
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;
|
||||||
|
|
||||||
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
|
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);
|
CVBufferRelease(imageBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +176,7 @@ STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
|
|||||||
case AVBindFrameFormatUYVY:
|
case AVBindFrameFormatUYVY:
|
||||||
*pFourCC = kCVPixelFormatType_422YpCbCr8;
|
*pFourCC = kCVPixelFormatType_422YpCbCr8;
|
||||||
break;
|
break;
|
||||||
case AVBindFrameFormatYUY2:
|
case AVBindFrameFormatYUYV:
|
||||||
*pFourCC = kCVPixelFormatType_422YpCbCr8_yuvs;
|
*pFourCC = kCVPixelFormatType_422YpCbCr8_yuvs;
|
||||||
break;
|
break;
|
||||||
// TODO: Add the rest of frame formats
|
// TODO: Add the rest of frame formats
|
||||||
@@ -163,7 +200,7 @@ STATUS frameFormatFromFourCC(FourCharCode fourCC, AVBindFrameFormat *pFormat) {
|
|||||||
*pFormat = AVBindFrameFormatUYVY;
|
*pFormat = AVBindFrameFormatUYVY;
|
||||||
break;
|
break;
|
||||||
case kCVPixelFormatType_422YpCbCr8_yuvs:
|
case kCVPixelFormatType_422YpCbCr8_yuvs:
|
||||||
*pFormat = AVBindFrameFormatYUY2;
|
*pFormat = AVBindFrameFormatYUYV;
|
||||||
break;
|
break;
|
||||||
// TODO: Add the rest of frame formats
|
// TODO: Add the rest of frame formats
|
||||||
default:
|
default:
|
||||||
@@ -182,15 +219,32 @@ STATUS AVBindDevices(AVBindMediaType mediaType, PAVBindDevice *ppDevices, int *p
|
|||||||
|
|
||||||
PAVBindDevice pDevice;
|
PAVBindDevice pDevice;
|
||||||
AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio;
|
AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio;
|
||||||
NSArray *refAllTypes = @[
|
|
||||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
NSArray *refAllTypes;
|
||||||
AVCaptureDeviceTypeBuiltInMicrophone,
|
#if defined(MAC_OS_VERSION_14_0)
|
||||||
AVCaptureDeviceTypeExternalUnknown
|
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
|
AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession
|
||||||
discoverySessionWithDeviceTypes: refAllTypes
|
discoverySessionWithDeviceTypes: refAllTypes
|
||||||
mediaType: _mediaType
|
mediaType: _mediaType
|
||||||
position: AVCaptureDevicePositionUnspecified];
|
position: AVCaptureDevicePositionUnspecified];
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (AVCaptureDevice *refDevice in refSession.devices) {
|
for (AVCaptureDevice *refDevice in refSession.devices) {
|
||||||
@@ -201,6 +255,8 @@ STATUS AVBindDevices(AVBindMediaType mediaType, PAVBindDevice *ppDevices, int *p
|
|||||||
pDevice = devices + i;
|
pDevice = devices + i;
|
||||||
strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS);
|
strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS);
|
||||||
pDevice->uid[MAX_DEVICE_UID_CHARS] = '\0';
|
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++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,8 +11,10 @@ package avfoundation
|
|||||||
// }
|
// }
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
@@ -32,6 +34,7 @@ type Device struct {
|
|||||||
// UID is a unique identifier for a device
|
// UID is a unique identifier for a device
|
||||||
UID string
|
UID string
|
||||||
cDevice C.AVBindDevice
|
cDevice C.AVBindDevice
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
||||||
@@ -42,8 +45,8 @@ func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
|||||||
return C.AVBindFrameFormatNV21, true
|
return C.AVBindFrameFormatNV21, true
|
||||||
case frame.FormatNV12:
|
case frame.FormatNV12:
|
||||||
return C.AVBindFrameFormatNV12, true
|
return C.AVBindFrameFormatNV12, true
|
||||||
case frame.FormatYUY2:
|
case frame.FormatYUYV:
|
||||||
return C.AVBindFrameFormatYUY2, true
|
return C.AVBindFrameFormatYUYV, true
|
||||||
case frame.FormatUYVY:
|
case frame.FormatUYVY:
|
||||||
return C.AVBindFrameFormatUYVY, true
|
return C.AVBindFrameFormatUYVY, true
|
||||||
default:
|
default:
|
||||||
@@ -59,8 +62,8 @@ func frameFormatFromAVBind(f C.AVBindFrameFormat) (frame.Format, bool) {
|
|||||||
return frame.FormatNV21, true
|
return frame.FormatNV21, true
|
||||||
case C.AVBindFrameFormatNV12:
|
case C.AVBindFrameFormatNV12:
|
||||||
return frame.FormatNV12, true
|
return frame.FormatNV12, true
|
||||||
case C.AVBindFrameFormatYUY2:
|
case C.AVBindFrameFormatYUYV:
|
||||||
return frame.FormatYUY2, true
|
return frame.FormatYUYV, true
|
||||||
case C.AVBindFrameFormatUYVY:
|
case C.AVBindFrameFormatUYVY:
|
||||||
return frame.FormatUYVY, true
|
return frame.FormatUYVY, true
|
||||||
default:
|
default:
|
||||||
@@ -85,6 +88,7 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
|||||||
for i := range devices {
|
for i := range devices {
|
||||||
devices[i].UID = C.GoString(&cDevices[i].uid[0])
|
devices[i].UID = C.GoString(&cDevices[i].uid[0])
|
||||||
devices[i].cDevice = cDevices[i]
|
devices[i].cDevice = cDevices[i]
|
||||||
|
devices[i].Name = C.GoString(&cDevices[i].name[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices, nil
|
return devices, nil
|
||||||
@@ -93,9 +97,13 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
|||||||
// ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the
|
// ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the
|
||||||
// the underlying callback can be retrieved by calling Read.
|
// the underlying callback can be retrieved by calling Read.
|
||||||
type ReadCloser struct {
|
type ReadCloser struct {
|
||||||
dataChan chan []byte
|
dataChan chan []byte
|
||||||
id handleID
|
id handleID
|
||||||
onClose func()
|
onClose func()
|
||||||
|
cancelCtx context.Context
|
||||||
|
cancelFunc func()
|
||||||
|
closeWG sync.WaitGroup
|
||||||
|
lock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReadCloser(onClose func()) *ReadCloser {
|
func newReadCloser(onClose func()) *ReadCloser {
|
||||||
@@ -103,12 +111,25 @@ func newReadCloser(onClose func()) *ReadCloser {
|
|||||||
rc.dataChan = make(chan []byte, 1)
|
rc.dataChan = make(chan []byte, 1)
|
||||||
rc.onClose = onClose
|
rc.onClose = onClose
|
||||||
rc.id = register(rc.dataCb)
|
rc.id = register(rc.dataCb)
|
||||||
|
cancelCtx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
rc.cancelCtx = cancelCtx
|
||||||
|
rc.cancelFunc = cancelFunc
|
||||||
return &rc
|
return &rc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *ReadCloser) dataCb(data []byte) {
|
func (rc *ReadCloser) dataCb(data []byte) {
|
||||||
|
rc.closeWG.Add(1)
|
||||||
|
defer rc.closeWG.Done()
|
||||||
|
|
||||||
// TODO: add a policy for slow reader
|
// 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:
|
// Read reads raw data, the format is determined by the media type and property:
|
||||||
@@ -124,17 +145,28 @@ func (rc *ReadCloser) Read() ([]byte, func(), error) {
|
|||||||
|
|
||||||
// Close closes the capturing session, and no data will flow anymore
|
// Close closes the capturing session, and no data will flow anymore
|
||||||
func (rc *ReadCloser) Close() {
|
func (rc *ReadCloser) Close() {
|
||||||
|
rc.lock.Lock()
|
||||||
|
defer rc.lock.Unlock()
|
||||||
|
|
||||||
|
if rc.cancelCtx.Err() != nil {
|
||||||
|
return // already closed
|
||||||
|
}
|
||||||
|
|
||||||
if rc.onClose != nil {
|
if rc.onClose != nil {
|
||||||
rc.onClose()
|
rc.onClose()
|
||||||
}
|
}
|
||||||
close(rc.dataChan)
|
rc.cancelFunc()
|
||||||
unregister(rc.id)
|
unregister(rc.id)
|
||||||
|
rc.closeWG.Wait()
|
||||||
|
close(rc.dataChan)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session represents a capturing session.
|
// Session represents a capturing session.
|
||||||
type Session struct {
|
type Session struct {
|
||||||
device Device
|
device Device
|
||||||
cSession C.PAVBindSession
|
cSession C.PAVBindSession
|
||||||
|
lock sync.Mutex
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSession creates a new capturing session
|
// NewSession creates a new capturing session
|
||||||
@@ -152,6 +184,13 @@ func NewSession(device Device) (*Session, error) {
|
|||||||
|
|
||||||
// Close stops capturing session and frees up resources
|
// Close stops capturing session and frees up resources
|
||||||
func (session *Session) Close() error {
|
func (session *Session) Close() error {
|
||||||
|
session.lock.Lock()
|
||||||
|
defer session.lock.Unlock()
|
||||||
|
if session.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
session.closed = true
|
||||||
|
|
||||||
if session.cSession == nil {
|
if session.cSession == nil {
|
||||||
return 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,6 +1,8 @@
|
|||||||
package codec
|
package codec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
@@ -8,7 +10,7 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"github.com/pion/rtp/codecs"
|
"github.com/pion/rtp/codecs"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future.
|
// RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future.
|
||||||
@@ -37,6 +39,23 @@ func NewRTPH264Codec(clockrate uint32) *RTPCodec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// NewRTPVP8Codec is a helper to create an VP8 codec
|
||||||
func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
|
func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
|
||||||
return &RTPCodec{
|
return &RTPCodec{
|
||||||
@@ -71,6 +90,23 @@ func NewRTPVP9Codec(clockrate uint32) *RTPCodec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// NewRTPOpusCodec is a helper to create an Opus codec
|
||||||
func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
|
func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
|
||||||
return &RTPCodec{
|
return &RTPCodec{
|
||||||
@@ -119,10 +155,19 @@ type ReadCloser interface {
|
|||||||
Controllable
|
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.
|
// EncoderController is the interface allowing to control the encoder behaviour after it's initialisation.
|
||||||
// It will possibly have common control method in the future.
|
// It will possibly have common control method in the future.
|
||||||
// A controller can have optional methods represented by *Controller interfaces
|
// A controller can have optional methods represented by *Controller interfaces
|
||||||
type EncoderController interface{}
|
type EncoderController any
|
||||||
|
|
||||||
// Controllable is a interface representing a encoder which can be controlled
|
// Controllable is a interface representing a encoder which can be controlled
|
||||||
// after it's initialisation with an EncoderController
|
// after it's initialisation with an EncoderController
|
||||||
@@ -145,6 +190,12 @@ type BitRateController interface {
|
|||||||
SetBitRate(int) error
|
SetBitRate(int) 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
|
// BaseParams represents an codec's encoding properties
|
||||||
type BaseParams struct {
|
type BaseParams struct {
|
||||||
// Target bitrate in bps.
|
// Target bitrate in bps.
|
||||||
|
@@ -119,3 +119,41 @@ func VideoEncoderCloseTwiceTest(t *testing.T, c codec.VideoEncoderBuilder, p pro
|
|||||||
t.Fatal(err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -64,10 +64,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
imgReal := img.(*image.YCbCr)
|
imgReal := img.(*image.YCbCr)
|
||||||
var y, cb, cr C.Slice
|
var y, cb, cr C.Slice
|
||||||
y.data = (*C.uchar)(&imgReal.Y[0])
|
y.data = (*C.uchar)(&imgReal.Y[0])
|
||||||
|
@@ -44,6 +44,25 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
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) {
|
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||||
|
@@ -21,29 +21,26 @@ Encoder *enc_new(const EncoderOptions opts, int *eresult) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove hardcoded values
|
params.iUsageType = opts.usage_type;
|
||||||
params.iUsageType = CAMERA_VIDEO_REAL_TIME;
|
|
||||||
params.iPicWidth = opts.width;
|
params.iPicWidth = opts.width;
|
||||||
params.iPicHeight = opts.height;
|
params.iPicHeight = opts.height;
|
||||||
params.iTargetBitrate = opts.target_bitrate;
|
params.iTargetBitrate = opts.target_bitrate;
|
||||||
params.iMaxBitrate = opts.target_bitrate;
|
params.iMaxBitrate = opts.target_bitrate;
|
||||||
params.iRCMode = RC_BITRATE_MODE;
|
params.iRCMode = opts.rc_mode;
|
||||||
params.fMaxFrameRate = opts.max_fps;
|
params.fMaxFrameRate = opts.max_fps;
|
||||||
params.bEnableFrameSkip = true;
|
params.bEnableFrameSkip = opts.enable_frame_skip;
|
||||||
params.uiMaxNalSize = 0;
|
params.uiMaxNalSize = opts.max_nal_size;
|
||||||
params.uiIntraPeriod = 30;
|
params.uiIntraPeriod = opts.intra_period;
|
||||||
// set to 0, so that it'll automatically use multi threads when needed
|
params.iMultipleThreadIdc = opts.multiple_thread_idc;
|
||||||
params.iMultipleThreadIdc = 0;
|
|
||||||
// The base spatial layer 0 is the only one we use.
|
// The base spatial layer 0 is the only one we use.
|
||||||
params.sSpatialLayers[0].iVideoWidth = params.iPicWidth;
|
params.sSpatialLayers[0].iVideoWidth = params.iPicWidth;
|
||||||
params.sSpatialLayers[0].iVideoHeight = params.iPicHeight;
|
params.sSpatialLayers[0].iVideoHeight = params.iPicHeight;
|
||||||
params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate;
|
params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate;
|
||||||
params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate;
|
params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate;
|
||||||
params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate;
|
params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate;
|
||||||
// Single NAL unit mode
|
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = opts.slice_num;
|
||||||
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = 1;
|
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = opts.slice_mode;
|
||||||
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SIZELIMITED_SLICE;
|
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = opts.slice_size_constraint;
|
||||||
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = 12800;
|
|
||||||
|
|
||||||
rv = engine->InitializeExt(¶ms);
|
rv = engine->InitializeExt(¶ms);
|
||||||
if (rv != 0) {
|
if (rv != 0) {
|
||||||
@@ -72,6 +69,16 @@ void enc_free(Encoder *e, int *eresult) {
|
|||||||
free(e);
|
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
|
// There's a good reference from ffmpeg in using the encode_frame
|
||||||
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
|
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
|
||||||
Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
||||||
@@ -81,16 +88,15 @@ Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
|||||||
Slice payload = {0};
|
Slice payload = {0};
|
||||||
|
|
||||||
if(e->force_key_frame == 1) {
|
if(e->force_key_frame == 1) {
|
||||||
info.eFrameType = videoFrameTypeI;
|
e->engine->ForceIntraFrame(true);
|
||||||
e->force_key_frame = 0;
|
e->force_key_frame = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pic.iPicWidth = f.width;
|
pic.iPicWidth = f.width;
|
||||||
pic.iPicHeight = f.height;
|
pic.iPicHeight = f.height;
|
||||||
pic.iColorFormat = videoFormatI420;
|
pic.iColorFormat = videoFormatI420;
|
||||||
// We always received I420 format
|
pic.iStride[0] = f.ystride;
|
||||||
pic.iStride[0] = pic.iPicWidth;
|
pic.iStride[1] = pic.iStride[2] = f.cstride;
|
||||||
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth / 2;
|
|
||||||
pic.pData[0] = (unsigned char *)f.y;
|
pic.pData[0] = (unsigned char *)f.y;
|
||||||
pic.pData[1] = (unsigned char *)f.u;
|
pic.pData[1] = (unsigned char *)f.u;
|
||||||
pic.pData[2] = (unsigned char *)f.v;
|
pic.pData[2] = (unsigned char *)f.v;
|
||||||
|
@@ -12,6 +12,8 @@ typedef struct Slice {
|
|||||||
|
|
||||||
typedef struct Frame {
|
typedef struct Frame {
|
||||||
void *y, *u, *v;
|
void *y, *u, *v;
|
||||||
|
int ystride;
|
||||||
|
int cstride;
|
||||||
int height;
|
int height;
|
||||||
int width;
|
int width;
|
||||||
} Frame;
|
} Frame;
|
||||||
@@ -20,6 +22,15 @@ typedef struct EncoderOptions {
|
|||||||
int width, height;
|
int width, height;
|
||||||
int target_bitrate;
|
int target_bitrate;
|
||||||
float max_fps;
|
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;
|
} EncoderOptions;
|
||||||
|
|
||||||
typedef struct Encoder {
|
typedef struct Encoder {
|
||||||
@@ -33,6 +44,7 @@ typedef struct Encoder {
|
|||||||
Encoder *enc_new(const EncoderOptions params, int *eresult);
|
Encoder *enc_new(const EncoderOptions params, int *eresult);
|
||||||
void enc_free(Encoder *e, int *eresult);
|
void enc_free(Encoder *e, int *eresult);
|
||||||
Slice enc_encode(Encoder *e, Frame f, int *eresult);
|
Slice enc_encode(Encoder *e, Frame f, int *eresult);
|
||||||
|
void enc_set_bitrate(Encoder *e, int bitrate);
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
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.
@@ -33,10 +33,19 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser,
|
|||||||
|
|
||||||
var rv C.int
|
var rv C.int
|
||||||
cEncoder := C.enc_new(C.EncoderOptions{
|
cEncoder := C.enc_new(C.EncoderOptions{
|
||||||
width: C.int(p.Width),
|
width: C.int(p.Width),
|
||||||
height: C.int(p.Height),
|
height: C.int(p.Height),
|
||||||
target_bitrate: C.int(params.BitRate),
|
target_bitrate: C.int(params.BitRate),
|
||||||
max_fps: C.float(p.FrameRate),
|
max_fps: C.float(p.FrameRate),
|
||||||
|
usage_type: C.EUsageType(params.UsageType),
|
||||||
|
rc_mode: C.RC_MODES(params.RCMode),
|
||||||
|
enable_frame_skip: C.bool(params.EnableFrameSkip),
|
||||||
|
max_nal_size: C.uint(params.MaxNalSize),
|
||||||
|
intra_period: C.uint(params.IntraPeriod),
|
||||||
|
multiple_thread_idc: C.int(params.MultipleThreadIdc),
|
||||||
|
slice_num: C.uint(params.SliceNum),
|
||||||
|
slice_mode: C.SliceModeEnum(params.SliceMode),
|
||||||
|
slice_size_constraint: C.uint(params.SliceSizeConstraint),
|
||||||
}, &rv)
|
}, &rv)
|
||||||
if err := errResult(rv); err != nil {
|
if err := errResult(rv); err != nil {
|
||||||
return nil, fmt.Errorf("failed in creating encoder: %v", err)
|
return nil, fmt.Errorf("failed in creating encoder: %v", err)
|
||||||
@@ -56,20 +65,23 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
|
|
||||||
yuvImg := img.(*image.YCbCr)
|
yuvImg := img.(*image.YCbCr)
|
||||||
bounds := yuvImg.Bounds()
|
bounds := yuvImg.Bounds()
|
||||||
var rv C.int
|
var rv C.int
|
||||||
s := C.enc_encode(e.engine, C.Frame{
|
s := C.enc_encode(e.engine, C.Frame{
|
||||||
y: unsafe.Pointer(&yuvImg.Y[0]),
|
y: unsafe.Pointer(&yuvImg.Y[0]),
|
||||||
u: unsafe.Pointer(&yuvImg.Cb[0]),
|
u: unsafe.Pointer(&yuvImg.Cb[0]),
|
||||||
v: unsafe.Pointer(&yuvImg.Cr[0]),
|
v: unsafe.Pointer(&yuvImg.Cr[0]),
|
||||||
height: C.int(bounds.Max.Y - bounds.Min.Y),
|
ystride: C.int(yuvImg.YStride),
|
||||||
width: C.int(bounds.Max.X - bounds.Min.X),
|
cstride: C.int(yuvImg.CStride),
|
||||||
|
height: C.int(bounds.Max.Y - bounds.Min.Y),
|
||||||
|
width: C.int(bounds.Max.X - bounds.Min.X),
|
||||||
}, &rv)
|
}, &rv)
|
||||||
if err := errResult(rv); err != nil {
|
if err := errResult(rv); err != nil {
|
||||||
return nil, func() {}, fmt.Errorf("failed in encoding: %v", err)
|
return nil, func() {}, fmt.Errorf("failed in encoding: %v", err)
|
||||||
@@ -84,6 +96,11 @@ func (e *encoder) ForceKeyFrame() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *encoder) SetBitRate(bitrate int) error {
|
||||||
|
C.enc_set_bitrate(e.engine, C.int(bitrate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoder) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@@ -8,5 +8,6 @@ package openh264
|
|||||||
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-arm64.a
|
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-arm64.a
|
||||||
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-x64.a
|
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-x64.a
|
||||||
//#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-darwin-x64.a
|
//#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-darwin-x64.a
|
||||||
|
//#cgo darwin,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264-darwin-arm64.a
|
||||||
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-windows-x64.a -lssp
|
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-windows-x64.a -lssp
|
||||||
import "C"
|
import "C"
|
||||||
|
@@ -60,4 +60,23 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
package openh264
|
package openh264
|
||||||
|
|
||||||
|
// #include <openh264/codec_api.h>
|
||||||
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
@@ -9,14 +12,62 @@ import (
|
|||||||
// Params stores libopenh264 specific encoding parameters.
|
// Params stores libopenh264 specific encoding parameters.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
codec.BaseParams
|
codec.BaseParams
|
||||||
|
UsageType UsageTypeEnum
|
||||||
|
RCMode RCModeEnum
|
||||||
|
EnableFrameSkip bool
|
||||||
|
MaxNalSize uint
|
||||||
|
IntraPeriod uint
|
||||||
|
MultipleThreadIdc int
|
||||||
|
SliceNum uint
|
||||||
|
SliceMode SliceModeEnum
|
||||||
|
SliceSizeConstraint uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UsageTypeEnum int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CameraVideoRealTime UsageTypeEnum = C.CAMERA_VIDEO_REAL_TIME ///< camera video for real-time communication
|
||||||
|
ScreenContentRealTime UsageTypeEnum = C.SCREEN_CONTENT_REAL_TIME ///< screen content signal
|
||||||
|
CameraVideoNonRealTime UsageTypeEnum = C.CAMERA_VIDEO_NON_REAL_TIME
|
||||||
|
ScreenContentNonRealTime UsageTypeEnum = C.SCREEN_CONTENT_NON_REAL_TIME
|
||||||
|
InputContentTypeAll UsageTypeEnum = C.INPUT_CONTENT_TYPE_ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
type RCModeEnum int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RCQualityMode RCModeEnum = C.RC_QUALITY_MODE ///< quality mode
|
||||||
|
RCBitrateMode RCModeEnum = C.RC_BITRATE_MODE ///< bitrate mode
|
||||||
|
RCBufferbaseedMode RCModeEnum = C.RC_BUFFERBASED_MODE ///< no bitrate control,only using buffer status,adjust the video quality
|
||||||
|
RCTimestampMode RCModeEnum = C.RC_TIMESTAMP_MODE //rate control based timestamp
|
||||||
|
RCBitrateModePostSkip RCModeEnum = C.RC_BITRATE_MODE_POST_SKIP ///< this is in-building RC MODE, WILL BE DELETED after algorithm tuning!
|
||||||
|
RCOffMode RCModeEnum = C.RC_OFF_MODE ///< rate control off mode
|
||||||
|
)
|
||||||
|
|
||||||
|
type SliceModeEnum uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
SMSingleSlice SliceModeEnum = C.SM_SINGLE_SLICE ///< | SliceNum==1
|
||||||
|
SMFixedslcnumSlice SliceModeEnum = C.SM_FIXEDSLCNUM_SLICE ///< | according to SliceNum | enabled dynamic slicing for multi-thread
|
||||||
|
SMRasterSlice SliceModeEnum = C.SM_RASTER_SLICE ///< | according to SlicesAssign | need input of MB numbers each slice. In addition, if other constraint in SSliceArgument is presented, need to follow the constraints. Typically if MB num and slice size are both constrained, re-encoding may be involved.
|
||||||
|
SMSizelimitedSlice SliceModeEnum = C.SM_SIZELIMITED_SLICE ///< | according to SliceSize | slicing according to size, the slicing will be dynamic(have no idea about slice_nums until encoding current frame)
|
||||||
|
)
|
||||||
|
|
||||||
// NewParams returns default openh264 codec specific parameters.
|
// NewParams returns default openh264 codec specific parameters.
|
||||||
func NewParams() (Params, error) {
|
func NewParams() (Params, error) {
|
||||||
return Params{
|
return Params{
|
||||||
BaseParams: codec.BaseParams{
|
BaseParams: codec.BaseParams{
|
||||||
BitRate: 100000,
|
BitRate: 100000,
|
||||||
},
|
},
|
||||||
|
UsageType: CameraVideoRealTime,
|
||||||
|
RCMode: RCBitrateMode,
|
||||||
|
EnableFrameSkip: true,
|
||||||
|
MaxNalSize: 0,
|
||||||
|
IntraPeriod: 30,
|
||||||
|
MultipleThreadIdc: 0, // Defaults to 0, so that it'll automatically use multi threads when needed
|
||||||
|
SliceNum: 1, // Defaults to single NAL unit mode
|
||||||
|
SliceMode: SMSizelimitedSlice,
|
||||||
|
SliceSizeConstraint: 12800,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
pkg/codec/opus/lib/libopus-darwin-arm64.a
Normal file
BIN
pkg/codec/opus/lib/libopus-darwin-arm64.a
Normal file
Binary file not shown.
@@ -3,6 +3,8 @@ package opus
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
@@ -14,7 +16,7 @@ import (
|
|||||||
/*
|
/*
|
||||||
#include <opus.h>
|
#include <opus.h>
|
||||||
|
|
||||||
int bridge_encoder_set_bitrate(OpusEncoder *e, opus_int32 bitrate)
|
int pion_set_encoder_bitrate(OpusEncoder *e, opus_int32 bitrate)
|
||||||
{
|
{
|
||||||
return opus_encoder_ctl(e, OPUS_SET_BITRATE(bitrate));
|
return opus_encoder_ctl(e, OPUS_SET_BITRATE(bitrate));
|
||||||
}
|
}
|
||||||
@@ -25,6 +27,8 @@ type encoder struct {
|
|||||||
inBuff wave.Audio
|
inBuff wave.Audio
|
||||||
reader audio.Reader
|
reader audio.Reader
|
||||||
engine *C.OpusEncoder
|
engine *C.OpusEncoder
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
||||||
@@ -79,6 +83,12 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
if e.engine == nil {
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
encoded := make([]byte, 1024)
|
encoded := make([]byte, 1024)
|
||||||
var n C.opus_int32
|
var n C.opus_int32
|
||||||
switch b := buff.(type) {
|
switch b := buff.(type) {
|
||||||
@@ -110,7 +120,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) SetBitRate(bitRate int) error {
|
func (e *encoder) SetBitRate(bitRate int) error {
|
||||||
cerror := C.bridge_encoder_set_bitrate(
|
cerror := C.pion_set_encoder_bitrate(
|
||||||
e.engine,
|
e.engine,
|
||||||
C.int(bitRate),
|
C.int(bitRate),
|
||||||
)
|
)
|
||||||
@@ -126,6 +136,8 @@ func (e *encoder) Controller() codec.EncoderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) Close() error {
|
func (e *encoder) Close() error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
if e.engine == nil {
|
if e.engine == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -8,5 +8,6 @@ package opus
|
|||||||
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopus-linux-arm64.a -lm
|
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopus-linux-arm64.a -lm
|
||||||
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-linux-x64.a -lm
|
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-linux-x64.a -lm
|
||||||
//#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-darwin-x64.a
|
//#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-darwin-x64.a
|
||||||
|
//#cgo darwin,arm64 LDFLAGS: ${SRCDIR}/lib/libopus-darwin-arm64.a
|
||||||
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-windows-x64.a
|
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopus-windows-x64.a
|
||||||
import "C"
|
import "C"
|
||||||
|
@@ -57,4 +57,23 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||||
|
p, err := NewParams()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
codectest.AudioEncoderReadAfterCloseTest(t, &p,
|
||||||
|
prop.Media{
|
||||||
|
Audio: prop.Audio{
|
||||||
|
SampleRate: 48000,
|
||||||
|
ChannelCount: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wave.NewInt16Interleaved(wave.ChunkInfo{
|
||||||
|
Len: 960,
|
||||||
|
SamplingRate: 48000,
|
||||||
|
Channels: 2,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,25 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||||
|
p, err := factory()
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,6 +62,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
@@ -100,6 +101,8 @@ type encoderVP8 struct {
|
|||||||
|
|
||||||
rate *framerateDetector
|
rate *framerateDetector
|
||||||
|
|
||||||
|
forceKeyFrame atomic.Bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
@@ -304,10 +307,11 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
yuvImg := img.(*image.YCbCr)
|
yuvImg := img.(*image.YCbCr)
|
||||||
|
|
||||||
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
||||||
@@ -315,7 +319,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
|||||||
|
|
||||||
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
||||||
|
|
||||||
if kf {
|
if kf || e.forceKeyFrame.CompareAndSwap(true, false) {
|
||||||
// Key frame
|
// Key frame
|
||||||
C.setForceKFFlagVP8(&e.picParam, 1)
|
C.setForceKFFlagVP8(&e.picParam, 1)
|
||||||
C.setFrameTypeFlagVP8(&e.picParam, 0)
|
C.setFrameTypeFlagVP8(&e.picParam, 0)
|
||||||
@@ -541,10 +545,14 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
|||||||
return encoded, func() {}, err
|
return encoded, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoderVP8) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *encoderVP8) ForceKeyFrame() {
|
||||||
|
e.forceKeyFrame.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoderVP8) Close() error {
|
func (e *encoderVP8) Close() error {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
func TestVP8ShouldImplementBitRateControl(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement bit rate control
|
t.SkipNow() // TODO: Implement bit rate control
|
||||||
|
|
||||||
e := &encoderVP8{}
|
e := &encoderVP8{}
|
||||||
@@ -17,9 +17,7 @@ func TestShouldImplementBitRateControl(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
func TestVP8ShouldImplementKeyFrameControl(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement key frame control
|
|
||||||
|
|
||||||
e := &encoderVP8{}
|
e := &encoderVP8{}
|
||||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||||
t.Error()
|
t.Error()
|
||||||
|
@@ -45,6 +45,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
@@ -92,6 +93,8 @@ type encoderVP9 struct {
|
|||||||
|
|
||||||
rate *framerateDetector
|
rate *framerateDetector
|
||||||
|
|
||||||
|
forceKeyFrame atomic.Bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
@@ -293,10 +296,11 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
yuvImg := img.(*image.YCbCr)
|
yuvImg := img.(*image.YCbCr)
|
||||||
|
|
||||||
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
||||||
@@ -304,7 +308,7 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
|||||||
|
|
||||||
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
||||||
|
|
||||||
if kf {
|
if kf || e.forceKeyFrame.CompareAndSwap(true, false) {
|
||||||
C.setForceKFFlag9(&e.picParam, 1)
|
C.setForceKFFlag9(&e.picParam, 1)
|
||||||
C.setFrameTypeFlagVP9(&e.picParam, 0)
|
C.setFrameTypeFlagVP9(&e.picParam, 0)
|
||||||
e.picParam.refresh_frame_flags = 0
|
e.picParam.refresh_frame_flags = 0
|
||||||
@@ -476,10 +480,14 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
|||||||
return encoded, func() {}, err
|
return encoded, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoderVP9) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *encoderVP9) ForceKeyFrame() {
|
||||||
|
e.forceKeyFrame.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoderVP9) Close() error {
|
func (e *encoderVP9) Close() error {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
@@ -3,7 +3,12 @@
|
|||||||
|
|
||||||
package vaapi
|
package vaapi
|
||||||
|
|
||||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
import (
|
||||||
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVP9ShouldImplementBitRateControl(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement bit rate control
|
t.SkipNow() // TODO: Implement bit rate control
|
||||||
|
|
||||||
e := &encoderVP9{}
|
e := &encoderVP9{}
|
||||||
@@ -12,9 +17,7 @@ func TestShouldImplementBitRateControl(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
func TestVP9ShouldImplementKeyFrameControl(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement key frame control
|
|
||||||
|
|
||||||
e := &encoderVP9{}
|
e := &encoderVP9{}
|
||||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||||
t.Error()
|
t.Error()
|
||||||
|
@@ -54,6 +54,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -69,17 +70,24 @@ type encoder struct {
|
|||||||
cfg *C.vpx_codec_enc_cfg_t
|
cfg *C.vpx_codec_enc_cfg_t
|
||||||
r video.Reader
|
r video.Reader
|
||||||
frameIndex int
|
frameIndex int
|
||||||
tStart int
|
tStart time.Time
|
||||||
tLastFrame int
|
tLastFrame time.Time
|
||||||
frame []byte
|
frame []byte
|
||||||
deadline int
|
deadline int
|
||||||
requireKeyFrame bool
|
requireKeyFrame bool
|
||||||
|
targetBitrate int
|
||||||
isKeyFrame bool
|
isKeyFrame bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
kRateControlThreshold = 0.15
|
||||||
|
kMinQuantizer = 20
|
||||||
|
kMaxQuantizer = 63
|
||||||
|
)
|
||||||
|
|
||||||
// VP8Params is codec specific paramaters
|
// VP8Params is codec specific paramaters
|
||||||
type VP8Params struct {
|
type VP8Params struct {
|
||||||
Params
|
Params
|
||||||
@@ -198,16 +206,17 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
|
|||||||
); ec != 0 {
|
); ec != 0 {
|
||||||
return nil, fmt.Errorf("vpx_codec_enc_init failed (%d)", ec)
|
return nil, fmt.Errorf("vpx_codec_enc_init failed (%d)", ec)
|
||||||
}
|
}
|
||||||
t0 := time.Now().Nanosecond() / 1000000
|
t0 := time.Now()
|
||||||
return &encoder{
|
return &encoder{
|
||||||
r: video.ToI420(r),
|
r: video.ToI420(r),
|
||||||
codec: codec,
|
codec: codec,
|
||||||
raw: rawNoBuffer,
|
raw: rawNoBuffer,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
tStart: t0,
|
tStart: t0,
|
||||||
tLastFrame: t0,
|
tLastFrame: t0,
|
||||||
deadline: int(params.Deadline / time.Microsecond),
|
deadline: int(params.Deadline / time.Microsecond),
|
||||||
frame: make([]byte, 1024),
|
frame: make([]byte, 1024),
|
||||||
|
targetBitrate: params.BitRate,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +228,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
yuvImg := img.(*image.YCbCr)
|
yuvImg := img.(*image.YCbCr)
|
||||||
bounds := yuvImg.Bounds()
|
bounds := yuvImg.Bounds()
|
||||||
height := C.int(bounds.Dy())
|
height := C.int(bounds.Dy())
|
||||||
@@ -232,7 +242,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
e.raw.stride[1] = C.int(yuvImg.CStride)
|
e.raw.stride[1] = C.int(yuvImg.CStride)
|
||||||
e.raw.stride[2] = C.int(yuvImg.CStride)
|
e.raw.stride[2] = C.int(yuvImg.CStride)
|
||||||
|
|
||||||
t := time.Now().Nanosecond() / 1000000
|
t := time.Now()
|
||||||
|
|
||||||
if e.cfg.g_w != C.uint(width) || e.cfg.g_h != C.uint(height) {
|
if e.cfg.g_w != C.uint(width) || e.cfg.g_h != C.uint(height) {
|
||||||
e.cfg.g_w, e.cfg.g_h = C.uint(width), C.uint(height)
|
e.cfg.g_w, e.cfg.g_h = C.uint(width), C.uint(height)
|
||||||
@@ -251,7 +261,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
duration := t - e.tLastFrame
|
if ec := C.vpx_codec_enc_config_set(e.codec, e.cfg); ec != 0 {
|
||||||
|
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := t.Sub(e.tLastFrame).Microseconds()
|
||||||
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
||||||
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
||||||
// and consequently the codec will read the first frame from the buffer. This means the first frame won't
|
// and consequently the codec will read the first frame from the buffer. This means the first frame won't
|
||||||
@@ -260,13 +274,23 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
if duration == 0 {
|
if duration == 0 {
|
||||||
duration = 1
|
duration = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetVpxBitrate := C.uint(float32(e.targetBitrate / 1000)) // convert to kilobits / second
|
||||||
|
if e.cfg.rc_target_bitrate != targetVpxBitrate && targetVpxBitrate >= 1 {
|
||||||
|
e.cfg.rc_target_bitrate = targetVpxBitrate
|
||||||
|
rc := C.vpx_codec_enc_config_set(e.codec, e.cfg)
|
||||||
|
if rc != C.VPX_CODEC_OK {
|
||||||
|
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var flags int
|
var flags int
|
||||||
if e.requireKeyFrame {
|
if e.requireKeyFrame {
|
||||||
flags = flags | C.VPX_EFLAG_FORCE_KF
|
flags = flags | C.VPX_EFLAG_FORCE_KF
|
||||||
}
|
}
|
||||||
if ec := C.encode_wrapper(
|
if ec := C.encode_wrapper(
|
||||||
e.codec, e.raw,
|
e.codec, e.raw,
|
||||||
C.long(t-e.tStart), C.ulong(duration), C.long(flags), C.ulong(e.deadline),
|
C.long(t.Sub(e.tStart).Microseconds()), C.ulong(duration), C.long(flags), C.ulong(e.deadline),
|
||||||
(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]),
|
(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]),
|
||||||
); ec != C.VPX_CODEC_OK {
|
); ec != C.VPX_CODEC_OK {
|
||||||
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
|
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
|
||||||
@@ -302,6 +326,31 @@ func (e *encoder) ForceKeyFrame() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *encoder) SetBitRate(bitrate int) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
e.targetBitrate = bitrate
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) DynamicQPControl(currentBitrate int, targetBitrate int) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
bitrateDiff := math.Abs(float64(currentBitrate - targetBitrate))
|
||||||
|
if bitrateDiff <= float64(currentBitrate)*kRateControlThreshold {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
currentMax := e.cfg.rc_max_quantizer
|
||||||
|
|
||||||
|
if targetBitrate < currentBitrate {
|
||||||
|
e.cfg.rc_max_quantizer = min(currentMax+1, kMaxQuantizer)
|
||||||
|
} else {
|
||||||
|
e.cfg.rc_max_quantizer = max(currentMax-1, kMinQuantizer)
|
||||||
|
}
|
||||||
|
e.cfg.rc_min_quantizer = e.cfg.rc_max_quantizer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoder) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
155
pkg/codec/vpx/vpx_decoder.go
Normal file
155
pkg/codec/vpx/vpx_decoder.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package vpx
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: vpx
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <vpx/vpx_decoder.h>
|
||||||
|
#include <vpx/vpx_codec.h>
|
||||||
|
#include <vpx/vpx_image.h>
|
||||||
|
#include <vpx/vp8dx.h>
|
||||||
|
|
||||||
|
vpx_codec_iface_t *ifaceVP8Decoder() {
|
||||||
|
return vpx_codec_vp8_dx();
|
||||||
|
}
|
||||||
|
vpx_codec_iface_t *ifaceVP9Decoder() {
|
||||||
|
return vpx_codec_vp9_dx();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocates a new decoder context
|
||||||
|
vpx_codec_ctx_t* newDecoderCtx() {
|
||||||
|
return (vpx_codec_ctx_t*)malloc(sizeof(vpx_codec_ctx_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes the decoder
|
||||||
|
vpx_codec_err_t decoderInit(vpx_codec_ctx_t* ctx, vpx_codec_iface_t* iface) {
|
||||||
|
return vpx_codec_dec_init_ver(ctx, iface, NULL, 0, VPX_DECODER_ABI_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decodes an encoded frame
|
||||||
|
vpx_codec_err_t decodeFrame(vpx_codec_ctx_t* ctx, const uint8_t* data, unsigned int data_sz) {
|
||||||
|
return vpx_codec_decode(ctx, data, data_sz, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an iterator
|
||||||
|
vpx_codec_iter_t* newIter() {
|
||||||
|
return (vpx_codec_iter_t*)malloc(sizeof(vpx_codec_iter_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the next decoded frame
|
||||||
|
vpx_image_t* getFrame(vpx_codec_ctx_t* ctx, vpx_codec_iter_t* iter) {
|
||||||
|
return vpx_codec_get_frame(ctx, iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frees a decoded frane
|
||||||
|
void freeFrame(vpx_image_t* f) {
|
||||||
|
vpx_img_free(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frees a decoder context
|
||||||
|
void freeDecoderCtx(vpx_codec_ctx_t* ctx) {
|
||||||
|
vpx_codec_destroy(ctx);
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
|
)
|
||||||
|
|
||||||
|
type decoder struct {
|
||||||
|
codec *C.vpx_codec_ctx_t
|
||||||
|
raw *C.vpx_image_t
|
||||||
|
cfg *C.vpx_codec_dec_cfg_t
|
||||||
|
iter C.vpx_codec_iter_t
|
||||||
|
frameIndex int
|
||||||
|
tStart time.Time
|
||||||
|
tLastFrame time.Time
|
||||||
|
reader io.Reader
|
||||||
|
buf []byte
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildVideoDecoder(r io.Reader, property prop.Media) (codec.VideoDecoder, error) {
|
||||||
|
return NewDecoder(r, property)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder(r io.Reader, p prop.Media) (codec.VideoDecoder, error) {
|
||||||
|
cfg := &C.vpx_codec_dec_cfg_t{}
|
||||||
|
cfg.threads = 1
|
||||||
|
cfg.w = C.uint(p.Width)
|
||||||
|
cfg.h = C.uint(p.Height)
|
||||||
|
|
||||||
|
codec := C.newDecoderCtx()
|
||||||
|
if C.decoderInit(codec, C.ifaceVP8Decoder()) != C.VPX_CODEC_OK {
|
||||||
|
return nil, fmt.Errorf("vpx_codec_dec_init failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &decoder{
|
||||||
|
codec: codec,
|
||||||
|
cfg: cfg,
|
||||||
|
iter: nil, // initialize to NULL to start iteration
|
||||||
|
reader: r,
|
||||||
|
buf: make([]byte, 1024*1024),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) Read() (image.Image, func(), error) {
|
||||||
|
var input *C.vpx_image_t
|
||||||
|
for {
|
||||||
|
input = C.getFrame(d.codec, &d.iter)
|
||||||
|
if input != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d.iter = nil
|
||||||
|
// Read if there are no remained frames in the decoder
|
||||||
|
n, err := d.reader.Read(d.buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
status := C.decodeFrame(d.codec, (*C.uint8_t)(&d.buf[0]), C.uint(n))
|
||||||
|
if status != C.VPX_CODEC_OK {
|
||||||
|
return nil, nil, fmt.Errorf("decode failed: %v", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w := int(input.d_w)
|
||||||
|
h := int(input.d_h)
|
||||||
|
yStride := int(input.stride[0])
|
||||||
|
uStride := int(input.stride[1])
|
||||||
|
vStride := int(input.stride[2])
|
||||||
|
|
||||||
|
ySrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[0])), yStride*h)
|
||||||
|
uSrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[1])), uStride*h/2)
|
||||||
|
vSrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[2])), vStride*h/2)
|
||||||
|
|
||||||
|
dst := image.NewYCbCr(image.Rect(0, 0, w, h), image.YCbCrSubsampleRatio420)
|
||||||
|
|
||||||
|
// copy luma
|
||||||
|
for r := 0; r < h; r++ {
|
||||||
|
copy(dst.Y[r*dst.YStride:r*dst.YStride+w], ySrc[r*yStride:r*yStride+w])
|
||||||
|
}
|
||||||
|
// copy chroma
|
||||||
|
for r := 0; r < h/2; r++ {
|
||||||
|
copy(dst.Cb[r*dst.CStride:r*dst.CStride+w/2], uSrc[r*uStride:r*uStride+w/2])
|
||||||
|
copy(dst.Cr[r*dst.CStride:r*dst.CStride+w/2], vSrc[r*vStride:r*vStride+w/2])
|
||||||
|
}
|
||||||
|
C.freeFrame(input)
|
||||||
|
return dst, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoder) Close() error {
|
||||||
|
C.freeDecoderCtx(d.codec)
|
||||||
|
d.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,16 +1,22 @@
|
|||||||
package vpx
|
package vpx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncoder(t *testing.T) {
|
func TestEncoder(t *testing.T) {
|
||||||
@@ -60,6 +66,25 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||||
|
p, err := factory()
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,9 +236,76 @@ func TestRequestKeyFrame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
func TestSetBitrate(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement bit rate control
|
for name, factory := range map[string]func() (codec.VideoEncoderBuilder, error){
|
||||||
|
"VP8": func() (codec.VideoEncoderBuilder, error) {
|
||||||
|
p, err := NewVP8Params()
|
||||||
|
return &p, err
|
||||||
|
},
|
||||||
|
"VP9": func() (codec.VideoEncoderBuilder, error) {
|
||||||
|
p, err := NewVP9Params()
|
||||||
|
// Disable latency to ease test and begin to receive packets for each input frame
|
||||||
|
p.LagInFrames = 0
|
||||||
|
return &p, err
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
factory := factory
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
param, err := factory()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialWidth, initialHeight, width, height int = 320, 240, 320, 240
|
||||||
|
|
||||||
|
var cnt uint32
|
||||||
|
r, err := param.BuildVideoEncoder(
|
||||||
|
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
i := atomic.AddUint32(&cnt, 1)
|
||||||
|
if i == 3 {
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
return image.NewYCbCr(
|
||||||
|
image.Rect(0, 0, width, height),
|
||||||
|
image.YCbCrSubsampleRatio420,
|
||||||
|
), func() {}, nil
|
||||||
|
}),
|
||||||
|
prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: initialWidth,
|
||||||
|
Height: initialHeight,
|
||||||
|
FrameRate: 1,
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, rel, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rel()
|
||||||
|
err = r.Controller().(codec.BitRateController).SetBitRate(1000) // 1000 bit/second is ridiculously low, but this is a testcase.
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, rel, err = r.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rel()
|
||||||
|
_, _, err = r.Read()
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||||
e := &encoder{}
|
e := &encoder{}
|
||||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||||
t.Error()
|
t.Error()
|
||||||
@@ -226,3 +318,201 @@ func TestShouldImplementKeyFrameControl(t *testing.T) {
|
|||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncoderFrameMonotonic(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
params, err := NewVP8Params()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder, err := params.BuildVideoEncoder(
|
||||||
|
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
return image.NewYCbCr(
|
||||||
|
image.Rect(0, 0, 320, 240),
|
||||||
|
image.YCbCrSubsampleRatio420,
|
||||||
|
), func() {}, nil
|
||||||
|
},
|
||||||
|
), prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: 320,
|
||||||
|
Height: 240,
|
||||||
|
FrameRate: 30,
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(33 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
ctxx, cancell := context.WithCancel(ctx)
|
||||||
|
defer cancell()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctxx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
_, rel, err := encoder.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVP8DynamicQPControl(t *testing.T) {
|
||||||
|
t.Run("VP8", func(t *testing.T) {
|
||||||
|
p, err := NewVP8Params()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p.LagInFrames = 0 // Disable frame lag buffering for real-time encoding
|
||||||
|
p.RateControlEndUsage = RateControlCBR
|
||||||
|
totalFrames := 100
|
||||||
|
frameRate := 10
|
||||||
|
initialWidth, initialHeight := 800, 600
|
||||||
|
var cnt uint32
|
||||||
|
|
||||||
|
r, err := p.BuildVideoEncoder(
|
||||||
|
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
i := atomic.AddUint32(&cnt, 1)
|
||||||
|
if i == uint32(totalFrames+1) {
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
img := image.NewYCbCr(image.Rect(0, 0, initialWidth, initialHeight), image.YCbCrSubsampleRatio420)
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
for i := range img.Y {
|
||||||
|
img.Y[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
for i := range img.Cb {
|
||||||
|
img.Cb[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
for i := range img.Cr {
|
||||||
|
img.Cr[i] = uint8(r.Intn(256))
|
||||||
|
}
|
||||||
|
return img, func() {}, nil
|
||||||
|
}),
|
||||||
|
prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: initialWidth,
|
||||||
|
Height: initialHeight,
|
||||||
|
FrameRate: float32(frameRate),
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
initialBitrate := 100
|
||||||
|
currentBitrate := initialBitrate
|
||||||
|
targetBitrate := 300
|
||||||
|
for i := 0; i < totalFrames; i++ {
|
||||||
|
r.Controller().(codec.KeyFrameController).ForceKeyFrame()
|
||||||
|
r.Controller().(codec.QPController).DynamicQPControl(currentBitrate, targetBitrate)
|
||||||
|
data, rel, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rel()
|
||||||
|
encodedSize := len(data)
|
||||||
|
currentBitrate = encodedSize * 8 / 1000 / frameRate
|
||||||
|
}
|
||||||
|
assert.Less(t, math.Abs(float64(targetBitrate-currentBitrate)), math.Abs(float64(initialBitrate-currentBitrate)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVP8EncodeDecode(t *testing.T) {
|
||||||
|
t.Run("VP8", func(t *testing.T) {
|
||||||
|
initialWidth, initialHeight := 800, 600
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
decoder, err := BuildVideoDecoder(reader, prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: initialWidth,
|
||||||
|
Height: initialHeight,
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error creating VP8 decoder: %v", err)
|
||||||
|
}
|
||||||
|
defer decoder.Close()
|
||||||
|
|
||||||
|
// [... encoder setup code ...]
|
||||||
|
p, err := NewVP8Params()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p.LagInFrames = 0 // Disable frame lag buffering for real-time encoding
|
||||||
|
p.RateControlEndUsage = RateControlCBR
|
||||||
|
totalFrames := 10
|
||||||
|
var cnt uint32
|
||||||
|
r, err := p.BuildVideoEncoder(
|
||||||
|
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
i := atomic.AddUint32(&cnt, 1)
|
||||||
|
if i == uint32(totalFrames+1) {
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
img := image.NewYCbCr(image.Rect(0, 0, initialWidth, initialHeight), image.YCbCrSubsampleRatio420)
|
||||||
|
return img, func() {}, nil
|
||||||
|
}),
|
||||||
|
prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: initialWidth,
|
||||||
|
Height: initialHeight,
|
||||||
|
FrameRate: 30,
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for {
|
||||||
|
img, rel, err := decoder.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("decoder read error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, initialWidth, img.Bounds().Dx())
|
||||||
|
assert.Equal(t, initialHeight, img.Bounds().Dy())
|
||||||
|
rel()
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// --- feed encoded frames to writer
|
||||||
|
for {
|
||||||
|
data, rel, err := r.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encoder error: %v", err)
|
||||||
|
}
|
||||||
|
_, werr := writer.Write(data)
|
||||||
|
rel()
|
||||||
|
if werr != nil {
|
||||||
|
t.Fatalf("writer error: %v", werr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Close()
|
||||||
|
wg.Wait()
|
||||||
|
assert.Equal(t, totalFrames, counter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
#define ERR_ALLOC_PICTURE -3
|
#define ERR_ALLOC_PICTURE -3
|
||||||
#define ERR_OPEN_ENGINE -4
|
#define ERR_OPEN_ENGINE -4
|
||||||
#define ERR_ENCODE -5
|
#define ERR_ENCODE -5
|
||||||
|
#define ERR_BITRATE_RECONFIG -6
|
||||||
|
|
||||||
typedef struct Slice {
|
typedef struct Slice {
|
||||||
unsigned char *data;
|
unsigned char *data;
|
||||||
@@ -18,6 +19,7 @@ typedef struct Encoder {
|
|||||||
x264_t *h;
|
x264_t *h;
|
||||||
x264_picture_t pic_in;
|
x264_picture_t pic_in;
|
||||||
x264_param_t param;
|
x264_param_t param;
|
||||||
|
int force_key_frame;
|
||||||
} Encoder;
|
} Encoder;
|
||||||
|
|
||||||
Encoder *enc_new(x264_param_t param, char *preset, int *rc) {
|
Encoder *enc_new(x264_param_t param, char *preset, int *rc) {
|
||||||
@@ -77,6 +79,22 @@ fail:
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define RC_MARGIN 10000 /* 1kilobits / second*/
|
||||||
|
static int apply_target_bitrate(Encoder *e, int target_bitrate) {
|
||||||
|
int target_encoder_bitrate = (int)target_bitrate / 1000;
|
||||||
|
if (e->param.rc.i_bitrate == target_encoder_bitrate || target_encoder_bitrate <= 1) {
|
||||||
|
return 0; // if no change to bitrate or target bitrate is too small, we return no error (0)
|
||||||
|
}
|
||||||
|
|
||||||
|
e->param.rc.i_bitrate = target_encoder_bitrate;
|
||||||
|
e->param.rc.f_rate_tolerance = 0.1;
|
||||||
|
e->param.rc.i_vbv_max_bitrate = target_encoder_bitrate + RC_MARGIN / 2;
|
||||||
|
e->param.rc.i_vbv_buffer_size = e->param.rc.i_vbv_max_bitrate;
|
||||||
|
e->param.rc.f_vbv_buffer_init = 0.6;
|
||||||
|
int success = x264_encoder_reconfig(e->h, &e->param);
|
||||||
|
return success; // 0 on success or negative on error
|
||||||
|
}
|
||||||
|
|
||||||
Slice enc_encode(Encoder *e, uint8_t *y, uint8_t *cb, uint8_t *cr, int *rc) {
|
Slice enc_encode(Encoder *e, uint8_t *y, uint8_t *cb, uint8_t *cr, int *rc) {
|
||||||
x264_nal_t *nal;
|
x264_nal_t *nal;
|
||||||
int i_nal;
|
int i_nal;
|
||||||
@@ -85,8 +103,14 @@ Slice enc_encode(Encoder *e, uint8_t *y, uint8_t *cb, uint8_t *cr, int *rc) {
|
|||||||
e->pic_in.img.plane[0] = y;
|
e->pic_in.img.plane[0] = y;
|
||||||
e->pic_in.img.plane[1] = cb;
|
e->pic_in.img.plane[1] = cb;
|
||||||
e->pic_in.img.plane[2] = cr;
|
e->pic_in.img.plane[2] = cr;
|
||||||
|
if (e->force_key_frame) {
|
||||||
|
e->pic_in.i_type = X264_TYPE_IDR;
|
||||||
|
} else {
|
||||||
|
e->pic_in.i_type = X264_TYPE_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
int frame_size = x264_encoder_encode(e->h, &nal, &i_nal, &e->pic_in, &pic_out);
|
int frame_size = x264_encoder_encode(e->h, &nal, &i_nal, &e->pic_in, &pic_out);
|
||||||
|
e->force_key_frame = 0;
|
||||||
Slice s = {.data_len = frame_size};
|
Slice s = {.data_len = frame_size};
|
||||||
if (frame_size <= 0) {
|
if (frame_size <= 0) {
|
||||||
*rc = ERR_ENCODE;
|
*rc = ERR_ENCODE;
|
||||||
|
@@ -39,6 +39,8 @@ func (e cerror) Error() string {
|
|||||||
return errOpenEngine.Error()
|
return errOpenEngine.Error()
|
||||||
case C.ERR_ENCODE:
|
case C.ERR_ENCODE:
|
||||||
return errEncode.Error()
|
return errEncode.Error()
|
||||||
|
case C.ERR_BITRATE_RECONFIG:
|
||||||
|
return errSetBitrate.Error()
|
||||||
default:
|
default:
|
||||||
return "unknown error"
|
return "unknown error"
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,7 @@ var (
|
|||||||
errAllocPicture = fmt.Errorf("failed to alloc picture")
|
errAllocPicture = fmt.Errorf("failed to alloc picture")
|
||||||
errOpenEngine = fmt.Errorf("failed to open x264")
|
errOpenEngine = fmt.Errorf("failed to open x264")
|
||||||
errEncode = fmt.Errorf("failed to encode")
|
errEncode = fmt.Errorf("failed to encode")
|
||||||
|
errSetBitrate = fmt.Errorf("failed to change x264 encoder bitrate")
|
||||||
)
|
)
|
||||||
|
|
||||||
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
||||||
@@ -102,10 +105,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
img, _, err := e.r.Read()
|
img, release, err := e.r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
yuvImg := img.(*image.YCbCr)
|
yuvImg := img.(*image.YCbCr)
|
||||||
|
|
||||||
var rc C.int
|
var rc C.int
|
||||||
@@ -124,6 +128,24 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
return encoded, func() {}, err
|
return encoded, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Implement bit rate control
|
||||||
|
//var _ codec.BitRateController = (*encoder)(nil)
|
||||||
|
|
||||||
|
func (e *encoder) ForceKeyFrame() error {
|
||||||
|
e.engine.force_key_frame = C.int(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) SetBitRate(bitrate int) error {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
errNum := C.apply_target_bitrate(e.engine, C.int(bitrate))
|
||||||
|
if err := errFromC(errNum); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoder) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,34 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getTestVideoEncoder() (codec.ReadCloser, error) {
|
||||||
|
p, err := NewParams()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.BitRate = 200000
|
||||||
|
enc, err := p.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
return image.NewYCbCr(
|
||||||
|
image.Rect(0, 0, 256, 144),
|
||||||
|
image.YCbCrSubsampleRatio420,
|
||||||
|
), nil, nil
|
||||||
|
}), prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: 256,
|
||||||
|
Height: 144,
|
||||||
|
FrameFormat: frame.FormatI420,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return enc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestEncoder(t *testing.T) {
|
func TestEncoder(t *testing.T) {
|
||||||
t.Run("SimpleRead", func(t *testing.T) {
|
t.Run("SimpleRead", func(t *testing.T) {
|
||||||
p, err := NewParams()
|
p, err := NewParams()
|
||||||
@@ -46,22 +71,76 @@ func TestEncoder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||||
|
p, err := NewParams()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p.BitRate = 200000
|
||||||
|
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 TestShouldImplementKeyFrameControl(t *testing.T) {
|
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement key frame control
|
|
||||||
|
|
||||||
e := &encoder{}
|
e := &encoder{}
|
||||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
func TestNoErrorOnForceKeyFrame(t *testing.T) {
|
||||||
t.SkipNow() // TODO: Implement bit rate control
|
enc, err := getTestVideoEncoder()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
kfc, ok := enc.Controller().(codec.KeyFrameController)
|
||||||
|
if !ok {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if err := kfc.ForceKeyFrame(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, rel, err := enc.Read() // try to read the encoded frame
|
||||||
|
rel()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||||
e := &encoder{}
|
e := &encoder{}
|
||||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoErrorOnSetBitRate(t *testing.T) {
|
||||||
|
enc, err := getTestVideoEncoder()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
brc, ok := enc.Controller().(codec.BitRateController)
|
||||||
|
if !ok {
|
||||||
|
t.Error()
|
||||||
|
}
|
||||||
|
if err := brc.SetBitRate(1000); err != nil { // 1000 bit/second is ridiculously low, but this is a testcase.
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, rel, err := enc.Read() // try to read the encoded frame
|
||||||
|
rel()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
pkg/driver/availability/error.go
Normal file
28
pkg/driver/availability/error.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package availability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnimplemented = NewError("not implemented")
|
||||||
|
ErrBusy = NewError("device or resource busy")
|
||||||
|
ErrNoDevice = NewError("no such device")
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorString struct {
|
||||||
|
s string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(text string) error {
|
||||||
|
return &errorString{text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsError(err error) bool {
|
||||||
|
var target *errorString
|
||||||
|
return errors.As(err, &target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errorString) Error() string {
|
||||||
|
return e.s
|
||||||
|
}
|
@@ -13,9 +13,15 @@ import (
|
|||||||
type camera struct {
|
type camera struct {
|
||||||
device avfoundation.Device
|
device avfoundation.Device
|
||||||
session *avfoundation.Session
|
session *avfoundation.Session
|
||||||
|
rcClose func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers camera devices. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
devices, err := avfoundation.Devices(avfoundation.Video)
|
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -26,6 +32,7 @@ func init() {
|
|||||||
driver.GetManager().Register(cam, driver.Info{
|
driver.GetManager().Register(cam, driver.Info{
|
||||||
Label: device.UID,
|
Label: device.UID,
|
||||||
DeviceType: driver.Camera,
|
DeviceType: driver.Camera,
|
||||||
|
Name: device.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +50,9 @@ func (cam *camera) Open() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cam *camera) Close() error {
|
func (cam *camera) Close() error {
|
||||||
|
if cam.rcClose != nil {
|
||||||
|
cam.rcClose()
|
||||||
|
}
|
||||||
return cam.session.Close()
|
return cam.session.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +66,7 @@ func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cam.rcClose = rc.Close
|
||||||
r := video.ReaderFunc(func() (image.Image, func(), error) {
|
r := video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
frame, _, err := rc.Read()
|
frame, _, err := rc.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -8,9 +8,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
|
|
||||||
"github.com/blackjack/webcam"
|
"github.com/blackjack/webcam"
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
@@ -59,6 +65,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const bufCount = 2
|
||||||
|
|
||||||
// Camera implementation using v4l2
|
// Camera implementation using v4l2
|
||||||
// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev
|
// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev
|
||||||
type camera struct {
|
type camera struct {
|
||||||
@@ -69,10 +77,23 @@ type camera struct {
|
|||||||
started bool
|
started bool
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
cancel func()
|
cancel func()
|
||||||
|
prevFrameTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers camera devices. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
|
// Clear all registered camera devices to prevent duplicates.
|
||||||
|
// If first initalize call, this will be a noop.
|
||||||
|
manager := driver.GetManager()
|
||||||
|
for _, d := range manager.Query(driver.FilterVideoRecorder()) {
|
||||||
|
manager.Delete(d.ID())
|
||||||
|
}
|
||||||
discovered := make(map[string]struct{})
|
discovered := make(map[string]struct{})
|
||||||
|
discover(discovered, "/dev/v4l/by-id/*")
|
||||||
discover(discovered, "/dev/v4l/by-path/*")
|
discover(discovered, "/dev/v4l/by-path/*")
|
||||||
discover(discovered, "/dev/video*")
|
discover(discovered, "/dev/video*")
|
||||||
}
|
}
|
||||||
@@ -101,7 +122,22 @@ func discover(discovered map[string]struct{}, pattern string) {
|
|||||||
if reallink == prioritizedDevice {
|
if reallink == prioritizedDevice {
|
||||||
priority = driver.PriorityHigh
|
priority = driver.PriorityHigh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var name, busInfo string
|
||||||
|
if webcamCam, err := webcam.Open(cam.path); err == nil {
|
||||||
|
defer webcamCam.Close()
|
||||||
|
name, _ = webcamCam.GetName()
|
||||||
|
busInfo, _ = webcamCam.GetBusInfo()
|
||||||
|
}
|
||||||
|
|
||||||
driver.GetManager().Register(cam, driver.Info{
|
driver.GetManager().Register(cam, driver.Info{
|
||||||
|
// Source: https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/vidioc-querycap.html
|
||||||
|
// Name of the device, a NUL-terminated UTF-8 string. For example: “Yoyodyne TV/FM”. One driver may support
|
||||||
|
// different brands or models of video hardware. This information is intended for users, for example in a
|
||||||
|
// menu of available devices. Since multiple TV cards of the same brand may be installed which are
|
||||||
|
// supported by the same driver, this name should be combined with the character device file name
|
||||||
|
// (e.g. /dev/video2) or the bus_info string to avoid ambiguities.
|
||||||
|
Name: name + LabelSeparator + busInfo,
|
||||||
Label: label + LabelSeparator + reallink,
|
Label: label + LabelSeparator + reallink,
|
||||||
DeviceType: driver.Camera,
|
DeviceType: driver.Camera,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
@@ -133,14 +169,32 @@ func newCamera(path string) *camera {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCameraReadTimeout() uint32 {
|
||||||
|
// default to 5 seconds
|
||||||
|
var readTimeoutSec uint32 = 5
|
||||||
|
if val, ok := os.LookupEnv("PION_MEDIADEVICES_CAMERA_READ_TIMEOUT"); ok {
|
||||||
|
if valInt, err := strconv.Atoi(val); err == nil {
|
||||||
|
if valInt > 0 {
|
||||||
|
readTimeoutSec = uint32(valInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readTimeoutSec
|
||||||
|
}
|
||||||
|
|
||||||
func (c *camera) Open() error {
|
func (c *camera) Open() error {
|
||||||
cam, err := webcam.Open(c.path)
|
cam, err := webcam.Open(c.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Late frames should be discarded. Buffering should be handled in higher level.
|
// Buffering should be handled in higher level.
|
||||||
cam.SetBufferCount(1)
|
err = cam.SetBufferCount(bufCount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.prevFrameTime = time.Now()
|
||||||
c.cam = cam
|
c.cam = cam
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -179,12 +233,21 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.FrameRate > 0 {
|
||||||
|
err = c.cam.SetFramerate(float32(p.FrameRate))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.cam.StartStreaming(); err != nil {
|
if err := c.cam.StartStreaming(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cam := c.cam
|
cam := c.cam
|
||||||
|
|
||||||
|
readTimeoutSec := getCameraReadTimeout()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
c.cancel = cancel
|
c.cancel = cancel
|
||||||
var buf []byte
|
var buf []byte
|
||||||
@@ -200,7 +263,14 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
|||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cam.WaitForFrame(5) // 5 seconds
|
if p.DiscardFramesOlderThan != 0 && time.Now().Sub(c.prevFrameTime) >= p.DiscardFramesOlderThan {
|
||||||
|
for i := 0; i < bufCount; i++ {
|
||||||
|
_ = cam.WaitForFrame(readTimeoutSec)
|
||||||
|
_, _ = cam.ReadFrame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cam.WaitForFrame(readTimeoutSec)
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
case *webcam.Timeout:
|
case *webcam.Timeout:
|
||||||
@@ -216,6 +286,10 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
|||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.DiscardFramesOlderThan != 0 {
|
||||||
|
c.prevFrameTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
// Frame is empty.
|
// Frame is empty.
|
||||||
// Retry reading and return errEmptyFrame if it exceeds maxEmptyFrameCount.
|
// Retry reading and return errEmptyFrame if it exceeds maxEmptyFrameCount.
|
||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
@@ -249,13 +323,31 @@ func (c *camera) Properties() []prop.Media {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if frameSize.StepWidth == 0 || frameSize.StepHeight == 0 {
|
if frameSize.StepWidth == 0 || frameSize.StepHeight == 0 {
|
||||||
properties = append(properties, prop.Media{
|
framerates := c.cam.GetSupportedFramerates(format, uint32(frameSize.MaxWidth), uint32(frameSize.MaxHeight))
|
||||||
Video: prop.Video{
|
// If the camera doesn't support framerate, we just add the resolution and format
|
||||||
Width: int(frameSize.MaxWidth),
|
if len(framerates) == 0 {
|
||||||
Height: int(frameSize.MaxHeight),
|
properties = append(properties, prop.Media{
|
||||||
FrameFormat: supportedFormat,
|
Video: prop.Video{
|
||||||
},
|
Width: int(frameSize.MaxWidth),
|
||||||
})
|
Height: int(frameSize.MaxHeight),
|
||||||
|
FrameFormat: supportedFormat,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, framerate := range framerates {
|
||||||
|
for _, fps := range enumFramerate(framerate) {
|
||||||
|
properties = append(properties, prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: int(frameSize.MaxWidth),
|
||||||
|
Height: int(frameSize.MaxHeight),
|
||||||
|
FrameFormat: supportedFormat,
|
||||||
|
FrameRate: fps,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// FIXME: we should probably use a custom data structure to capture all of the supported resolutions
|
// FIXME: we should probably use a custom data structure to capture all of the supported resolutions
|
||||||
for _, supportedResolution := range supportedResolutions {
|
for _, supportedResolution := range supportedResolutions {
|
||||||
@@ -274,16 +366,100 @@ func (c *camera) Properties() []prop.Media {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
properties = append(properties, prop.Media{
|
framerates := c.cam.GetSupportedFramerates(format, uint32(width), uint32(height))
|
||||||
Video: prop.Video{
|
if len(framerates) == 0 {
|
||||||
Width: width,
|
properties = append(properties, prop.Media{
|
||||||
Height: height,
|
Video: prop.Video{
|
||||||
FrameFormat: supportedFormat,
|
Width: width,
|
||||||
},
|
Height: height,
|
||||||
})
|
FrameFormat: supportedFormat,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, framerate := range framerates {
|
||||||
|
for _, fps := range enumFramerate(framerate) {
|
||||||
|
properties = append(properties, prop.Media{
|
||||||
|
Video: prop.Video{
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
FrameFormat: supportedFormat,
|
||||||
|
FrameRate: fps,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *camera) IsAvailable() (bool, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// close the opened file descriptor as quickly as possible and in all cases, including panics
|
||||||
|
func() {
|
||||||
|
var cam *webcam.Webcam
|
||||||
|
if cam, err = webcam.Open(c.path); err == nil {
|
||||||
|
defer cam.Close()
|
||||||
|
var index int32
|
||||||
|
// "Drivers must implement all the input ioctls when the device has one or more inputs..."
|
||||||
|
// Source: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/video.html?highlight=vidioc_enuminput
|
||||||
|
if index, err = cam.GetInput(); err == nil {
|
||||||
|
err = cam.SelectInput(uint32(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var errno syscall.Errno
|
||||||
|
errors.As(err, &errno)
|
||||||
|
|
||||||
|
// See https://man7.org/linux/man-pages/man3/errno.3.html
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return true, nil
|
||||||
|
case errno == syscall.EBUSY:
|
||||||
|
return false, availability.ErrBusy
|
||||||
|
case errno == syscall.ENODEV || errno == syscall.ENOENT:
|
||||||
|
return false, availability.ErrNoDevice
|
||||||
|
default:
|
||||||
|
return false, availability.NewError(errno.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enumFramerate returns a list of fps options from a FrameRate struct.
|
||||||
|
// discrete framerates will return a list of 1 fps element.
|
||||||
|
// stepwise framerates will return a list of all possible fps options.
|
||||||
|
func enumFramerate(framerate webcam.FrameRate) []float32 {
|
||||||
|
var framerates []float32
|
||||||
|
if framerate.StepNumerator == 0 && framerate.StepDenominator == 0 {
|
||||||
|
fr, err := calcFramerate(framerate.MaxNumerator, framerate.MaxDenominator)
|
||||||
|
if err != nil {
|
||||||
|
return framerates
|
||||||
|
}
|
||||||
|
framerates = append(framerates, fr)
|
||||||
|
} else {
|
||||||
|
for n := framerate.MinNumerator; n <= framerate.MaxNumerator; n += framerate.StepNumerator {
|
||||||
|
for d := framerate.MinDenominator; d <= framerate.MaxDenominator; d += framerate.StepDenominator {
|
||||||
|
fr, err := calcFramerate(n, d)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
framerates = append(framerates, fr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return framerates
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcFramerate turns fraction into a float32 fps value.
|
||||||
|
func calcFramerate(numerator uint32, denominator uint32) (float32, error) {
|
||||||
|
if denominator == 0 {
|
||||||
|
return 0, errors.New("framerate denominator is zero")
|
||||||
|
}
|
||||||
|
// round to three decimal places to avoid floating point precision issues
|
||||||
|
return float32(math.Round(1000.0/((float64(numerator))/float64(denominator))) / 1000), nil
|
||||||
|
}
|
||||||
|
@@ -57,6 +57,7 @@ func TestDiscover(t *testing.T) {
|
|||||||
drvs[0].Info().Label,
|
drvs[0].Info().Label,
|
||||||
drvs[1].Info().Label,
|
drvs[1].Info().Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returned drivers are unordered. Sort to get static result.
|
// Returned drivers are unordered. Sort to get static result.
|
||||||
sort.Sort(sort.StringSlice(labels))
|
sort.Sort(sort.StringSlice(labels))
|
||||||
|
|
||||||
@@ -70,3 +71,123 @@ func TestDiscover(t *testing.T) {
|
|||||||
t.Errorf("Expected label: %s, got: %s", expectedNoLink, label)
|
t.Errorf("Expected label: %s, got: %s", expectedNoLink, label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDiscoverByID(t *testing.T) {
|
||||||
|
const (
|
||||||
|
shortName = "id-unittest-video0"
|
||||||
|
shortName2 = "id-unittest-video1"
|
||||||
|
longName = "id-unittest-long-device-name:0:1:2:3"
|
||||||
|
)
|
||||||
|
|
||||||
|
dir, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
byIdDir := filepath.Join(dir, "v4l", "by-id")
|
||||||
|
if err := os.MkdirAll(byIdDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(dir, shortName), []byte{}, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(filepath.Join(dir, shortName2), []byte{}, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(
|
||||||
|
filepath.Join(dir, shortName),
|
||||||
|
filepath.Join(byIdDir, longName),
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
discovered := make(map[string]struct{})
|
||||||
|
discover(discovered, filepath.Join(byIdDir, "*"))
|
||||||
|
discover(discovered, filepath.Join(dir, "id-unittest-video*"))
|
||||||
|
|
||||||
|
drvs := driver.GetManager().Query(func(d driver.Driver) bool {
|
||||||
|
// Ignore real cameras.
|
||||||
|
return d.Info().DeviceType == driver.Camera && strings.Contains(d.Info().Label, "id-unittest")
|
||||||
|
})
|
||||||
|
if len(drvs) != 2 {
|
||||||
|
t.Fatalf("Expected 2 driver, got %d drivers", len(drvs))
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := []string{
|
||||||
|
drvs[0].Info().Label,
|
||||||
|
drvs[1].Info().Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returned drivers are unordered. Sort to get static result.
|
||||||
|
sort.Sort(sort.StringSlice(labels))
|
||||||
|
|
||||||
|
expected := longName + LabelSeparator + shortName
|
||||||
|
if label := labels[0]; label != expected {
|
||||||
|
t.Errorf("Expected label: %s, got: %s", expected, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedNoLink := shortName2 + LabelSeparator + shortName2
|
||||||
|
if label := labels[1]; label != expectedNoLink {
|
||||||
|
t.Errorf("Expected label: %s, got: %s", expectedNoLink, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCameraReadTimeout(t *testing.T) {
|
||||||
|
var expected uint32 = 5
|
||||||
|
value := getCameraReadTimeout()
|
||||||
|
if value != expected {
|
||||||
|
t.Errorf("Expected: %d, got: %d", expected, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
envVarName := "PION_MEDIADEVICES_CAMERA_READ_TIMEOUT"
|
||||||
|
os.Setenv(envVarName, "text")
|
||||||
|
value = getCameraReadTimeout()
|
||||||
|
if value != expected {
|
||||||
|
t.Errorf("Expected: %d, got: %d", expected, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv(envVarName, "-1")
|
||||||
|
value = getCameraReadTimeout()
|
||||||
|
if value != expected {
|
||||||
|
t.Errorf("Expected: %d, got: %d", expected, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv(envVarName, "1")
|
||||||
|
expected = 1
|
||||||
|
value = getCameraReadTimeout()
|
||||||
|
if value != expected {
|
||||||
|
t.Errorf("Expected: %d, got: %d", expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcFramerate(t *testing.T) {
|
||||||
|
framerates := []struct {
|
||||||
|
numerator uint32
|
||||||
|
denominator uint32
|
||||||
|
expected float32
|
||||||
|
}{
|
||||||
|
{1, 10, 10.0},
|
||||||
|
{1, 15, 15.0},
|
||||||
|
{1, 30, 30.0},
|
||||||
|
{1, 60, 60.0},
|
||||||
|
{1, 120, 120.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, framerate := range framerates {
|
||||||
|
value, err := calcFramerate(framerate.numerator, framerate.denominator)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// make sure we do not have any rounding errors
|
||||||
|
if value != framerate.expected {
|
||||||
|
t.Errorf("Expected: %f, got: %f", framerate.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// divide by zero check
|
||||||
|
_, err := calcFramerate(1, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected divide by zero error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -8,6 +8,18 @@
|
|||||||
#include "camera_windows.hpp"
|
#include "camera_windows.hpp"
|
||||||
#include "_cgo_export.h"
|
#include "_cgo_export.h"
|
||||||
|
|
||||||
|
|
||||||
|
imageProp* getProp(camera* cam, int i)
|
||||||
|
{
|
||||||
|
return &cam->props[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
char* getName(cameraList* list, int i)
|
||||||
|
{
|
||||||
|
return list->name[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// printErr shows string representation of HRESULT.
|
// printErr shows string representation of HRESULT.
|
||||||
// This is for debugging.
|
// This is for debugging.
|
||||||
void printErr(HRESULT hr)
|
void printErr(HRESULT hr)
|
||||||
@@ -76,6 +88,7 @@ int listCamera(cameraList* list, const char** errstr)
|
|||||||
{
|
{
|
||||||
list->name[i] = getCameraName(moniker);
|
list->name[i] = getCameraName(moniker);
|
||||||
moniker->Release();
|
moniker->Release();
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,6 +32,11 @@ type camera struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers camera devices. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
C.CoInitializeEx(nil, C.COINIT_MULTITHREADED)
|
C.CoInitializeEx(nil, C.COINIT_MULTITHREADED)
|
||||||
|
|
||||||
var list C.cameraList
|
var list C.cameraList
|
||||||
|
@@ -40,15 +40,9 @@ int listResolution(camera* cam, const char** errstr);
|
|||||||
int listCamera(cameraList* list, const char** errstr);
|
int listCamera(cameraList* list, const char** errstr);
|
||||||
int freeCameraList(cameraList* list, const char** errstr);
|
int freeCameraList(cameraList* list, const char** errstr);
|
||||||
|
|
||||||
inline imageProp* getProp(camera* cam, int i)
|
imageProp* getProp(camera* cam, int i);
|
||||||
{
|
|
||||||
return &cam->props[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
inline char* getName(cameraList* list, int i)
|
char* getName(cameraList* list, int i);
|
||||||
{
|
|
||||||
return list->name[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package driver
|
package driver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -30,6 +31,7 @@ type Info struct {
|
|||||||
Label string
|
Label string
|
||||||
DeviceType DeviceType
|
DeviceType DeviceType
|
||||||
Priority Priority
|
Priority Priority
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Adapter interface {
|
type Adapter interface {
|
||||||
@@ -44,3 +46,14 @@ type Driver interface {
|
|||||||
Info() Info
|
Info() Info
|
||||||
Status() State
|
Status() State
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AvailabilityAdapter interface {
|
||||||
|
IsAvailable() (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAvailable(d Driver) (bool, error) {
|
||||||
|
if aa, ok := d.(AvailabilityAdapter); ok {
|
||||||
|
return aa.IsAvailable()
|
||||||
|
}
|
||||||
|
return false, availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package driver
|
package driver
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
// FilterFn is being used to decide if a driver should be included in the
|
// FilterFn is being used to decide if a driver should be included in the
|
||||||
// query result.
|
// query result.
|
||||||
type FilterFn func(Driver) bool
|
type FilterFn func(Driver) bool
|
||||||
@@ -55,6 +57,7 @@ func FilterNot(filter FilterFn) FilterFn {
|
|||||||
|
|
||||||
// Manager is a singleton to manage multiple drivers and their states
|
// Manager is a singleton to manage multiple drivers and their states
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
mu sync.Mutex
|
||||||
drivers map[string]Driver
|
drivers map[string]Driver
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +72,8 @@ func GetManager() *Manager {
|
|||||||
|
|
||||||
// Register registers adapter to be discoverable by Query
|
// Register registers adapter to be discoverable by Query
|
||||||
func (m *Manager) Register(a Adapter, info Info) error {
|
func (m *Manager) Register(a Adapter, info Info) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
d := wrapAdapter(a, info)
|
d := wrapAdapter(a, info)
|
||||||
m.drivers[d.ID()] = d
|
m.drivers[d.ID()] = d
|
||||||
return nil
|
return nil
|
||||||
@@ -76,6 +81,8 @@ func (m *Manager) Register(a Adapter, info Info) error {
|
|||||||
|
|
||||||
// Query queries by using f to filter drivers, and simply return the filtered results.
|
// Query queries by using f to filter drivers, and simply return the filtered results.
|
||||||
func (m *Manager) Query(f FilterFn) []Driver {
|
func (m *Manager) Query(f FilterFn) []Driver {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
results := make([]Driver, 0)
|
results := make([]Driver, 0)
|
||||||
for _, d := range m.drivers {
|
for _, d := range m.drivers {
|
||||||
if ok := f(d); ok {
|
if ok := f(d); ok {
|
||||||
@@ -85,3 +92,10 @@ func (m *Manager) Query(f FilterFn) []Driver {
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete deletes a driver from manager given its ID
|
||||||
|
func (m *Manager) Delete(id string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
delete(m.drivers, id)
|
||||||
|
}
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
package driver
|
package driver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func filterTrue(d Driver) bool {
|
func filterTrue(_ Driver) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func filterFalse(d Driver) bool {
|
func filterFalse(_ Driver) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,3 +44,71 @@ func TestFilterAnd(t *testing.T) {
|
|||||||
t.Error("FilterAnd(filterTrue, filterTrue, filterTrue)() must be true")
|
t.Error("FilterAnd(filterTrue, filterTrue, filterTrue)() must be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeVideoAdapter struct{}
|
||||||
|
|
||||||
|
func (a *fakeVideoAdapter) Open() error { return nil }
|
||||||
|
func (a *fakeVideoAdapter) Close() error { return nil }
|
||||||
|
func (a *fakeVideoAdapter) Properties() []prop.Media { return nil }
|
||||||
|
|
||||||
|
func (a *fakeVideoAdapter) VideoRecord(_ prop.Media) (r video.Reader, err error) { return nil, nil }
|
||||||
|
|
||||||
|
type fakeAudioAdapter struct{}
|
||||||
|
|
||||||
|
func (a *fakeAudioAdapter) Open() error { return nil }
|
||||||
|
func (a *fakeAudioAdapter) Close() error { return nil }
|
||||||
|
func (a *fakeAudioAdapter) Properties() []prop.Media { return nil }
|
||||||
|
|
||||||
|
func (a *fakeAudioAdapter) AudioRecord(_ prop.Media) (r audio.Reader, err error) { return nil, nil }
|
||||||
|
|
||||||
|
type fakeAdapter struct{}
|
||||||
|
|
||||||
|
func (a *fakeAdapter) Open() error { return nil }
|
||||||
|
func (a *fakeAdapter) Close() error { return nil }
|
||||||
|
func (a *fakeAdapter) Properties() []prop.Media { return nil }
|
||||||
|
|
||||||
|
func TestRegister(t *testing.T) {
|
||||||
|
m := GetManager()
|
||||||
|
|
||||||
|
va := &fakeVideoAdapter{}
|
||||||
|
err := m.Register(va, Info{})
|
||||||
|
assert.NoError(t, err, "cannot register video adapter")
|
||||||
|
assert.Equal(t, len(m.Query(filterTrue)), 1)
|
||||||
|
|
||||||
|
aa := &fakeAudioAdapter{}
|
||||||
|
err = m.Register(aa, Info{})
|
||||||
|
assert.NoError(t, err, "cannot register audio adapter")
|
||||||
|
assert.Equal(t, len(m.Query(filterTrue)), 2)
|
||||||
|
|
||||||
|
a := &fakeAdapter{}
|
||||||
|
assert.Panics(t, func() { m.Register(a, Info{}) }, "should not register adapter that is neither audio nor video")
|
||||||
|
assert.Equal(t, len(m.Query(filterTrue)), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterSync(t *testing.T) {
|
||||||
|
m := GetManager()
|
||||||
|
start := make(chan struct{})
|
||||||
|
race := func() {
|
||||||
|
<-start
|
||||||
|
assert.NoError(t, m.Register(&fakeVideoAdapter{}, Info{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
go race()
|
||||||
|
go race()
|
||||||
|
close(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuerySync(t *testing.T) {
|
||||||
|
m := GetManager()
|
||||||
|
start := make(chan struct{})
|
||||||
|
race := func() {
|
||||||
|
<-start
|
||||||
|
m.Query(filterTrue)
|
||||||
|
}
|
||||||
|
|
||||||
|
go race()
|
||||||
|
go race()
|
||||||
|
close(start)
|
||||||
|
// write while reading
|
||||||
|
assert.NoError(t, m.Register(&fakeVideoAdapter{}, Info{}))
|
||||||
|
}
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
package microphone
|
package microphone
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -32,10 +34,16 @@ var (
|
|||||||
|
|
||||||
type microphone struct {
|
type microphone struct {
|
||||||
malgo.DeviceInfo
|
malgo.DeviceInfo
|
||||||
chunkChan chan []byte
|
chunkChan chan []byte
|
||||||
|
deviceCloseFunc func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers active playback or capture devices. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
var err error
|
var err error
|
||||||
ctx, err = malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
|
ctx, err = malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
|
||||||
logger.Debugf("%v\n", message)
|
logger.Debugf("%v\n", message)
|
||||||
@@ -60,6 +68,7 @@ func init() {
|
|||||||
Label: device.ID.String(),
|
Label: device.ID.String(),
|
||||||
DeviceType: driver.Microphone,
|
DeviceType: driver.Microphone,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
|
Name: info.Name(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,9 +96,8 @@ func (m *microphone) Open() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *microphone) Close() error {
|
func (m *microphone) Close() error {
|
||||||
if m.chunkChan != nil {
|
if m.deviceCloseFunc != nil {
|
||||||
close(m.chunkChan)
|
m.deviceCloseFunc()
|
||||||
m.chunkChan = nil
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -111,6 +119,9 @@ func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
|||||||
config.PerformanceProfile = malgo.LowLatency
|
config.PerformanceProfile = malgo.LowLatency
|
||||||
config.Capture.Channels = uint32(inputProp.ChannelCount)
|
config.Capture.Channels = uint32(inputProp.ChannelCount)
|
||||||
config.SampleRate = uint32(inputProp.SampleRate)
|
config.SampleRate = uint32(inputProp.SampleRate)
|
||||||
|
config.PeriodSizeInMilliseconds = uint32(inputProp.Latency.Milliseconds())
|
||||||
|
//FIX: Turn on the microphone with the current device id
|
||||||
|
config.Capture.DeviceID = m.ID.Pointer()
|
||||||
if inputProp.SampleSize == 4 && inputProp.IsFloat {
|
if inputProp.SampleSize == 4 && inputProp.IsFloat {
|
||||||
config.Capture.Format = malgo.FormatF32
|
config.Capture.Format = malgo.FormatF32
|
||||||
} else if inputProp.SampleSize == 2 && !inputProp.IsFloat {
|
} else if inputProp.SampleSize == 2 && !inputProp.IsFloat {
|
||||||
@@ -119,26 +130,44 @@ func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
|||||||
return nil, errUnsupportedFormat
|
return nil, errUnsupportedFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||||
onRecvChunk := func(_, chunk []byte, framecount uint32) {
|
onRecvChunk := func(_, chunk []byte, framecount uint32) {
|
||||||
m.chunkChan <- chunk
|
select {
|
||||||
|
case <-cancelCtx.Done():
|
||||||
|
case m.chunkChan <- chunk:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
callbacks.Data = onRecvChunk
|
callbacks.Data = onRecvChunk
|
||||||
|
|
||||||
device, err := malgo.InitDevice(ctx.Context, config, callbacks)
|
device, err := malgo.InitDevice(ctx.Context, config, callbacks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = device.Start()
|
err = device.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var closeDeviceOnce sync.Once
|
||||||
|
m.deviceCloseFunc = func() {
|
||||||
|
closeDeviceOnce.Do(func() {
|
||||||
|
cancel() // Unblock onRecvChunk
|
||||||
|
device.Uninit()
|
||||||
|
|
||||||
|
if m.chunkChan != nil {
|
||||||
|
close(m.chunkChan)
|
||||||
|
m.chunkChan = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var reader audio.Reader = audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
var reader audio.Reader = audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||||
chunk, ok := <-m.chunkChan
|
chunk, ok := <-m.chunkChan
|
||||||
if !ok {
|
if !ok {
|
||||||
device.Stop()
|
m.deviceCloseFunc()
|
||||||
device.Uninit()
|
|
||||||
return nil, func() {}, io.EOF
|
return nil, func() {}, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,36 +197,39 @@ func (m *microphone) Properties() []prop.Media {
|
|||||||
isBigEndian = true
|
isBigEndian = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for ch := m.MinChannels; ch <= m.MaxChannels; ch++ {
|
for _, format := range m.Formats {
|
||||||
// FIXME: Currently support 48kHz only. We need to implement a resampler first.
|
// FIXME: Currently support 48kHz only. We need to implement a resampler first.
|
||||||
// for sampleRate := m.MinSampleRate; sampleRate <= m.MaxSampleRate; sampleRate += sampleRateStep {
|
// for sampleRate := m.MinSampleRate; sampleRate <= m.MaxSampleRate; sampleRate += sampleRateStep {
|
||||||
sampleRate := 48000
|
sampleRate := 48000
|
||||||
for i := 0; i < int(m.FormatCount); i++ {
|
supportedProp := prop.Media{
|
||||||
format := m.Formats[i]
|
Audio: prop.Audio{
|
||||||
|
ChannelCount: int(format.Channels),
|
||||||
supportedProp := prop.Media{
|
SampleRate: int(sampleRate),
|
||||||
Audio: prop.Audio{
|
IsBigEndian: isBigEndian,
|
||||||
ChannelCount: int(ch),
|
// miniaudio only supports interleaved at the moment
|
||||||
SampleRate: int(sampleRate),
|
IsInterleaved: true,
|
||||||
IsBigEndian: isBigEndian,
|
// FIXME: should change this to a less discrete value
|
||||||
// miniaudio only supports interleaved at the moment
|
Latency: time.Millisecond * 20,
|
||||||
IsInterleaved: true,
|
},
|
||||||
// FIXME: should change this to a less discrete value
|
|
||||||
Latency: time.Millisecond * 20,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
switch malgo.FormatType(format) {
|
|
||||||
case malgo.FormatF32:
|
|
||||||
supportedProp.SampleSize = 4
|
|
||||||
supportedProp.IsFloat = true
|
|
||||||
case malgo.FormatS16:
|
|
||||||
supportedProp.SampleSize = 2
|
|
||||||
supportedProp.IsFloat = false
|
|
||||||
}
|
|
||||||
|
|
||||||
supportedProps = append(supportedProps, supportedProp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
supportedFormat := true
|
||||||
|
switch malgo.FormatType(format.Format) {
|
||||||
|
case malgo.FormatF32:
|
||||||
|
supportedProp.SampleSize = 4
|
||||||
|
supportedProp.IsFloat = true
|
||||||
|
case malgo.FormatS16:
|
||||||
|
supportedProp.SampleSize = 2
|
||||||
|
supportedProp.IsFloat = false
|
||||||
|
default:
|
||||||
|
supportedFormat = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !supportedFormat {
|
||||||
|
logger.Warnf("format '%s' not supported", format.Format)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
supportedProps = append(supportedProps, supportedProp)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
return supportedProps
|
return supportedProps
|
||||||
|
@@ -20,6 +20,11 @@ type screen struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers active displays. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
activeDisplays := screenshot.NumActiveDisplays()
|
activeDisplays := screenshot.NumActiveDisplays()
|
||||||
for i := 0; i < activeDisplays; i++ {
|
for i := 0; i < activeDisplays; i++ {
|
||||||
priority := driver.PriorityNormal
|
priority := driver.PriorityNormal
|
||||||
|
@@ -22,6 +22,11 @@ func deviceID(num int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize finds and registers active displays. This is part of an experimental API.
|
||||||
|
func Initialize() {
|
||||||
dp, err := openDisplay()
|
dp, err := openDisplay()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No x11 display available.
|
// No x11 display available.
|
||||||
|
@@ -47,10 +47,6 @@ func (d *dummy) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) {
|
func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||||
if p.FrameRate == 0 {
|
|
||||||
p.FrameRate = 30
|
|
||||||
}
|
|
||||||
|
|
||||||
colors := [][3]byte{
|
colors := [][3]byte{
|
||||||
{235, 128, 128},
|
{235, 128, 128},
|
||||||
{210, 16, 146},
|
{210, 16, 146},
|
||||||
@@ -143,6 +139,7 @@ func (d dummy) Properties() []prop.Media {
|
|||||||
Width: 640,
|
Width: 640,
|
||||||
Height: 480,
|
Height: 480,
|
||||||
FrameFormat: frame.FormatYUYV,
|
FrameFormat: frame.FormatYUYV,
|
||||||
|
FrameRate: 30,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,13 @@
|
|||||||
go-vnc is a VNC library for Go, initially supporting VNC clients but
|
go-vnc is a VNC library for Go, initially supporting VNC clients but
|
||||||
with the goal of eventually implementing a VNC server.
|
with the goal of eventually implementing a VNC server.
|
||||||
|
|
||||||
This library implements [RFC 6143](http://tools.ietf.org/html/rfc6143).
|
This library implements [RFC 6143][rfc6143].
|
||||||
|
|
||||||
|
## RFCs
|
||||||
|
### Implemented
|
||||||
|
- **RFC **: [The Remote Framebuffer Protocol][rfc6143]
|
||||||
|
|
||||||
|
[rfc6143]: http://tools.ietf.org/html/rfc6143
|
||||||
|
|
||||||
## Usage & Installation
|
## Usage & Installation
|
||||||
|
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
// Package vnc implements a VNC client.
|
// Package vnc implements a VNC client.
|
||||||
//
|
//
|
||||||
// References:
|
// References:
|
||||||
// [PROTOCOL]: http://tools.ietf.org/html/rfc6143
|
//
|
||||||
|
// [PROTOCOL]: http://tools.ietf.org/html/rfc6143
|
||||||
package vnc
|
package vnc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -96,7 +97,7 @@ func (c *ClientConn) CutText(text string) error {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
// This is the fixed size data we'll send
|
// This is the fixed size data we'll send
|
||||||
fixedData := []interface{}{
|
fixedData := []any{
|
||||||
uint8(6),
|
uint8(6),
|
||||||
uint8(0),
|
uint8(0),
|
||||||
uint8(0),
|
uint8(0),
|
||||||
@@ -141,7 +142,7 @@ func (c *ClientConn) FramebufferUpdateRequest(incremental bool, x, y, width, hei
|
|||||||
incrementalByte = 1
|
incrementalByte = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
data := []interface{}{
|
data := []any{
|
||||||
uint8(3),
|
uint8(3),
|
||||||
incrementalByte,
|
incrementalByte,
|
||||||
x, y, width, height,
|
x, y, width, height,
|
||||||
@@ -172,7 +173,7 @@ func (c *ClientConn) KeyEvent(keysym uint32, down bool) error {
|
|||||||
downFlag = 1
|
downFlag = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
data := []interface{}{
|
data := []any{
|
||||||
uint8(4),
|
uint8(4),
|
||||||
downFlag,
|
downFlag,
|
||||||
uint8(0),
|
uint8(0),
|
||||||
@@ -199,7 +200,7 @@ func (c *ClientConn) KeyEvent(keysym uint32, down bool) error {
|
|||||||
func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
data := []interface{}{
|
data := []any{
|
||||||
uint8(5),
|
uint8(5),
|
||||||
uint8(mask),
|
uint8(mask),
|
||||||
x,
|
x,
|
||||||
@@ -225,7 +226,7 @@ func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
|||||||
//
|
//
|
||||||
// See RFC 6143 Section 7.5.2
|
// See RFC 6143 Section 7.5.2
|
||||||
func (c *ClientConn) SetEncodings(encs []Encoding) error {
|
func (c *ClientConn) SetEncodings(encs []Encoding) error {
|
||||||
data := make([]interface{}, 3+len(encs))
|
data := make([]any, 3+len(encs))
|
||||||
data[0] = uint8(2)
|
data[0] = uint8(2)
|
||||||
data[1] = uint8(0)
|
data[1] = uint8(0)
|
||||||
data[2] = uint16(len(encs))
|
data[2] = uint16(len(encs))
|
||||||
@@ -319,7 +320,7 @@ func (c *ClientConn) handshake() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Respond with the version we will support
|
// Respond with the version we will support
|
||||||
if maxMinor<8 {
|
if maxMinor < 8 {
|
||||||
if _, err = c.c.Write([]byte("RFB 003.003\n")); err != nil {
|
if _, err = c.c.Write([]byte("RFB 003.003\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -331,7 +332,7 @@ func (c *ClientConn) handshake() error {
|
|||||||
if numSecurityTypes == 0 {
|
if numSecurityTypes == 0 {
|
||||||
return fmt.Errorf("no security types: %s", c.readErrorReason())
|
return fmt.Errorf("no security types: %s", c.readErrorReason())
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
if _, err = c.c.Write([]byte("RFB 003.008\n")); err != nil {
|
if _, err = c.c.Write([]byte("RFB 003.008\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -18,13 +18,32 @@ type Encoding interface {
|
|||||||
// the proper data.
|
// the proper data.
|
||||||
Read(*ClientConn, *Rectangle, io.Reader) (Encoding, error)
|
Read(*ClientConn, *Rectangle, io.Reader) (Encoding, error)
|
||||||
}
|
}
|
||||||
|
type CursorEncoding struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*CursorEncoding) Type() int32 {
|
||||||
|
return -239
|
||||||
|
}
|
||||||
|
func (*CursorEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding, error) {
|
||||||
|
size := int(rect.Height) * int(rect.Width) * int(c.PixelFormat.BPP) / 8
|
||||||
|
pixelBytes := make([]uint8, size)
|
||||||
|
if _, err := io.ReadFull(r, pixelBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mask := ((int(rect.Width) + 7) / 8) * int(rect.Height)
|
||||||
|
maskBytes := make([]uint8, mask)
|
||||||
|
if _, err := io.ReadFull(r, maskBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &CursorEncoding{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RawEncoding is raw pixel data sent by the server.
|
// RawEncoding is raw pixel data sent by the server.
|
||||||
//
|
//
|
||||||
// See RFC 6143 Section 7.7.1
|
// See RFC 6143 Section 7.7.1
|
||||||
type RawEncoding struct {
|
type RawEncoding struct {
|
||||||
Colors []Color
|
Colors []Color
|
||||||
RawPixel []uint32 //RGBA
|
RawPixel []uint32 //RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*RawEncoding) Type() int32 {
|
func (*RawEncoding) Type() int32 {
|
||||||
@@ -32,6 +51,7 @@ func (*RawEncoding) Type() int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (*RawEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding, error) {
|
func (*RawEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding, error) {
|
||||||
|
//fmt.Println("RawEncoding")
|
||||||
bytesPerPixel := c.PixelFormat.BPP / 8
|
bytesPerPixel := c.PixelFormat.BPP / 8
|
||||||
pixelBytes := make([]uint8, bytesPerPixel)
|
pixelBytes := make([]uint8, bytesPerPixel)
|
||||||
|
|
||||||
@@ -41,7 +61,7 @@ func (*RawEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding,
|
|||||||
}
|
}
|
||||||
|
|
||||||
colors := make([]Color, int(rect.Height)*int(rect.Width))
|
colors := make([]Color, int(rect.Height)*int(rect.Width))
|
||||||
rawPixels:=make([]uint32,int(rect.Height)*int(rect.Width))
|
rawPixels := make([]uint32, int(rect.Height)*int(rect.Width))
|
||||||
for y := uint16(0); y < rect.Height; y++ {
|
for y := uint16(0); y < rect.Height; y++ {
|
||||||
for x := uint16(0); x < rect.Width; x++ {
|
for x := uint16(0); x < rect.Width; x++ {
|
||||||
if _, err := io.ReadFull(r, pixelBytes); err != nil {
|
if _, err := io.ReadFull(r, pixelBytes); err != nil {
|
||||||
@@ -62,24 +82,30 @@ func (*RawEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding,
|
|||||||
color.R = uint16((rawPixel >> c.PixelFormat.RedShift) & uint32(c.PixelFormat.RedMax))
|
color.R = uint16((rawPixel >> c.PixelFormat.RedShift) & uint32(c.PixelFormat.RedMax))
|
||||||
color.G = uint16((rawPixel >> c.PixelFormat.GreenShift) & uint32(c.PixelFormat.GreenMax))
|
color.G = uint16((rawPixel >> c.PixelFormat.GreenShift) & uint32(c.PixelFormat.GreenMax))
|
||||||
color.B = uint16((rawPixel >> c.PixelFormat.BlueShift) & uint32(c.PixelFormat.BlueMax))
|
color.B = uint16((rawPixel >> c.PixelFormat.BlueShift) & uint32(c.PixelFormat.BlueMax))
|
||||||
|
if c.PixelFormat.BPP == 16 {
|
||||||
|
color.B = color.B<<3 | color.B>>2
|
||||||
|
color.G = color.G<<2 | color.G>>2
|
||||||
|
color.R = color.R<<3 | color.R>>2
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
*color = c.ColorMap[rawPixel]
|
*color = c.ColorMap[rawPixel]
|
||||||
}
|
}
|
||||||
rawPixels[int(y)*int(rect.Width)+int(x)]=uint32(color.B)<<16 | uint32(color.G)<<8 | uint32(color.R)
|
rawPixels[int(y)*int(rect.Width)+int(x)] = uint32(0xff)<<24 | uint32(color.B)<<16 | uint32(color.G)<<8 | uint32(color.R)
|
||||||
//fmt.Printf("%x %x",rawPixel,rawPixels[int(y)*int(rect.Width)+int(x)])
|
//fmt.Printf("%x %x",rawPixel,rawPixels[int(y)*int(rect.Width)+int(x)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &RawEncoding{colors,rawPixels}, nil
|
return &RawEncoding{colors, rawPixels}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZlibEncoding is raw pixel data sent by the server compressed by Zlib.
|
// ZlibEncoding is raw pixel data sent by the server compressed by Zlib.
|
||||||
//
|
//
|
||||||
// A single Zlib stream is created. There is only a single header for a framebuffer request response.
|
// A single Zlib stream is created. There is only a single header for a framebuffer request response.
|
||||||
type ZlibEncoding struct {
|
type ZlibEncoding struct {
|
||||||
Colors []Color
|
Colors []Color
|
||||||
RawPixel[] uint32
|
RawPixel []uint32
|
||||||
ZStream *bytes.Buffer
|
ZStream *bytes.Buffer
|
||||||
ZReader io.ReadCloser
|
ZReader io.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ZlibEncoding) Type() int32 {
|
func (*ZlibEncoding) Type() int32 {
|
||||||
@@ -87,6 +113,7 @@ func (*ZlibEncoding) Type() int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ze *ZlibEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding, error) {
|
func (ze *ZlibEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encoding, error) {
|
||||||
|
//fmt.Println("ZlibEncoding")
|
||||||
bytesPerPixel := c.PixelFormat.BPP / 8
|
bytesPerPixel := c.PixelFormat.BPP / 8
|
||||||
pixelBytes := make([]uint8, bytesPerPixel)
|
pixelBytes := make([]uint8, bytesPerPixel)
|
||||||
|
|
||||||
@@ -98,7 +125,6 @@ func (ze *ZlibEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encod
|
|||||||
// Format
|
// Format
|
||||||
// 4 bytes | uint32 | length
|
// 4 bytes | uint32 | length
|
||||||
// 'length' bytes | []byte | zlibData
|
// 'length' bytes | []byte | zlibData
|
||||||
|
|
||||||
// Read zlib length
|
// Read zlib length
|
||||||
var zipLength uint32
|
var zipLength uint32
|
||||||
err := binary.Read(r, binary.BigEndian, &zipLength)
|
err := binary.Read(r, binary.BigEndian, &zipLength)
|
||||||
@@ -146,7 +172,7 @@ func (ze *ZlibEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encod
|
|||||||
colorReader := bytes.NewReader(colorBytes)
|
colorReader := bytes.NewReader(colorBytes)
|
||||||
|
|
||||||
colors := make([]Color, int(rect.Height)*int(rect.Width))
|
colors := make([]Color, int(rect.Height)*int(rect.Width))
|
||||||
rawPixels:=make([]uint32,int(rect.Height)*int(rect.Width))
|
rawPixels := make([]uint32, int(rect.Height)*int(rect.Width))
|
||||||
for y := uint16(0); y < rect.Height; y++ {
|
for y := uint16(0); y < rect.Height; y++ {
|
||||||
for x := uint16(0); x < rect.Width; x++ {
|
for x := uint16(0); x < rect.Width; x++ {
|
||||||
if _, err := io.ReadFull(colorReader, pixelBytes); err != nil {
|
if _, err := io.ReadFull(colorReader, pixelBytes); err != nil {
|
||||||
@@ -167,14 +193,19 @@ func (ze *ZlibEncoding) Read(c *ClientConn, rect *Rectangle, r io.Reader) (Encod
|
|||||||
color.R = uint16((rawPixel >> c.PixelFormat.RedShift) & uint32(c.PixelFormat.RedMax))
|
color.R = uint16((rawPixel >> c.PixelFormat.RedShift) & uint32(c.PixelFormat.RedMax))
|
||||||
color.G = uint16((rawPixel >> c.PixelFormat.GreenShift) & uint32(c.PixelFormat.GreenMax))
|
color.G = uint16((rawPixel >> c.PixelFormat.GreenShift) & uint32(c.PixelFormat.GreenMax))
|
||||||
color.B = uint16((rawPixel >> c.PixelFormat.BlueShift) & uint32(c.PixelFormat.BlueMax))
|
color.B = uint16((rawPixel >> c.PixelFormat.BlueShift) & uint32(c.PixelFormat.BlueMax))
|
||||||
|
if c.PixelFormat.BPP == 16 {
|
||||||
|
color.B = color.B<<3 | color.B>>2
|
||||||
|
color.G = color.G<<2 | color.G>>2
|
||||||
|
color.R = color.R<<3 | color.R>>2
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
*color = c.ColorMap[rawPixel]
|
*color = c.ColorMap[rawPixel]
|
||||||
}
|
}
|
||||||
rawPixels[int(y)*int(rect.Width)+int(x)]=uint32(color.B)<<16 | uint32(color.G)<<8 | uint32(color.R)
|
rawPixels[int(y)*int(rect.Width)+int(x)] = uint32(0xff)<<24 | uint32(color.B)<<16 | uint32(color.G)<<8 | uint32(color.R)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ZlibEncoding{Colors: colors,RawPixel: rawPixels}, nil
|
return &ZlibEncoding{Colors: colors, RawPixel: rawPixels}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ze *ZlibEncoding) Close() {
|
func (ze *ZlibEncoding) Close() {
|
||||||
|
@@ -63,7 +63,7 @@ func (*FramebufferUpdateMessage) Read(c *ClientConn, r io.Reader) (ServerMessage
|
|||||||
var encodingType int32
|
var encodingType int32
|
||||||
|
|
||||||
rect := &rects[i]
|
rect := &rects[i]
|
||||||
data := []interface{}{
|
data := []any{
|
||||||
&rect.X,
|
&rect.X,
|
||||||
&rect.Y,
|
&rect.Y,
|
||||||
&rect.Width,
|
&rect.Width,
|
||||||
@@ -128,7 +128,7 @@ func (*SetColorMapEntriesMessage) Read(c *ClientConn, r io.Reader) (ServerMessag
|
|||||||
for i := uint16(0); i < numColors; i++ {
|
for i := uint16(0); i < numColors; i++ {
|
||||||
|
|
||||||
color := &result.Colors[i]
|
color := &result.Colors[i]
|
||||||
data := []interface{}{
|
data := []any{
|
||||||
&color.R,
|
&color.R,
|
||||||
&color.G,
|
&color.G,
|
||||||
&color.B,
|
&color.B,
|
||||||
|
@@ -5,13 +5,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pion/mediadevices/pkg/driver/vncdriver/vnc"
|
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/vncdriver/vnc"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -32,12 +33,12 @@ func NewVnc(vncAddr string) *vncDevice {
|
|||||||
return &vncDevice{vncAddr: vncAddr}
|
return &vncDevice{vncAddr: vncAddr}
|
||||||
}
|
}
|
||||||
func (d *vncDevice) PointerEvent(mask uint8, x, y uint16) {
|
func (d *vncDevice) PointerEvent(mask uint8, x, y uint16) {
|
||||||
if d.vClient!=nil{
|
if d.vClient != nil {
|
||||||
d.vClient.PointerEvent(vnc.ButtonMask(mask), x, y)
|
d.vClient.PointerEvent(vnc.ButtonMask(mask), x, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (d *vncDevice) KeyEvent(keysym uint32, down bool) {
|
func (d *vncDevice) KeyEvent(keysym uint32, down bool) {
|
||||||
if d.vClient!=nil {
|
if d.vClient != nil {
|
||||||
d.vClient.KeyEvent(keysym, down)
|
d.vClient.KeyEvent(keysym, down)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,10 @@ func (d *vncDevice) Open() error {
|
|||||||
d.closed = ctx.Done()
|
d.closed = ctx.Done()
|
||||||
d.cancel = cancel
|
d.cancel = cancel
|
||||||
msg := make(chan vnc.ServerMessage, 1)
|
msg := make(chan vnc.ServerMessage, 1)
|
||||||
|
//auth:=new(vnc.PasswordAuth)
|
||||||
|
//auth.Password="####"
|
||||||
conf := vnc.ClientConfig{
|
conf := vnc.ClientConfig{
|
||||||
|
//Auth: []vnc.ClientAuth{auth},
|
||||||
ServerMessageCh: msg,
|
ServerMessageCh: msg,
|
||||||
Exclusive: false,
|
Exclusive: false,
|
||||||
}
|
}
|
||||||
@@ -66,6 +70,7 @@ func (d *vncDevice) Open() error {
|
|||||||
d.vClient.SetEncodings([]vnc.Encoding{
|
d.vClient.SetEncodings([]vnc.Encoding{
|
||||||
&vnc.ZlibEncoding{},
|
&vnc.ZlibEncoding{},
|
||||||
&vnc.RawEncoding{},
|
&vnc.RawEncoding{},
|
||||||
|
&vnc.CursorEncoding{},
|
||||||
})
|
})
|
||||||
d.w = int(d.vClient.FrameBufferWidth)
|
d.w = int(d.vClient.FrameBufferWidth)
|
||||||
d.h = int(d.vClient.FrameBufferHeight)
|
d.h = int(d.vClient.FrameBufferHeight)
|
||||||
@@ -89,6 +94,9 @@ func (d *vncDevice) Open() error {
|
|||||||
for _, rect := range t.Rectangles {
|
for _, rect := range t.Rectangles {
|
||||||
var pix []uint32
|
var pix []uint32
|
||||||
switch t := rect.Enc.(type) {
|
switch t := rect.Enc.(type) {
|
||||||
|
case *vnc.CursorEncoding:
|
||||||
|
//ignore remote cursor messages
|
||||||
|
continue
|
||||||
case *vnc.RawEncoding:
|
case *vnc.RawEncoding:
|
||||||
pix = t.RawPixel
|
pix = t.RawPixel
|
||||||
case *vnc.ZlibEncoding:
|
case *vnc.ZlibEncoding:
|
||||||
@@ -97,19 +105,16 @@ func (d *vncDevice) Open() error {
|
|||||||
for y := int(rect.Y); y < int(rect.Height+rect.Y); y++ {
|
for y := int(rect.Y); y < int(rect.Height+rect.Y); y++ {
|
||||||
for x := int(rect.X); x < int(rect.Width+rect.X); x++ {
|
for x := int(rect.X); x < int(rect.Width+rect.X); x++ {
|
||||||
binary.LittleEndian.PutUint32(d.rawPixel[(y*d.w+x)*4:], pix[(y-int(rect.Y))*int(rect.Width)+(x-int(rect.X))])
|
binary.LittleEndian.PutUint32(d.rawPixel[(y*d.w+x)*4:], pix[(y-int(rect.Y))*int(rect.Width)+(x-int(rect.X))])
|
||||||
//BigEndian
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//time.Sleep(33 * time.Millisecond)
|
|
||||||
d.vClient.FramebufferUpdateRequest(true, 0, 0, uint16(d.w), uint16(d.h))
|
d.vClient.FramebufferUpdateRequest(true, 0, 0, uint16(d.w), uint16(d.h))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
|
||||||
}
|
}
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(10 * time.Second):
|
||||||
//fmt.Println("Timeout FramebufferUpdate")
|
|
||||||
if d.vClient.FramebufferUpdateRequest(true, 0, 0, uint16(d.w), uint16(d.h)) != nil {
|
if d.vClient.FramebufferUpdateRequest(true, 0, 0, uint16(d.w), uint16(d.h)) != nil {
|
||||||
d.cancel()
|
d.cancel()
|
||||||
return
|
return
|
||||||
@@ -137,13 +142,12 @@ func (d *vncDevice) Close() error {
|
|||||||
|
|
||||||
func (d *vncDevice) VideoRecord(p prop.Media) (video.Reader, error) {
|
func (d *vncDevice) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||||
if p.FrameRate == 0 {
|
if p.FrameRate == 0 {
|
||||||
p.FrameRate = 15
|
p.FrameRate = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
tick := time.NewTicker(time.Duration(float32(time.Second) / p.FrameRate))
|
tick := time.NewTicker(time.Duration(float32(time.Second) / p.FrameRate))
|
||||||
d.tick = tick
|
d.tick = tick
|
||||||
closed := d.closed
|
closed := d.closed
|
||||||
pixs := make([]byte, d.h*d.w*4)
|
|
||||||
r := video.ReaderFunc(func() (image.Image, func(), error) {
|
r := video.ReaderFunc(func() (image.Image, func(), error) {
|
||||||
select {
|
select {
|
||||||
case <-closed:
|
case <-closed:
|
||||||
@@ -153,9 +157,8 @@ func (d *vncDevice) VideoRecord(p prop.Media) (video.Reader, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<-tick.C
|
<-tick.C
|
||||||
copy(pixs, d.rawPixel)
|
|
||||||
return &image.RGBA{
|
return &image.RGBA{
|
||||||
Pix: pixs,
|
Pix: d.rawPixel,
|
||||||
Stride: 4,
|
Stride: 4,
|
||||||
Rect: image.Rect(0, 0, d.w, d.h),
|
Rect: image.Rect(0, 0, d.w, d.h),
|
||||||
}, func() {}, nil
|
}, func() {}, nil
|
||||||
|
@@ -2,6 +2,7 @@ package driver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -21,6 +22,10 @@ func wrapAdapter(a Adapter, info Info) Driver {
|
|||||||
state: StateClosed,
|
state: StateClosed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aa, ok := a.(AvailabilityAdapter); ok {
|
||||||
|
d.isAvailable = aa.IsAvailable
|
||||||
|
}
|
||||||
|
|
||||||
switch v := a.(type) {
|
switch v := a.(type) {
|
||||||
case VideoRecorder:
|
case VideoRecorder:
|
||||||
// Only expose Driver and VideoRecorder interfaces
|
// Only expose Driver and VideoRecorder interfaces
|
||||||
@@ -28,7 +33,8 @@ func wrapAdapter(a Adapter, info Info) Driver {
|
|||||||
r := &struct {
|
r := &struct {
|
||||||
Driver
|
Driver
|
||||||
VideoRecorder
|
VideoRecorder
|
||||||
}{d, d}
|
AvailabilityAdapter
|
||||||
|
}{d, d, d}
|
||||||
return r
|
return r
|
||||||
case AudioRecorder:
|
case AudioRecorder:
|
||||||
// Only expose Driver and AudioRecorder interfaces
|
// Only expose Driver and AudioRecorder interfaces
|
||||||
@@ -36,7 +42,8 @@ func wrapAdapter(a Adapter, info Info) Driver {
|
|||||||
return &struct {
|
return &struct {
|
||||||
Driver
|
Driver
|
||||||
AudioRecorder
|
AudioRecorder
|
||||||
}{d, d}
|
AvailabilityAdapter
|
||||||
|
}{d, d, d}
|
||||||
default:
|
default:
|
||||||
panic("adapter has to be either VideoRecorder/AudioRecorder")
|
panic("adapter has to be either VideoRecorder/AudioRecorder")
|
||||||
}
|
}
|
||||||
@@ -46,9 +53,10 @@ type adapterWrapper struct {
|
|||||||
Adapter
|
Adapter
|
||||||
VideoRecorder
|
VideoRecorder
|
||||||
AudioRecorder
|
AudioRecorder
|
||||||
id string
|
id string
|
||||||
info Info
|
info Info
|
||||||
state State
|
state State
|
||||||
|
isAvailable func() (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *adapterWrapper) ID() string {
|
func (w *adapterWrapper) ID() string {
|
||||||
@@ -104,3 +112,10 @@ func (w *adapterWrapper) AudioRecord(p prop.Media) (r audio.Reader, err error) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *adapterWrapper) IsAvailable() (bool, error) {
|
||||||
|
if w.isAvailable == nil {
|
||||||
|
return false, availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
return w.isAvailable()
|
||||||
|
}
|
||||||
|
@@ -39,6 +39,10 @@ func (a *audioAdapterBrokenMock) AudioRecord(p prop.Media) (r audio.Reader, err
|
|||||||
return nil, recordErr
|
return nil, recordErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type availabilityAdapterMock struct{ videoAdapterMock }
|
||||||
|
|
||||||
|
func (a *availabilityAdapterMock) IsAvailable() (bool, error) { return true, nil }
|
||||||
|
|
||||||
func TestVideoWrapperState(t *testing.T) {
|
func TestVideoWrapperState(t *testing.T) {
|
||||||
var a videoAdapterMock
|
var a videoAdapterMock
|
||||||
d := wrapAdapter(&a, Info{})
|
d := wrapAdapter(&a, Info{})
|
||||||
@@ -136,3 +140,38 @@ func TestAudioWrapperWithBrokenRecorderState(t *testing.T) {
|
|||||||
t.Errorf("expected the status to be %v, but got %v", StateClosed, d.Status())
|
t.Errorf("expected the status to be %v, but got %v", StateClosed, d.Status())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWrapperAvailabilityAdapter(t *testing.T) {
|
||||||
|
var aa availabilityAdapterMock
|
||||||
|
d := wrapAdapter(&aa, Info{})
|
||||||
|
|
||||||
|
ok, err := IsAvailable(d)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected nil, but got %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected true, but got %v", ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
var v videoAdapterMock
|
||||||
|
d = wrapAdapter(&v, Info{})
|
||||||
|
|
||||||
|
ok, err = IsAvailable(d)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected err, but got %v", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Errorf("expected false, but got %v", ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
var a audioAdapterMock
|
||||||
|
d = wrapAdapter(&a, Info{})
|
||||||
|
|
||||||
|
ok, err = IsAvailable(d)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected err, but got %v", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Errorf("expected false, but got %v", ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,11 +2,86 @@ package frame
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Thank you to https://github.com/filiptc/gorbit/blob/fa87ff39b68a6706306f34c318e0b9a5a3c97110/image/overlay.go#L37-L40
|
||||||
|
for addMotionDht, dhtMarker, dht, and sosMarker. These are protected under the following license:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Philip Thomas Casado
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
dhtMarker = []byte{255, 196}
|
||||||
|
dht = []byte{1, 162, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125, 1, 2, 3, 0, 4, 17, 5, 18, 33, 49, 65, 6, 19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193, 21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, 37, 38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 17, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 119, 0, 1, 2, 3, 17, 4, 5, 33, 49, 6, 18, 65, 81, 7, 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177, 193, 9, 35, 51, 82, 240, 21, 98, 114, 209, 10, 22, 36, 52, 225, 37, 241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 130, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, 242, 243, 244, 245, 246, 247, 248, 249, 250}
|
||||||
|
sosMarker = []byte{255, 218}
|
||||||
|
huffmanTableInfoLength = len(dhtMarker) + len(dht) + len(sosMarker)
|
||||||
|
uninitializedHuffmanTableError jpeg.FormatError = jpeg.FormatError("uninitialized Huffman table")
|
||||||
|
)
|
||||||
|
|
||||||
func decodeMJPEG(frame []byte, width, height int) (image.Image, func(), error) {
|
func decodeMJPEG(frame []byte, width, height int) (image.Image, func(), error) {
|
||||||
img, err := jpeg.Decode(bytes.NewReader(frame))
|
img, err := jpeg.Decode(bytes.NewReader(frame))
|
||||||
|
if err == nil {
|
||||||
|
return img, func() {}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.As(err, &uninitializedHuffmanTableError) {
|
||||||
|
if err.Error() == uninitializedHuffmanTableError.Error() {
|
||||||
|
img, err = jpeg.Decode(bytes.NewReader(addMotionDht(frame)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return img, func() {}, err
|
return img, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addMotionDht(frame []byte) []byte {
|
||||||
|
jpegParts := bytes.Split(frame, sosMarker)
|
||||||
|
if len(jpegParts) != 2 {
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
correctedFrame := make([]byte, len(jpegParts[0])+huffmanTableInfoLength+len(jpegParts[1]))
|
||||||
|
correctedFrameOffset := 0
|
||||||
|
|
||||||
|
copy(correctedFrame[correctedFrameOffset:], jpegParts[0])
|
||||||
|
correctedFrameOffset += len(jpegParts[0])
|
||||||
|
|
||||||
|
copy(correctedFrame[correctedFrameOffset:], dhtMarker)
|
||||||
|
correctedFrameOffset += len(dhtMarker)
|
||||||
|
|
||||||
|
copy(correctedFrame[correctedFrameOffset:], dht)
|
||||||
|
correctedFrameOffset += len(dht)
|
||||||
|
|
||||||
|
copy(correctedFrame[correctedFrameOffset:], sosMarker)
|
||||||
|
correctedFrameOffset += len(sosMarker)
|
||||||
|
|
||||||
|
copy(correctedFrame[correctedFrameOffset:], jpegParts[1])
|
||||||
|
return correctedFrame
|
||||||
|
}
|
||||||
|
51
pkg/frame/compressed_test.go
Normal file
51
pkg/frame/compressed_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package frame
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image/jpeg"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddMotionDht(t *testing.T) {
|
||||||
|
uninitializedHuffmanTableFrame, err := jpeg.Decode(bytes.NewReader(UninitializedHuffmanTable))
|
||||||
|
|
||||||
|
// Decode fails with an uninitialized Huffman table error for sample input
|
||||||
|
expectedErrorMessage := "invalid JPEG format: uninitialized Huffman table"
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Fatalf("Wrong decode error result,\nexpected:\n%+v\ngot:\n%+v", expectedErrorMessage, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode passes after adding default Huffman table to
|
||||||
|
defaultHuffmanTableFrame, err := jpeg.Decode(bytes.NewReader(addMotionDht(UninitializedHuffmanTable)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected decode function to pass after adding default Huffman table. Failed with %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding default Huffman table to a valid frame without a Huffman table changes the table
|
||||||
|
if uninitializedHuffmanTableFrame == defaultHuffmanTableFrame {
|
||||||
|
t.Fatalf("Expected addMotionDht to update frame. Instead returned original frame")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that an improperly constructed frame does not get updated by addMotionDht
|
||||||
|
randomBytes := []byte{1, 2, 3, 4}
|
||||||
|
frame1, err := jpeg.Decode(bytes.NewReader(randomBytes))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected decode function to fail with random bytes but passed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
frame2, err := jpeg.Decode(bytes.NewReader(addMotionDht(randomBytes)))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected decode function to fail with random bytes but passed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame1 != frame2 {
|
||||||
|
t.Fatalf("addMotionDht updated the frame despite being improperly constructed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeMJPEG(t *testing.T) {
|
||||||
|
_, _, err := decodeMJPEG(UninitializedHuffmanTable, 640, 480)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected decode function to pass. Failed with %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
@@ -7,36 +7,38 @@ import (
|
|||||||
type Format string
|
type Format string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FormatI420 https://www.fourcc.org/pixel-format/yuv-i420/
|
// FormatI420 https://wiki.videolan.org/YUV#I420
|
||||||
FormatI420 Format = "I420"
|
FormatI420 Format = "I420"
|
||||||
// FormatI444 is a YUV format without sub-sampling
|
// FormatI444 is a YUV format without sub-sampling
|
||||||
FormatI444 Format = "I444"
|
FormatI444 Format = "I444"
|
||||||
// FormatNV21 https://www.fourcc.org/pixel-format/yuv-nv21/
|
// FormatNV21 https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-nv12.html
|
||||||
FormatNV21 = "NV21"
|
FormatNV21 = "NV21"
|
||||||
// FormatNV12 https://www.fourcc.org/pixel-format/yuv-nv12/
|
// FormatNV12 https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-nv12.html
|
||||||
FormatNV12 = "NV12"
|
FormatNV12 = "NV12"
|
||||||
// FormatYUY2 https://www.fourcc.org/pixel-format/yuv-yuy2/
|
// FormatYUY2 https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-yuyv.html
|
||||||
|
// YUY2 is what Windows calls YUYV
|
||||||
FormatYUY2 = "YUY2"
|
FormatYUY2 = "YUY2"
|
||||||
// FormatUYVY https://www.fourcc.org/pixel-format/yuv-uyvy/
|
// FormatYUYV https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-yuyv.html
|
||||||
|
FormatYUYV = "YUYV"
|
||||||
|
// FormatUYVY https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-uyvy.html
|
||||||
FormatUYVY = "UYVY"
|
FormatUYVY = "UYVY"
|
||||||
|
|
||||||
// FormatRGBA https://www.fourcc.org/pixel-format/rgb-rgba/
|
// FormatRGBA https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-rgb.html
|
||||||
FormatRGBA Format = "RGBA"
|
FormatRGBA Format = "RGBA"
|
||||||
|
|
||||||
// FormatMJPEG https://www.fourcc.org/mjpg/
|
// FormatMJPEG https://wiki.videolan.org/MJPEG
|
||||||
FormatMJPEG = "MJPEG"
|
FormatMJPEG = "MJPEG"
|
||||||
|
|
||||||
// FormatZ16 https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-z16.html
|
// FormatZ16 https://www.kernel.org/doc/html/v5.9/userspace-api/media/v4l/pixfmt-z16.html
|
||||||
FormatZ16 = "Z16"
|
FormatZ16 = "Z16"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FormatYUYV = FormatYUY2
|
|
||||||
|
|
||||||
var decoderMap = map[Format]decoderFunc{
|
var decoderMap = map[Format]decoderFunc{
|
||||||
FormatI420: decodeI420,
|
FormatI420: decodeI420,
|
||||||
FormatNV21: decodeNV21,
|
FormatNV21: decodeNV21,
|
||||||
FormatNV12: decodeNV12,
|
FormatNV12: decodeNV12,
|
||||||
FormatYUY2: decodeYUY2,
|
FormatYUY2: decodeYUY2,
|
||||||
|
FormatYUYV: decodeYUY2,
|
||||||
FormatUYVY: decodeUYVY,
|
FormatUYVY: decodeUYVY,
|
||||||
FormatMJPEG: decodeMJPEG,
|
FormatMJPEG: decodeMJPEG,
|
||||||
FormatZ16: decodeZ16,
|
FormatZ16: decodeZ16,
|
||||||
|
3
pkg/frame/sampleFrames.go
Normal file
3
pkg/frame/sampleFrames.go
Normal file
File diff suppressed because one or more lines are too long
@@ -27,7 +27,7 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
coreConfig = config.Core
|
coreConfig = config.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) {
|
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (any, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}), coreConfig)
|
}), coreConfig)
|
||||||
|
|
||||||
@@ -39,11 +39,11 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
||||||
copyFn := func(src interface{}) interface{} { return src }
|
copyFn := func(src any) any { return src }
|
||||||
|
|
||||||
if copyChunk {
|
if copyChunk {
|
||||||
buffer := wave.NewBuffer()
|
buffer := wave.NewBuffer()
|
||||||
copyFn = func(src interface{}) interface{} {
|
copyFn = func(src any) any {
|
||||||
realSrc, _ := src.(wave.Audio)
|
realSrc, _ := src.(wave.Audio)
|
||||||
buffer.StoreCopy(realSrc)
|
buffer.StoreCopy(realSrc)
|
||||||
return buffer.Load()
|
return buffer.Load()
|
||||||
@@ -60,7 +60,7 @@ func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
|||||||
|
|
||||||
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) {
|
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (any, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ const (
|
|||||||
var errEmptySource = fmt.Errorf("Source can't be nil")
|
var errEmptySource = fmt.Errorf("Source can't be nil")
|
||||||
|
|
||||||
type broadcasterData struct {
|
type broadcasterData struct {
|
||||||
data interface{}
|
data any
|
||||||
count uint32
|
count uint32
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -124,10 +124,10 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{}) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyFn func(any) any) Reader {
|
||||||
currentCount := broadcaster.buffer.lastCount()
|
currentCount := broadcaster.buffer.lastCount()
|
||||||
|
|
||||||
return ReaderFunc(func() (data interface{}, release func(), err error) {
|
return ReaderFunc(func() (data any, release func(), err error) {
|
||||||
currentCount++
|
currentCount++
|
||||||
if push := broadcaster.buffer.acquire(currentCount); push != nil {
|
if push := broadcaster.buffer.acquire(currentCount); push != nil {
|
||||||
data, _, err = broadcaster.source.Load().(Reader).Read()
|
data, _, err = broadcaster.source.Load().(Reader).Read()
|
||||||
@@ -141,7 +141,9 @@ func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{})
|
|||||||
data, err, currentCount = ringData.data, ringData.err, ringData.count
|
data, err, currentCount = ringData.data, ringData.err, ringData.count
|
||||||
}
|
}
|
||||||
|
|
||||||
data = copyFn(data)
|
if data != nil { // data is nil if an error occurred during reading
|
||||||
|
data = copyFn(data)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@ func TestBroadcast(t *testing.T) {
|
|||||||
frameCount := 0
|
frameCount := 0
|
||||||
frameSent := 0
|
frameSent := 0
|
||||||
lastSend := time.Now()
|
lastSend := time.Now()
|
||||||
src = ReaderFunc(func() (interface{}, func(), error) {
|
src = ReaderFunc(func() (any, func(), error) {
|
||||||
if pauseCond.src && frameSent == 30 {
|
if pauseCond.src && frameSent == 30 {
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func TestBroadcast(t *testing.T) {
|
|||||||
wg.Add(n)
|
wg.Add(n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
reader := broadcaster.NewReader(func(src interface{}) interface{} { return src })
|
reader := broadcaster.NewReader(func(src any) any { return src })
|
||||||
count := 0
|
count := 0
|
||||||
lastFrameCount := -1
|
lastFrameCount := -1
|
||||||
droppedFrames := 0
|
droppedFrames := 0
|
||||||
|
@@ -11,13 +11,13 @@ type Reader interface {
|
|||||||
// there will be new allocations during streaming, and old unused memory will become garbage. As a consequence,
|
// there will be new allocations during streaming, and old unused memory will become garbage. As a consequence,
|
||||||
// these garbage will put a lot of pressure to the garbage collector and makes it to run more often and finish
|
// these garbage will put a lot of pressure to the garbage collector and makes it to run more often and finish
|
||||||
// slower as the heap memory usage increases and more garbage to collect.
|
// slower as the heap memory usage increases and more garbage to collect.
|
||||||
Read() (data interface{}, release func(), err error)
|
Read() (data any, release func(), err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReaderFunc is a proxy type for Reader
|
// ReaderFunc is a proxy type for Reader
|
||||||
type ReaderFunc func() (data interface{}, release func(), err error)
|
type ReaderFunc func() (data any, release func(), err error)
|
||||||
|
|
||||||
func (f ReaderFunc) Read() (data interface{}, release func(), err error) {
|
func (f ReaderFunc) Read() (data any, release func(), err error) {
|
||||||
data, release, err = f()
|
data, release, err = f()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
coreConfig = config.Core
|
coreConfig = config.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) {
|
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (any, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}), coreConfig)
|
}), coreConfig)
|
||||||
|
|
||||||
@@ -39,11 +39,11 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
||||||
copyFn := func(src interface{}) interface{} { return src }
|
copyFn := func(src any) any { return src }
|
||||||
|
|
||||||
if copyFrame {
|
if copyFrame {
|
||||||
buffer := NewFrameBuffer(0)
|
buffer := NewFrameBuffer(0)
|
||||||
copyFn = func(src interface{}) interface{} {
|
copyFn = func(src any) any {
|
||||||
realSrc, _ := src.(image.Image)
|
realSrc, _ := src.(image.Image)
|
||||||
buffer.StoreCopy(realSrc)
|
buffer.StoreCopy(realSrc)
|
||||||
return buffer.Load()
|
return buffer.Load()
|
||||||
@@ -60,7 +60,7 @@ func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
|||||||
|
|
||||||
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) {
|
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (any, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package video
|
package video
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -47,3 +48,21 @@ func TestBroadcast(t *testing.T) {
|
|||||||
t.Fatal("Expected actual frame without copy to be the same with the original")
|
t.Fatal("Expected actual frame without copy to be the same with the original")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBroadcastWithCopyOnReadError(t *testing.T) {
|
||||||
|
expectedError := errors.New("expected error")
|
||||||
|
source := ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
return nil, func() {}, expectedError
|
||||||
|
})
|
||||||
|
|
||||||
|
broadcaster := NewBroadcaster(source, nil)
|
||||||
|
readerWithCopy := broadcaster.NewReader(true)
|
||||||
|
actualWithCopy, _, err := readerWithCopy.Read()
|
||||||
|
|
||||||
|
if actualWithCopy != nil {
|
||||||
|
t.Fatal("Expected actual frame with copy to be nil")
|
||||||
|
}
|
||||||
|
if err != expectedError {
|
||||||
|
t.Fatal("Expected error to be the same")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// imageToYCbCr converts src to *image.YCbCr and store it to dst
|
// imageToYCbCr converts src to *image.YCbCr and store it to dst
|
||||||
@@ -60,30 +61,61 @@ func imageToYCbCr(dst *image.YCbCr, src image.Image) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bytePool stores slices to be reused
|
||||||
|
// New method is not set as the slice size
|
||||||
|
// should be allocated according to subsample ratio
|
||||||
|
var bytesPool sync.Pool
|
||||||
|
|
||||||
// ToI420 converts r to a new reader that will output images in I420 format
|
// ToI420 converts r to a new reader that will output images in I420 format
|
||||||
func ToI420(r Reader) Reader {
|
func ToI420(r Reader) Reader {
|
||||||
var yuvImg image.YCbCr
|
var yuvImg image.YCbCr
|
||||||
|
|
||||||
|
getSlice := func(cLen int) []uint8 {
|
||||||
|
// Retrieve slice from pool
|
||||||
|
dst, ok := bytesPool.Get().([]byte)
|
||||||
|
|
||||||
|
// Compare value or capacity of retrieved object
|
||||||
|
// If less than expected, reallocate new object
|
||||||
|
if !ok || cap(dst) < 2*cLen {
|
||||||
|
// Allocating memory for Cb and Cr
|
||||||
|
dst = make([]byte, 2*cLen, 2*cLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
return ReaderFunc(func() (image.Image, func(), error) {
|
return ReaderFunc(func() (image.Image, func(), error) {
|
||||||
img, _, err := r.Read()
|
img, _, err := r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var releaseFunc func() = func() {}
|
||||||
|
|
||||||
imageToYCbCr(&yuvImg, img)
|
imageToYCbCr(&yuvImg, img)
|
||||||
|
|
||||||
// Covert pixel format to I420
|
// Covert pixel format to I420
|
||||||
switch yuvImg.SubsampleRatio {
|
switch yuvImg.SubsampleRatio {
|
||||||
case image.YCbCrSubsampleRatio444:
|
|
||||||
i444ToI420(&yuvImg)
|
|
||||||
case image.YCbCrSubsampleRatio422:
|
|
||||||
i422ToI420(&yuvImg)
|
|
||||||
case image.YCbCrSubsampleRatio420:
|
case image.YCbCrSubsampleRatio420:
|
||||||
|
case image.YCbCrSubsampleRatio444:
|
||||||
|
cLen := yuvImg.CStride * yuvImg.Rect.Dy() / 4
|
||||||
|
dst := getSlice(cLen)
|
||||||
|
yuvImg = i444ToI420(yuvImg, dst)
|
||||||
|
releaseFunc = func() {
|
||||||
|
bytesPool.Put(dst)
|
||||||
|
}
|
||||||
|
case image.YCbCrSubsampleRatio422:
|
||||||
|
cLen := yuvImg.CStride * (yuvImg.Rect.Dy() / 2)
|
||||||
|
dst := getSlice(cLen)
|
||||||
|
yuvImg = i422ToI420(yuvImg, dst)
|
||||||
|
releaseFunc = func() {
|
||||||
|
bytesPool.Put(dst)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, func() {}, fmt.Errorf("unsupported pixel format: %s", yuvImg.SubsampleRatio)
|
return nil, releaseFunc, fmt.Errorf("unsupported pixel format: %s", yuvImg.SubsampleRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
yuvImg.SubsampleRatio = image.YCbCrSubsampleRatio420
|
return &yuvImg, releaseFunc, nil
|
||||||
return &yuvImg, func() {}, nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
#include "_cgo_export.h"
|
#include "_cgo_export.h"
|
||||||
|
|
||||||
void i444ToI420CGO(
|
void i444ToI420CGO(
|
||||||
|
unsigned char *cb_dst,
|
||||||
|
unsigned char *cr_dst,
|
||||||
unsigned char* cb,
|
unsigned char* cb,
|
||||||
unsigned char* cr,
|
unsigned char* cr,
|
||||||
const int stride, const int h)
|
const int stride, const int h)
|
||||||
@@ -22,8 +24,8 @@ void i444ToI420CGO(
|
|||||||
((uint16_t)cr[isrc0] + (uint16_t)cr[isrc1] +
|
((uint16_t)cr[isrc0] + (uint16_t)cr[isrc1] +
|
||||||
(uint16_t)cr[isrc0 + 1] + (uint16_t)cr[isrc1 + 1]) /
|
(uint16_t)cr[isrc0 + 1] + (uint16_t)cr[isrc1 + 1]) /
|
||||||
4;
|
4;
|
||||||
cb[idst] = cb2;
|
cb_dst[idst] = cb2;
|
||||||
cr[idst] = cr2;
|
cr_dst[idst] = cr2;
|
||||||
isrc0 += 2;
|
isrc0 += 2;
|
||||||
isrc1 += 2;
|
isrc1 += 2;
|
||||||
idst++;
|
idst++;
|
||||||
@@ -34,6 +36,8 @@ void i444ToI420CGO(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void i422ToI420CGO(
|
void i422ToI420CGO(
|
||||||
|
unsigned char *cb_dst,
|
||||||
|
unsigned char *cr_dst,
|
||||||
unsigned char* cb,
|
unsigned char* cb,
|
||||||
unsigned char* cr,
|
unsigned char* cr,
|
||||||
const int stride, const int h)
|
const int stride, const int h)
|
||||||
@@ -46,8 +50,8 @@ void i422ToI420CGO(
|
|||||||
{
|
{
|
||||||
const uint8_t cb2 = ((uint16_t)cb[isrc] + (uint16_t)cb[isrc + stride]) / 2;
|
const uint8_t cb2 = ((uint16_t)cb[isrc] + (uint16_t)cb[isrc + stride]) / 2;
|
||||||
const uint8_t cr2 = ((uint16_t)cr[isrc] + (uint16_t)cr[isrc + stride]) / 2;
|
const uint8_t cr2 = ((uint16_t)cr[isrc] + (uint16_t)cr[isrc + stride]) / 2;
|
||||||
cb[idst] = cb2;
|
cb_dst[idst] = cb2;
|
||||||
cr[idst] = cr2;
|
cr_dst[idst] = cr2;
|
||||||
isrc++;
|
isrc++;
|
||||||
idst++;
|
idst++;
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build cgo
|
||||||
// +build cgo
|
// +build cgo
|
||||||
|
|
||||||
package video
|
package video
|
||||||
@@ -14,27 +15,41 @@ import "C"
|
|||||||
// All functions switched at runtime must be declared also in convert_nocgo.go.
|
// All functions switched at runtime must be declared also in convert_nocgo.go.
|
||||||
const hasCGOConvert = true
|
const hasCGOConvert = true
|
||||||
|
|
||||||
func i444ToI420(img *image.YCbCr) {
|
func i444ToI420(img image.YCbCr, dst []uint8) image.YCbCr {
|
||||||
h := img.Rect.Dy()
|
h := img.Rect.Dy()
|
||||||
|
cLen := img.CStride * h / 4
|
||||||
|
// Divide preallocated memory to cbDst and crDst
|
||||||
|
// and truncate cap and len to cLen
|
||||||
|
cbDst, crDst := dst[:cLen:cLen], dst[cLen:]
|
||||||
|
crDst = crDst[:cLen:cLen]
|
||||||
C.i444ToI420CGO(
|
C.i444ToI420CGO(
|
||||||
|
(*C.uchar)(&cbDst[0]), (*C.uchar)(&crDst[0]),
|
||||||
(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
|
(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
|
||||||
C.int(img.CStride), C.int(h),
|
C.int(img.CStride), C.int(h),
|
||||||
)
|
)
|
||||||
img.CStride = img.CStride / 2
|
img.CStride = img.CStride / 2
|
||||||
cLen := img.CStride * (h / 2)
|
img.Cb = cbDst
|
||||||
img.Cb = img.Cb[:cLen]
|
img.Cr = crDst
|
||||||
img.Cr = img.Cr[:cLen]
|
img.SubsampleRatio = image.YCbCrSubsampleRatio420
|
||||||
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
func i422ToI420(img *image.YCbCr) {
|
func i422ToI420(img image.YCbCr, dst []uint8) image.YCbCr {
|
||||||
h := img.Rect.Dy()
|
h := img.Rect.Dy()
|
||||||
|
cLen := img.CStride * (h / 2)
|
||||||
|
// Divide preallocated memory to cbDst and crDst
|
||||||
|
// and truncate cap and len to cLen
|
||||||
|
cbDst, crDst := dst[:cLen:cLen], dst[cLen:]
|
||||||
|
crDst = crDst[:cLen:cLen]
|
||||||
C.i422ToI420CGO(
|
C.i422ToI420CGO(
|
||||||
|
(*C.uchar)(&cbDst[0]), (*C.uchar)(&crDst[0]),
|
||||||
(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
|
(*C.uchar)(&img.Cb[0]), (*C.uchar)(&img.Cr[0]),
|
||||||
C.int(img.CStride), C.int(h),
|
C.int(img.CStride), C.int(h),
|
||||||
)
|
)
|
||||||
cLen := img.CStride * (h / 2)
|
img.Cb = cbDst
|
||||||
img.Cb = img.Cb[:cLen]
|
img.Cr = crDst
|
||||||
img.Cr = img.Cr[:cLen]
|
img.SubsampleRatio = image.YCbCrSubsampleRatio420
|
||||||
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
func rgbToYCbCrCGO(y, cb, cr *uint8, r, g, b uint8) { // For testing
|
func rgbToYCbCrCGO(y, cb, cr *uint8, r, g, b uint8) { // For testing
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
void i444ToI420CGO(
|
void i444ToI420CGO(
|
||||||
|
unsigned char *cb_dst,
|
||||||
|
unsigned char *cr_dst,
|
||||||
unsigned char* cb,
|
unsigned char* cb,
|
||||||
unsigned char* cr,
|
unsigned char* cr,
|
||||||
const int stride, const int h);
|
const int stride, const int h);
|
||||||
|
|
||||||
void i422ToI420CGO(
|
void i422ToI420CGO(
|
||||||
|
unsigned char *cb_dst,
|
||||||
|
unsigned char *cr_dst,
|
||||||
unsigned char* cb,
|
unsigned char* cb,
|
||||||
unsigned char* cr,
|
unsigned char* cr,
|
||||||
const int stride, const int h);
|
const int stride, const int h);
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !cgo
|
||||||
// +build !cgo
|
// +build !cgo
|
||||||
|
|
||||||
package video
|
package video
|
||||||
@@ -9,19 +10,25 @@ import (
|
|||||||
|
|
||||||
const hasCGOConvert = false
|
const hasCGOConvert = false
|
||||||
|
|
||||||
func i444ToI420(img *image.YCbCr) {
|
func i444ToI420(img image.YCbCr, dst []uint8) image.YCbCr {
|
||||||
h := img.Rect.Dy()
|
h := img.Rect.Dy()
|
||||||
addrSrc0 := 0
|
addrSrc0 := 0
|
||||||
addrSrc1 := img.CStride
|
addrSrc1 := img.CStride
|
||||||
|
cLen := img.CStride * (h / 4)
|
||||||
addrDst := 0
|
addrDst := 0
|
||||||
|
// Divide preallocated memory to cbDst and crDst
|
||||||
|
// and truncate cap and len to cLen
|
||||||
|
cbDst, crDst := dst[:cLen:cLen], dst[cLen:]
|
||||||
|
crDst = crDst[:cLen:cLen]
|
||||||
|
|
||||||
for i := 0; i < h/2; i++ {
|
for i := 0; i < h/2; i++ {
|
||||||
for j := 0; j < img.CStride/2; j++ {
|
for j := 0; j < img.CStride/2; j++ {
|
||||||
cb := uint16(img.Cb[addrSrc0]) + uint16(img.Cb[addrSrc1]) +
|
cb := uint16(img.Cb[addrSrc0]) + uint16(img.Cb[addrSrc1]) +
|
||||||
uint16(img.Cb[addrSrc0+1]) + uint16(img.Cb[addrSrc1+1])
|
uint16(img.Cb[addrSrc0+1]) + uint16(img.Cb[addrSrc1+1])
|
||||||
cr := uint16(img.Cr[addrSrc0]) + uint16(img.Cr[addrSrc1]) +
|
cr := uint16(img.Cr[addrSrc0]) + uint16(img.Cr[addrSrc1]) +
|
||||||
uint16(img.Cr[addrSrc0+1]) + uint16(img.Cr[addrSrc1+1])
|
uint16(img.Cr[addrSrc0+1]) + uint16(img.Cr[addrSrc1+1])
|
||||||
img.Cb[addrDst] = uint8(cb / 4)
|
cbDst[addrDst] = uint8(cb / 4)
|
||||||
img.Cr[addrDst] = uint8(cr / 4)
|
crDst[addrDst] = uint8(cr / 4)
|
||||||
addrSrc0 += 2
|
addrSrc0 += 2
|
||||||
addrSrc1 += 2
|
addrSrc1 += 2
|
||||||
addrDst++
|
addrDst++
|
||||||
@@ -30,29 +37,37 @@ func i444ToI420(img *image.YCbCr) {
|
|||||||
addrSrc1 += img.CStride
|
addrSrc1 += img.CStride
|
||||||
}
|
}
|
||||||
img.CStride = img.CStride / 2
|
img.CStride = img.CStride / 2
|
||||||
cLen := img.CStride * (h / 2)
|
img.Cb = cbDst
|
||||||
img.Cb = img.Cb[:cLen]
|
img.Cr = crDst
|
||||||
img.Cr = img.Cr[:cLen]
|
img.SubsampleRatio = image.YCbCrSubsampleRatio420
|
||||||
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
func i422ToI420(img *image.YCbCr) {
|
func i422ToI420(img image.YCbCr, dst []uint8) image.YCbCr {
|
||||||
h := img.Rect.Dy()
|
h := img.Rect.Dy()
|
||||||
addrSrc := 0
|
addrSrc := 0
|
||||||
|
cLen := img.CStride * (h / 2)
|
||||||
|
// Divide preallocated memory to cbDst and crDst
|
||||||
|
// and truncate cap and len to cLen
|
||||||
|
cbDst, crDst := dst[:cLen:cLen], dst[cLen:]
|
||||||
|
crDst = crDst[:cLen:cLen]
|
||||||
addrDst := 0
|
addrDst := 0
|
||||||
|
|
||||||
for i := 0; i < h/2; i++ {
|
for i := 0; i < h/2; i++ {
|
||||||
for j := 0; j < img.CStride; j++ {
|
for j := 0; j < img.CStride; j++ {
|
||||||
cb := uint16(img.Cb[addrSrc]) + uint16(img.Cb[addrSrc+img.CStride])
|
cb := uint16(img.Cb[addrSrc]) + uint16(img.Cb[addrSrc+img.CStride])
|
||||||
cr := uint16(img.Cr[addrSrc]) + uint16(img.Cr[addrSrc+img.CStride])
|
cr := uint16(img.Cr[addrSrc]) + uint16(img.Cr[addrSrc+img.CStride])
|
||||||
img.Cb[addrDst] = uint8(cb / 2)
|
cbDst[addrDst] = uint8(cb / 2)
|
||||||
img.Cr[addrDst] = uint8(cr / 2)
|
crDst[addrDst] = uint8(cr / 2)
|
||||||
addrDst++
|
|
||||||
addrSrc++
|
addrSrc++
|
||||||
|
addrDst++
|
||||||
}
|
}
|
||||||
addrSrc += img.CStride
|
addrSrc += img.CStride
|
||||||
}
|
}
|
||||||
cLen := img.CStride * (h / 2)
|
img.Cb = cbDst
|
||||||
img.Cb = img.Cb[:cLen]
|
img.Cr = crDst
|
||||||
img.Cr = img.Cr[:cLen]
|
img.SubsampleRatio = image.YCbCrSubsampleRatio420
|
||||||
|
return img
|
||||||
}
|
}
|
||||||
|
|
||||||
func i444ToRGBA(dst *image.RGBA, src *image.YCbCr) {
|
func i444ToRGBA(dst *image.RGBA, src *image.YCbCr) {
|
||||||
|
@@ -147,10 +147,11 @@ func TestToI420(t *testing.T) {
|
|||||||
r := ToI420(ReaderFunc(func() (image.Image, func(), error) {
|
r := ToI420(ReaderFunc(func() (image.Image, func(), error) {
|
||||||
return c.src, func() {}, nil
|
return c.src, func() {}, nil
|
||||||
}))
|
}))
|
||||||
out, _, err := r.Read()
|
out, release, err := r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
defer release()
|
||||||
if !reflect.DeepEqual(c.expected, out) {
|
if !reflect.DeepEqual(c.expected, out) {
|
||||||
t.Errorf("Expected output image:\n%v\ngot:\n%v", c.expected, out)
|
t.Errorf("Expected output image:\n%v\ngot:\n%v", c.expected, out)
|
||||||
}
|
}
|
||||||
@@ -222,18 +223,19 @@ func BenchmarkToI420(b *testing.B) {
|
|||||||
"RGBA": image.NewRGBA(image.Rect(0, 0, sz[0], sz[1])),
|
"RGBA": image.NewRGBA(image.Rect(0, 0, sz[0], sz[1])),
|
||||||
}
|
}
|
||||||
b.Run(name, func(b *testing.B) {
|
b.Run(name, func(b *testing.B) {
|
||||||
for name, img := range cases {
|
for _, name := range [...]string{"I444", "I422", "I420", "RGBA"} {
|
||||||
img := img
|
img := cases[name]
|
||||||
b.Run(name, func(b *testing.B) {
|
b.Run(name, func(b *testing.B) {
|
||||||
r := ToI420(ReaderFunc(func() (image.Image, func(), error) {
|
r := ToI420(ReaderFunc(func() (image.Image, func(), error) {
|
||||||
return img, func() {}, nil
|
return img, func() {}, nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, _, err := r.Read()
|
_, release, err := r.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Unexpected error: %v", err)
|
b.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -109,7 +109,7 @@ func Scale(width, height int, scaler Scaler) TransformFunc {
|
|||||||
yDy := rect.Dy()
|
yDy := rect.Dy()
|
||||||
cRect := fixedRect(rect, i1.SubsampleRatio)
|
cRect := fixedRect(rect, i1.SubsampleRatio)
|
||||||
cDx := cRect.Dx()
|
cDx := cRect.Dx()
|
||||||
cDy := cRect.Dx()
|
cDy := cRect.Dy()
|
||||||
yLen := yDx * yDy
|
yLen := yDx * yDy
|
||||||
cLen := cDx * cDy
|
cLen := cDx * cDy
|
||||||
if len(imgDst.Y) < yLen {
|
if len(imgDst.Y) < yLen {
|
||||||
|
@@ -36,6 +36,32 @@ func TestScale(t *testing.T) {
|
|||||||
Rect: image.Rect(0, 0, 2, 2),
|
Rect: image.Rect(0, 0, 2, 2),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"RGBASameSize": {
|
||||||
|
src: &image.RGBA{
|
||||||
|
Pix: []uint8{
|
||||||
|
// R G B A | R G B A | R G B A | R G B A
|
||||||
|
0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x00, 0x40, 0x00, 0xFF, 0x00, 0x40, 0x00, 0xFF, 0x00, 0x00, 0x60, 0xFF, 0x00, 0x00, 0x60, 0xFF,
|
||||||
|
0x00, 0x40, 0x00, 0xFF, 0x00, 0x40, 0x00, 0xFF, 0x00, 0x00, 0x60, 0xFF, 0x00, 0x00, 0x60, 0xFF,
|
||||||
|
},
|
||||||
|
Stride: 16,
|
||||||
|
Rect: image.Rect(0, 0, 4, 4),
|
||||||
|
},
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
expected: &image.RGBA{
|
||||||
|
Pix: []uint8{
|
||||||
|
// R G B A | R G B A | R G B A | R G B A
|
||||||
|
0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x00, 0x40, 0x00, 0xFF, 0x00, 0x40, 0x00, 0xFF, 0x00, 0x00, 0x60, 0xFF, 0x00, 0x00, 0x60, 0xFF,
|
||||||
|
0x00, 0x40, 0x00, 0xFF, 0x00, 0x40, 0x00, 0xFF, 0x00, 0x00, 0x60, 0xFF, 0x00, 0x00, 0x60, 0xFF,
|
||||||
|
},
|
||||||
|
Stride: 16,
|
||||||
|
Rect: image.Rect(0, 0, 4, 4),
|
||||||
|
},
|
||||||
|
},
|
||||||
"I444": {
|
"I444": {
|
||||||
src: &image.YCbCr{
|
src: &image.YCbCr{
|
||||||
SubsampleRatio: image.YCbCrSubsampleRatio444,
|
SubsampleRatio: image.YCbCrSubsampleRatio444,
|
||||||
@@ -91,6 +117,70 @@ func TestScale(t *testing.T) {
|
|||||||
Rect: image.Rect(0, 0, 3, 3),
|
Rect: image.Rect(0, 0, 3, 3),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"I444SameSize": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio444,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x40, 0x40, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x40, 0x40, 0x80, 0x80,
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 6,
|
||||||
|
Rect: image.Rect(0, 0, 6, 6),
|
||||||
|
},
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio444,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0xC0, 0xC0,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x40, 0x40, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x40, 0x40, 0x80, 0x80,
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 6,
|
||||||
|
Rect: image.Rect(0, 0, 6, 6),
|
||||||
|
},
|
||||||
|
},
|
||||||
"I422": {
|
"I422": {
|
||||||
src: &image.YCbCr{
|
src: &image.YCbCr{
|
||||||
SubsampleRatio: image.YCbCrSubsampleRatio422,
|
SubsampleRatio: image.YCbCrSubsampleRatio422,
|
||||||
@@ -155,6 +245,82 @@ func TestScale(t *testing.T) {
|
|||||||
Rect: image.Rect(0, 0, 4, 4),
|
Rect: image.Rect(0, 0, 4, 4),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"I422SameSize": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio422,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
},
|
||||||
|
YStride: 8,
|
||||||
|
CStride: 4,
|
||||||
|
Rect: image.Rect(0, 0, 8, 8),
|
||||||
|
},
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio422,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0x80, 0x80,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
},
|
||||||
|
YStride: 8,
|
||||||
|
CStride: 4,
|
||||||
|
Rect: image.Rect(0, 0, 8, 8),
|
||||||
|
},
|
||||||
|
},
|
||||||
"I420": {
|
"I420": {
|
||||||
src: &image.YCbCr{
|
src: &image.YCbCr{
|
||||||
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
@@ -207,6 +373,118 @@ func TestScale(t *testing.T) {
|
|||||||
Rect: image.Rect(0, 0, 4, 4),
|
Rect: image.Rect(0, 0, 4, 4),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"I420SameSize": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
},
|
||||||
|
YStride: 8,
|
||||||
|
CStride: 4,
|
||||||
|
Rect: image.Rect(0, 0, 8, 8),
|
||||||
|
},
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x20, 0x20, 0x80, 0x80,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40,
|
||||||
|
},
|
||||||
|
YStride: 8,
|
||||||
|
CStride: 4,
|
||||||
|
Rect: image.Rect(0, 0, 8, 8),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"I420NonSquareImage": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x50, 0x50,
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x50, 0x50,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0, 0x30, 0x30,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0, 0x30, 0x30,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0xB0, 0xB0,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0xB0, 0xB0,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40, 0xC0, 0xC0,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40, 0xC0, 0xC0,
|
||||||
|
},
|
||||||
|
YStride: 12,
|
||||||
|
CStride: 6,
|
||||||
|
Rect: image.Rect(0, 0, 12, 8),
|
||||||
|
},
|
||||||
|
width: 6,
|
||||||
|
height: 4,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0x10, 0x00, 0x00, 0xF0, 0x10,
|
||||||
|
0x00, 0x00, 0x40, 0x00, 0x40, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x80, 0x30, 0x00, 0x30, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x80, 0x50,
|
||||||
|
0x80, 0xE0, 0x30,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0x80, 0xB0,
|
||||||
|
0xF0, 0x40, 0xC0,
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 3,
|
||||||
|
Rect: image.Rect(0, 0, 6, 4),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, algo := range scalerTestAlgos {
|
for name, algo := range scalerTestAlgos {
|
||||||
algo := algo
|
algo := algo
|
||||||
@@ -247,6 +525,183 @@ func TestScale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScaleFastBoxSampling(t *testing.T) {
|
||||||
|
if !hasCGOConvert {
|
||||||
|
t.Skip("Skip: nocgo implementation is not supported for FastBoxSampling")
|
||||||
|
}
|
||||||
|
cases := map[string]struct {
|
||||||
|
src image.Image
|
||||||
|
width, height int
|
||||||
|
expected image.Image
|
||||||
|
}{
|
||||||
|
"I420NonSquareImage": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF8, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10,
|
||||||
|
0xF8, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00,
|
||||||
|
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||||
|
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x50, 0x50,
|
||||||
|
0x20, 0x20, 0x80, 0x80, 0x50, 0x50,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0, 0x34, 0x34,
|
||||||
|
0x80, 0x80, 0xE0, 0xE0, 0x30, 0x30,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0xB0, 0xB0,
|
||||||
|
0xE0, 0xE0, 0x80, 0x80, 0xB0, 0xB0,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40, 0xC0, 0xC0,
|
||||||
|
0xF0, 0xF0, 0x40, 0x40, 0xC0, 0xC0,
|
||||||
|
},
|
||||||
|
YStride: 12,
|
||||||
|
CStride: 6,
|
||||||
|
Rect: image.Rect(0, 0, 12, 8),
|
||||||
|
},
|
||||||
|
width: 6,
|
||||||
|
height: 4,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF4, 0x10, 0x00, 0x00, 0xF0, 0x10,
|
||||||
|
0x00, 0x00, 0x40, 0x00, 0x40, 0x00,
|
||||||
|
0x04, 0x00, 0x00, 0x00, 0x02, 0x00,
|
||||||
|
0x00, 0x80, 0x30, 0x00, 0x30, 0x00,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x80, 0x50,
|
||||||
|
0x80, 0xE0, 0x32,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0x80, 0xB0,
|
||||||
|
0xF0, 0x40, 0xC0,
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 3,
|
||||||
|
Rect: image.Rect(0, 0, 6, 4),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"I420Uneven6x4to3x2": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x40,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80,
|
||||||
|
0x20, 0x20, 0x80,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80,
|
||||||
|
0xFF, // dummy data to detect out of range read
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 3,
|
||||||
|
Rect: image.Rect(0, 0, 6, 3),
|
||||||
|
},
|
||||||
|
width: 4,
|
||||||
|
height: 2,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0x10, 0x08, 0x00,
|
||||||
|
0x00, 0x00, 0x20, 0x40,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x80,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0x80,
|
||||||
|
},
|
||||||
|
YStride: 4,
|
||||||
|
CStride: 2,
|
||||||
|
Rect: image.Rect(0, 0, 4, 2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"I420Uneven6x3to5x2": {
|
||||||
|
src: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00,
|
||||||
|
0xF0, 0xF0, 0x10, 0x10, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x40, 0x50,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x20, 0x80,
|
||||||
|
0x20, 0x20, 0x80,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0xE0, 0x80,
|
||||||
|
0xE0, 0xE0, 0x80,
|
||||||
|
0xFF, // dummy data to detect out of range read
|
||||||
|
},
|
||||||
|
YStride: 6,
|
||||||
|
CStride: 3,
|
||||||
|
Rect: image.Rect(0, 0, 6, 3),
|
||||||
|
},
|
||||||
|
width: 5,
|
||||||
|
height: 2,
|
||||||
|
expected: &image.YCbCr{
|
||||||
|
SubsampleRatio: image.YCbCrSubsampleRatio420,
|
||||||
|
Y: []uint8{
|
||||||
|
0xF0, 0x10, 0x10, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x40, 0x50,
|
||||||
|
},
|
||||||
|
Cb: []uint8{
|
||||||
|
0x20, 0x80, 0x00,
|
||||||
|
},
|
||||||
|
Cr: []uint8{
|
||||||
|
0xE0, 0x80, 0x00,
|
||||||
|
},
|
||||||
|
YStride: 5,
|
||||||
|
CStride: 3,
|
||||||
|
Rect: image.Rect(0, 0, 5, 2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
c := c
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
trans := Scale(c.width, c.height, ScalerFastBoxSampling)
|
||||||
|
r := trans(ReaderFunc(func() (image.Image, func(), error) {
|
||||||
|
return c.src, func() {}, nil
|
||||||
|
}))
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
out, _, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(c.expected, out) {
|
||||||
|
t.Errorf("Expected output image:\n%v\ngot:\n%v\nrepeat: %d", c.expected, out, i)
|
||||||
|
}
|
||||||
|
// Destroy output contents
|
||||||
|
switch v := out.(type) {
|
||||||
|
case *image.RGBA:
|
||||||
|
v.Stride = 10
|
||||||
|
v.Pix = v.Pix[:1]
|
||||||
|
v.Rect.Max.X = 1
|
||||||
|
case *image.YCbCr:
|
||||||
|
v.YStride = 10
|
||||||
|
v.CStride = 100
|
||||||
|
v.Y = v.Y[:1]
|
||||||
|
v.Cb = v.Cb[:2]
|
||||||
|
v.Cr = v.Cr[:1]
|
||||||
|
v.Rect.Max.X = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkScale(b *testing.B) {
|
func BenchmarkScale(b *testing.B) {
|
||||||
for name, algo := range scalerBenchAlgos {
|
for name, algo := range scalerBenchAlgos {
|
||||||
algo := algo
|
algo := algo
|
||||||
|
@@ -29,7 +29,7 @@ void fastBoxSampling(
|
|||||||
const int sw, const int sh, const int sstride,
|
const int sw, const int sh, const int sstride,
|
||||||
uint32_t* tmp)
|
uint32_t* tmp)
|
||||||
{
|
{
|
||||||
memset(tmp, 0, dw * dh * ch);
|
memset(tmp, 0, dstride * dh * ch * sizeof(tmp[0]));
|
||||||
|
|
||||||
for (int sy = 0; sy < sh; sy++)
|
for (int sy = 0; sy < sh; sy++)
|
||||||
{
|
{
|
||||||
@@ -39,13 +39,14 @@ void fastBoxSampling(
|
|||||||
uint32_t* tmp2 = &tmp[ty * dstride];
|
uint32_t* tmp2 = &tmp[ty * dstride];
|
||||||
for (int sx = 0; sx < sw * ch; sx += ch)
|
for (int sx = 0; sx < sw * ch; sx += ch)
|
||||||
{
|
{
|
||||||
if (tx * sw < sx * dw)
|
|
||||||
tx += ch;
|
|
||||||
|
|
||||||
for (int c = 0; c < ch; c++)
|
for (int c = 0; c < ch; c++)
|
||||||
{
|
{
|
||||||
tmp2[tx + c] += 0x10000 | src2[sx + c];
|
tmp2[tx + c] += 0x10000 | src2[sx + c];
|
||||||
}
|
}
|
||||||
|
if (tx * sw < sx * dw)
|
||||||
|
{
|
||||||
|
tx += ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
pkg/io/video/scaler_nocgo.go
Normal file
22
pkg/io/video/scaler_nocgo.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//go:build !cgo
|
||||||
|
// +build !cgo
|
||||||
|
|
||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScalerFastBoxSampling mock scaler for nocgo implementation to pass tests for CGO_ENABLED=0
|
||||||
|
var (
|
||||||
|
ScalerFastBoxSampling = Scaler(&FastBoxSampling{})
|
||||||
|
)
|
||||||
|
|
||||||
|
// FastBoxSampling mock implementation for nocgo implementation
|
||||||
|
// TODO implement nocgo FastBoxSampling scaling algorithm
|
||||||
|
type FastBoxSampling struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FastBoxSampling) Scale(_ draw.Image, _ image.Rectangle, _ image.Image, _ image.Rectangle, _ draw.Op, _ *draw.Options) {
|
||||||
|
}
|
@@ -30,10 +30,18 @@ func (p *rgbLikeYCbCr) At(x, y int) color.Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *rgbLikeYCbCr) Set(x, y int, c color.Color) {
|
func (p *rgbLikeYCbCr) Set(x, y int, c color.Color) {
|
||||||
rgb := c.(*color.RGBA64)
|
switch v := c.(type) {
|
||||||
p.y.SetGray(x, y, color.Gray{uint8(rgb.R / 0x100)})
|
case color.RGBA:
|
||||||
if (image.Point{x, y}.In(p.cb.Rect)) {
|
p.y.SetGray(x, y, color.Gray{v.R})
|
||||||
p.cb.SetGray(x, y, color.Gray{uint8(rgb.G / 0x100)})
|
if (image.Point{x, y}.In(p.cb.Rect)) {
|
||||||
p.cr.SetGray(x, y, color.Gray{uint8(rgb.B / 0x100)})
|
p.cb.SetGray(x, y, color.Gray{v.G})
|
||||||
|
p.cr.SetGray(x, y, color.Gray{v.B})
|
||||||
|
}
|
||||||
|
case *color.RGBA64:
|
||||||
|
p.y.SetGray(x, y, color.Gray{uint8(v.R / 0x100)})
|
||||||
|
if (image.Point{x, y}.In(p.cb.Rect)) {
|
||||||
|
p.cb.SetGray(x, y, color.Gray{uint8(v.G / 0x100)})
|
||||||
|
p.cr.SetGray(x, y, color.Gray{uint8(v.B / 0x100)})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package prop
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -54,10 +55,8 @@ type DurationOneOf []time.Duration
|
|||||||
|
|
||||||
// Compare implements DurationConstraint.
|
// Compare implements DurationConstraint.
|
||||||
func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
|
func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
|
||||||
for _, ii := range d {
|
if slices.Contains(d, a) {
|
||||||
if ii == a {
|
return 0.0, true
|
||||||
return 0.0, true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user