mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-26 20:41:46 +08:00
Compare commits
265 Commits
v0.1.14
...
62ae95a16a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
62ae95a16a | ||
![]() |
1406108fb2 | ||
![]() |
a2a211857c | ||
![]() |
c0721738c4 | ||
![]() |
6047a32ea0 | ||
![]() |
60bf158757 | ||
![]() |
c4fd28c7df | ||
![]() |
c79e16706b | ||
![]() |
89420ae84d | ||
![]() |
4db71e5b52 | ||
![]() |
a45a5e50cd | ||
![]() |
d90220699e | ||
![]() |
71deb52047 | ||
![]() |
84ccb15157 | ||
![]() |
ec6a4b6925 | ||
![]() |
551fb6afd8 | ||
![]() |
20e8c50735 | ||
![]() |
7211d077ee | ||
![]() |
cd5f8eb43a | ||
![]() |
2d7bdd4e24 | ||
![]() |
5cad3f1b41 | ||
![]() |
9d5e9cb3ea | ||
![]() |
9a47a07eba | ||
![]() |
4c70a5f686 | ||
![]() |
36a03e823e | ||
![]() |
24e3a722cf | ||
![]() |
ce9b412d0e | ||
![]() |
8f916c5c67 | ||
![]() |
c10fb000db | ||
![]() |
309b50a801 | ||
![]() |
6f6c42a695 | ||
![]() |
4027590fcb | ||
![]() |
5ac53a463c | ||
![]() |
f2a550d5e2 | ||
![]() |
9ea1754cc7 | ||
![]() |
bc0b11e3bf | ||
![]() |
ac66b130b9 | ||
![]() |
cf1be6fd31 | ||
![]() |
6339a1890f | ||
![]() |
6a00ffcdf8 | ||
![]() |
c6aa90a133 | ||
![]() |
e3eae5d8db | ||
![]() |
7020457f63 | ||
![]() |
e3fef141d9 | ||
![]() |
8fb8d65764 | ||
![]() |
ae63fa65bf | ||
![]() |
23d77e2bb2 | ||
![]() |
02d4e0e896 | ||
![]() |
38a3b829f6 | ||
![]() |
c4bb9eb649 | ||
![]() |
bc3bdc1855 | ||
![]() |
a6ffeb31ab | ||
![]() |
a9046e6ac1 | ||
![]() |
2bc011f39f | ||
![]() |
68df5b3eb5 | ||
![]() |
bf52b1af58 | ||
![]() |
ccee17d04c | ||
![]() |
f4f5a24ce4 | ||
![]() |
dec2300a95 | ||
![]() |
0c8f3cfc7a | ||
![]() |
6829d71e58 | ||
![]() |
1b36c0360d | ||
![]() |
5aa157e8fc | ||
![]() |
cd8e34f3ed | ||
![]() |
d03383a6fd | ||
![]() |
c4fd1de9a0 | ||
![]() |
47638e3290 | ||
![]() |
09b497727d | ||
![]() |
7ae82fbda7 | ||
![]() |
4477898296 | ||
![]() |
d5d41e9ca5 | ||
![]() |
9fb24fb036 | ||
![]() |
1cd1b136cc | ||
![]() |
08fb3e8a48 | ||
![]() |
4aae1bc842 | ||
![]() |
3c9fee958e | ||
![]() |
68092afe36 | ||
![]() |
cd92becd1c | ||
![]() |
2bad953124 | ||
![]() |
634addc04d | ||
![]() |
d0c1677cfb | ||
![]() |
b4770e5fbf | ||
![]() |
855441dad1 | ||
![]() |
2882fd42d5 | ||
![]() |
694b4abd83 | ||
![]() |
afb2f78e3c | ||
![]() |
b14ce7987c | ||
![]() |
728207526d | ||
![]() |
3582e5d017 | ||
![]() |
774b7de8d2 | ||
![]() |
96efc0932e | ||
![]() |
a1b4c0f69a | ||
![]() |
6bba5f1663 | ||
![]() |
79aef00e07 | ||
![]() |
03c44ee803 | ||
![]() |
dad145ef11 | ||
![]() |
72c1d7bb89 | ||
![]() |
fc301a8a92 | ||
![]() |
aca3ee9126 | ||
![]() |
4924411e88 | ||
![]() |
b88f541c4f | ||
![]() |
09f6bdeac4 | ||
![]() |
e64f0d8697 | ||
![]() |
36f908c6e2 | ||
![]() |
9987e01d3f | ||
![]() |
ae173b1b61 | ||
![]() |
4eea55285e | ||
![]() |
18cf1fe38a | ||
![]() |
138499b52d | ||
![]() |
87486146d5 | ||
![]() |
cadb155755 | ||
![]() |
2372f55064 | ||
![]() |
8568b1b20d | ||
![]() |
f0f6be7350 | ||
![]() |
8146e84f2f | ||
![]() |
4ff24bd656 | ||
![]() |
64dbe507f0 | ||
![]() |
dffaf0fcb4 | ||
![]() |
d3adaeea1a | ||
![]() |
7a414948c6 | ||
![]() |
e1616b8cc2 | ||
![]() |
52a080b55a | ||
![]() |
c2fe66c579 | ||
![]() |
ac50077e77 | ||
![]() |
2f5c61e1f3 | ||
![]() |
0715258726 | ||
![]() |
bccff100e5 | ||
![]() |
0507093a59 | ||
![]() |
bf290b026c | ||
![]() |
0dc4f43c94 | ||
![]() |
14bfaa5dbd | ||
![]() |
09c31a264c | ||
![]() |
30badd819d | ||
![]() |
dc8aeea11f | ||
![]() |
57c9ba0fc5 | ||
![]() |
b9ce5bb861 | ||
![]() |
e4ac96ea6b | ||
![]() |
11bf55f80c | ||
![]() |
55881ddd41 | ||
![]() |
62009a882b | ||
![]() |
dbd37689e4 | ||
![]() |
d561715bf9 | ||
![]() |
76ba048312 | ||
![]() |
5da0ebf443 | ||
![]() |
f8f8511d94 | ||
![]() |
85597da5bb | ||
![]() |
6ac1424488 | ||
![]() |
73a158d097 | ||
![]() |
cb23f1fa82 | ||
![]() |
a0f090dced | ||
![]() |
4f7542b614 | ||
![]() |
860f4a1490 | ||
![]() |
09f3bcc013 | ||
![]() |
f0ad407da8 | ||
![]() |
8d7ccada1c | ||
![]() |
2d9208de5b | ||
![]() |
52cf6e72b1 | ||
![]() |
7335797301 | ||
![]() |
3bec69bbf8 | ||
![]() |
58dc90d03a | ||
![]() |
8ad810e61e | ||
![]() |
6f204fa3d1 | ||
![]() |
5215057409 | ||
![]() |
3bcbed0286 | ||
![]() |
907e0d68e2 | ||
![]() |
9fd2d01dbe | ||
![]() |
285f8cd23c | ||
![]() |
b309c30ca0 | ||
![]() |
601f27c014 | ||
![]() |
2a04a14225 | ||
![]() |
416bbc33f3 | ||
![]() |
4a682a48c1 | ||
![]() |
14db2b8130 | ||
![]() |
a3c15d1fb0 | ||
![]() |
43272ea965 | ||
![]() |
e32fc1bdb8 | ||
![]() |
2af325d1a5 | ||
![]() |
5e0df5e5cf | ||
![]() |
d038133783 | ||
![]() |
cd6aaa1393 | ||
![]() |
82cc32308b | ||
![]() |
8fce8a2bb5 | ||
![]() |
5b99500290 | ||
![]() |
e371c0d955 | ||
![]() |
69f9cbe008 | ||
![]() |
b5acc5d7f6 | ||
![]() |
55e65027f9 | ||
![]() |
f0ff9261b4 | ||
![]() |
08a396571f | ||
![]() |
0d09f7f458 | ||
![]() |
e780bdc6f9 | ||
![]() |
ff18b21629 | ||
![]() |
eaf9ff42a8 | ||
![]() |
5ba49e03e7 | ||
![]() |
1250e06923 | ||
![]() |
651c847674 | ||
![]() |
3b2316081e | ||
![]() |
70261260cb | ||
![]() |
548cdac668 | ||
![]() |
79f9fc31f6 | ||
![]() |
1f92ea40da | ||
![]() |
4beb7e5a23 | ||
![]() |
9bb5755cd2 | ||
![]() |
e316b30964 | ||
![]() |
596b8c4e11 | ||
![]() |
be5f684ea6 | ||
![]() |
a88c2daf89 | ||
![]() |
1f313a9d61 | ||
![]() |
19eaf375ff | ||
![]() |
b3c94a1f7b | ||
![]() |
86cb9f8ce8 | ||
![]() |
f8d1f974cf | ||
![]() |
809f74cafc | ||
![]() |
eb2db82766 | ||
![]() |
b4c6eb5409 | ||
![]() |
b263026d52 | ||
![]() |
070ab924f9 | ||
![]() |
23177a5d75 | ||
![]() |
3a04686875 | ||
![]() |
5f95b84719 | ||
![]() |
8919ba4fe5 | ||
![]() |
153c36e461 | ||
![]() |
10769b702e | ||
![]() |
2948735964 | ||
![]() |
ba848b3416 | ||
![]() |
94c6b66e46 | ||
![]() |
96fd92142c | ||
![]() |
d71b72c64d | ||
![]() |
8c2c8a9b27 | ||
![]() |
1e03f61b4b | ||
![]() |
b863c105c8 | ||
![]() |
6411b00e93 | ||
![]() |
acd2cb992b | ||
![]() |
dafd208de7 | ||
![]() |
fcec5a9149 | ||
![]() |
3d3830f7ff | ||
![]() |
655b513810 | ||
![]() |
eaaaacfc6b | ||
![]() |
fa95e47bad | ||
![]() |
020de77bc9 | ||
![]() |
33b6412c26 | ||
![]() |
f29d08ae6b | ||
![]() |
b5b0653697 | ||
![]() |
a1087f7f4e | ||
![]() |
217e634f7e | ||
![]() |
60b8e3ae1b | ||
![]() |
7df3114cdc | ||
![]() |
9741508d2b | ||
![]() |
7a569f0901 | ||
![]() |
ee40fcd070 | ||
![]() |
499c08d513 | ||
![]() |
5a1bd11087 | ||
![]() |
7ce935eac8 | ||
![]() |
a359005a7d | ||
![]() |
ca4116b5ce | ||
![]() |
8a4e0779d7 | ||
![]() |
d222ff3d74 | ||
![]() |
b9bb4fdc34 | ||
![]() |
cc823958e1 | ||
![]() |
64f39187b8 | ||
![]() |
c56cc487a3 | ||
![]() |
d86fc4a3e9 | ||
![]() |
2f21d9e738 | ||
![]() |
3316476b30 | ||
![]() |
0b1a19f343 |
72
.github/workflows/ci.yaml
vendored
72
.github/workflows/ci.yaml
vendored
@@ -13,13 +13,16 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ '1.15', '1.14' ]
|
||||
go:
|
||||
- '1.21' # oldest version this package supports
|
||||
- '1.22' # oldstable Go version
|
||||
- '1.23' # stable Go version
|
||||
name: Linux Go ${{ matrix.go }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install dependencies
|
||||
@@ -29,69 +32,54 @@ jobs:
|
||||
libopus-dev \
|
||||
libva-dev \
|
||||
libvpx-dev \
|
||||
libx264-dev
|
||||
- name: go vet
|
||||
run: go vet $(go list ./... | grep -v mmal)
|
||||
- name: go build
|
||||
run: go build $(go list ./... | grep -v mmal)
|
||||
- name: go build without CGO
|
||||
run: go build . pkg/...
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: go test
|
||||
run: go test -v -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v mmal)
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: matrix.go == '1.15'
|
||||
- name: go test without CGO
|
||||
run: go test . pkg/... -v
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
libx11-dev \
|
||||
libx264-dev \
|
||||
libxext-dev
|
||||
- name: Run Test Suite
|
||||
run: make test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
build-darwin:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [ '1.15', '1.14' ]
|
||||
go:
|
||||
- '1.22'
|
||||
- '1.23'
|
||||
runs-on: macos-latest
|
||||
name: Darwin Go ${{ matrix.go }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
which brew
|
||||
brew install \
|
||||
pkg-config \
|
||||
opus \
|
||||
libvpx \
|
||||
x264
|
||||
- name: go vet
|
||||
run: go vet $(go list ./... | grep -v mmal)
|
||||
- name: go build
|
||||
run: go build $(go list ./... | grep -v mmal)
|
||||
- name: go build without CGO
|
||||
run: go build . pkg/...
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: go test
|
||||
run: go test -v -race $(go list ./... | grep -v mmal)
|
||||
- name: go test without CGO
|
||||
run: go test . pkg/... -v
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
- name: Run Test Suite
|
||||
run: make test
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
check-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check Licenses
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.15'
|
||||
go-version: stable
|
||||
- name: Installing go-licenses
|
||||
run: go get github.com/google/go-licenses
|
||||
run: go install github.com/google/go-licenses@latest
|
||||
- name: Checking licenses
|
||||
run: go-licenses check ./...
|
||||
|
2
.github/workflows/renovate-go-mod-fix.yaml
vendored
2
.github/workflows/renovate-go-mod-fix.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: fix
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,3 +12,6 @@
|
||||
*.out
|
||||
|
||||
scripts/cross
|
||||
coverage.txt
|
||||
|
||||
.idea
|
||||
|
83
Makefile
Normal file
83
Makefile
Normal file
@@ -0,0 +1,83 @@
|
||||
docker_owner := lherman
|
||||
docker_prefix := cross
|
||||
toolchain_dockerfiles := dockerfiles
|
||||
script_path := $(realpath scripts)
|
||||
toolchain_path := $(script_path)/$(docker_prefix)
|
||||
os_list := \
|
||||
linux \
|
||||
windows \
|
||||
darwin
|
||||
arch_list := \
|
||||
armv7 \
|
||||
arm64 \
|
||||
x64
|
||||
supported_platforms := \
|
||||
linux-armv7 \
|
||||
linux-arm64 \
|
||||
linux-x64 \
|
||||
windows-x64 \
|
||||
darwin-x64 \
|
||||
darwin-arm64
|
||||
cmd_build := build
|
||||
cmd_test := test
|
||||
examples_dir := examples
|
||||
codec_dir := pkg/codec
|
||||
codec_list := $(shell ls $(codec_dir)/*/Makefile)
|
||||
codec_list := $(codec_list:$(codec_dir)/%/Makefile=%)
|
||||
targets := $(foreach codec, $(codec_list), $(addprefix $(cmd_build)-$(codec)-, $(supported_platforms)))
|
||||
pkgs_without_ext_device := $(shell go list ./... | grep -v mmal | grep -v vaapi)
|
||||
pkgs_without_cgo := $(shell go list ./... | grep -v pkg/codec | grep -v pkg/driver | grep -v pkg/avfoundation)
|
||||
|
||||
define BUILD_TEMPLATE
|
||||
ifneq (,$$(findstring $(2)-$(3),$$(supported_platforms)))
|
||||
$$(cmd_build)-$(1)-$(2)-$(3): toolchain-$(2)-$(3)
|
||||
$$(MAKE) --directory=$$(codec_dir)/$(1) \
|
||||
MEDIADEVICES_TOOLCHAIN_BIN=$$(toolchain_path)/$(docker_prefix)-$(2)-$(3) \
|
||||
MEDIADEVICES_TARGET_PLATFORM=$(2)-$(3) \
|
||||
MEDIADEVICES_TARGET_OS=$(2) \
|
||||
MEDIADEVICES_TARGET_ARCH=$(3)
|
||||
endif
|
||||
endef
|
||||
|
||||
.PHONY: all
|
||||
all: $(cmd_test) $(cmd_build)
|
||||
|
||||
# Subcommand:
|
||||
# make build[-<codec_name>-<os>-<arch>]
|
||||
#
|
||||
# Description:
|
||||
# Build codec dependencies to multiple platforms.
|
||||
#
|
||||
# Examples:
|
||||
# * make build: build all codecs for all supported platforms
|
||||
# * make build-opus-darwin-x64: only build opus for darwin-x64 platform
|
||||
$(cmd_build): $(targets)
|
||||
|
||||
toolchain-%: $(toolchain_dockerfiles)
|
||||
$(MAKE) --directory=$< "$*" \
|
||||
MEDIADEVICES_DOCKER_OWNER=$(docker_owner) \
|
||||
MEDIADEVICES_DOCKER_PREFIX=$(docker_prefix)
|
||||
@mkdir -p $(toolchain_path)
|
||||
@docker run $(docker_owner)/$(docker_prefix)-$* > \
|
||||
$(toolchain_path)/$(docker_prefix)-$*
|
||||
@chmod +x $(toolchain_path)/$(docker_prefix)-$*
|
||||
|
||||
$(foreach codec, $(codec_list), \
|
||||
$(foreach os, $(os_list), \
|
||||
$(foreach arch, $(arch_list), \
|
||||
$(eval $(call BUILD_TEMPLATE,$(codec),$(os),$(arch))))))
|
||||
|
||||
# Subcommand:
|
||||
# make test
|
||||
#
|
||||
# Description:
|
||||
# Run a series of tests
|
||||
$(cmd_test):
|
||||
go vet $(pkgs_without_ext_device)
|
||||
go build $(pkgs_without_ext_device)
|
||||
# go build without CGO
|
||||
CGO_ENABLED=0 go build $(pkgs_without_cgo)
|
||||
# go build with CGO
|
||||
CGO_ENABLED=1 go build $(pkgs_without_ext_device)
|
||||
$(MAKE) --directory=$(examples_dir)
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(pkgs_without_ext_device)
|
83
README.md
83
README.md
@@ -5,9 +5,9 @@
|
||||
</h1>
|
||||
<h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4>
|
||||
<p align="center">
|
||||
<a href="https://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://github.com/pion/mediadevices/actions"><img src="https://github.com/pion/mediadevices/workflows/CI/badge.svg?branch=master" alt="Build status"></a>
|
||||
<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://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a> <a href="https://twitter.com/_pion?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40_pion&style=social&url=https%3A%2F%2Ftwitter.com%2F_pion" alt="Twitter Widget"></a>
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/mediadevices/test.yaml">
|
||||
<a href="https://pkg.go.dev/github.com/pion/mediadevices"><img src="https://pkg.go.dev/badge/github.com/pion/mediadevices.svg" alt="Go Reference"></a>
|
||||
<a href="https://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
||||
</p>
|
||||
@@ -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!
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -67,27 +69,23 @@ func main() {
|
||||
output, _ := os.Create("frame.jpg")
|
||||
jpeg.Encode(output, frame, nil)
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
## More Examples
|
||||
|
||||
### More Examples
|
||||
* [Webrtc](/examples/webrtc) - Use Webrtc to create a realtime peer-to-peer video call
|
||||
* [Face Detection](/examples/facedetection) - Use a machine learning algorithm to detect faces in a camera stream
|
||||
* [RTP Stream](examples/rtp) - Capture camera stream, encode it in H264/VP8/VP9, and send it to a RTP server
|
||||
* [HTTP Broadcast](/examples/http) - Broadcast camera stream through HTTP with MJPEG
|
||||
* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera
|
||||
|
||||
## Available Media Inputs
|
||||
|
||||
| Input | Linux | Mac | Windows |
|
||||
### Available Media Inputs
|
||||
| Input | Linux | Mac | Windows |
|
||||
| :--------: | :---: | :-: | :-----: |
|
||||
| Camera | ✔️ | ✔️ | ✔️ |
|
||||
| Microphone | ✔️ | ✔️ | ✔️ |
|
||||
| Screen | ✔️ | ✔️ | ✔️ |
|
||||
| Camera | ✔️ | ✔️ | ✔️ |
|
||||
| Microphone | ✔️ | ✔️ | ✔️ |
|
||||
| Screen | ✔️ | ✔️ | ✔️ |
|
||||
|
||||
By default, there's no media input registered. This decision was made to allow you to pay 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:
|
||||
|
||||
```go
|
||||
import (
|
||||
@@ -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`:
|
||||
|
||||
```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.
|
||||
|
||||
### 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.
|
||||
|
||||
* 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`
|
||||
* Ubuntu: `apt install libx264-dev`
|
||||
|
||||
#### mmal
|
||||
##### mmal
|
||||
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)
|
||||
* 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.
|
||||
|
||||
* 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
|
||||
|
||||
#### vpx
|
||||
##### vpx
|
||||
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)
|
||||
@@ -160,7 +157,7 @@ A free software video codec library from Google and the Alliance for Open Media
|
||||
* Mac: `brew install libvpx`
|
||||
* 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).
|
||||
|
||||
* 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`
|
||||
|
||||
|
||||
### Audio Codecs
|
||||
#### Audio Codecs
|
||||
|
||||
#### opus
|
||||
##### opus
|
||||
A totally open, royalty-free, highly versatile audio codec.
|
||||
|
||||
* Package: [github.com/pion/mediadevices/pkg/codec/opus](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/opus)
|
||||
@@ -178,16 +175,14 @@ A totally open, royalty-free, highly versatile audio codec.
|
||||
* Mac: `brew install opus`
|
||||
* 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**.
|
||||
|
||||
The test was taken by capturing a camera stream, decoding the raw frames, encoding the video stream with mmal, and sending the stream through Webrtc.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Failed to find the best driver that fits the constraints
|
||||
### FAQ
|
||||
|
||||
#### Failed to find the best driver that fits the constraints
|
||||
`mediadevices` provides an automated driver discovery through `GetUserMedia` and `GetDisplayMedia`. The driver discover algorithm works something like:
|
||||
|
||||
1. Open all registered drivers
|
||||
@@ -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`
|
||||
* Your constraint is too strict that there's no driver can fullfil your requirements. In this case, you can try to turn up the debug level by specifying the following environment variable: `export PION_LOG_DEBUG=all` to see what was too strict and tune that.
|
||||
* Your driver is not supported/implemented. In this case, you can either let us know (file an issue) and wait for the maintainers to implement it. Or, you can implement it yourself and register it through `RegisterDriverAdapter`
|
||||
* If trying to use `import _ github.com/pion/mediadevices/pkg/driver/screen` note that you will need to use `GetDisplayMedia` instead of `GetUserMedia`
|
||||
|
||||
### Failed to find vpx/x264/mmal/opus codecs
|
||||
|
||||
#### Failed to find vpx/x264/mmal/opus codecs
|
||||
Since `mediadevices` uses cgo to access video/audio codecs, it needs to find these libraries from the system. To accomplish this, [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) is used for library discovery.
|
||||
|
||||
If you see the following error message at compile time:
|
||||
```
|
||||
|
||||
```bash
|
||||
# pkg-config --cflags -- vpx
|
||||
Package vpx was not found in the pkg-config search path.
|
||||
Perhaps you should add the directory containing `vpx.pc'
|
||||
@@ -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).
|
||||
* Pkg-config fails to find the `.pc` files for this codec ([reference](https://people.freedesktop.org/~dbn/pkg-config-guide.html#using)). In this case, you need to find where the codec library's `.pc` is stored, and let pkg-config knows with: `export PKG_CONFIG_PATH=/path/to/directory`.
|
||||
|
||||
### Roadmap
|
||||
The library can be used with our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
|
||||
|
||||
## Community
|
||||
Pion has an active community on the [Slack](https://pion.ly/slack).
|
||||
### Community
|
||||
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!
|
||||
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
|
||||
|
||||
## Contributing
|
||||
Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
|
||||
### Contributing
|
||||
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_
|
||||
* [Atsushi Watanabe](https://github.com/at-wat) - _VP8, Screencast, etc._
|
||||
|
||||
## License
|
||||
### License
|
||||
MIT License - see [LICENSE](LICENSE) for full text
|
||||
|
70
build.sh
70
build.sh
@@ -1,70 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
MEDIADEVICES_TOOLCHAIN_OWNER=lherman
|
||||
MEDIADEVICES_TOOLCHAIN_PREFIX=cross
|
||||
MEDIADEVICES_SCRIPT_PATH=$(realpath ./scripts)
|
||||
MEDIADEVICES_TOOLCHAIN_PATH=${MEDIADEVICES_SCRIPT_PATH}/${MEDIADEVICES_TOOLCHAIN_PREFIX}
|
||||
MEDIADEVICES_DOCKERFILES_PATH=dockerfiles
|
||||
|
||||
# Reference: https://github.com/dockcross/dockcross#cross-compilers
|
||||
MEDIADEVICES_TARGET_PLATFORMS=(
|
||||
linux-armv7
|
||||
linux-arm64
|
||||
linux-x64
|
||||
windows-x64
|
||||
darwin-x64
|
||||
)
|
||||
|
||||
if [[ -z ${VERBOSE} ]]; then
|
||||
MEDIADEVICES_OUTPUT=/dev/null
|
||||
else
|
||||
MEDIADEVICES_OUTPUT=/dev/stdout
|
||||
fi
|
||||
|
||||
install_toolchains() {
|
||||
bash ${MEDIADEVICES_DOCKERFILES_PATH}/build.sh &> ${MEDIADEVICES_OUTPUT}
|
||||
for platform in ${MEDIADEVICES_TARGET_PLATFORMS[@]}
|
||||
do
|
||||
mkdir -p ${MEDIADEVICES_TOOLCHAIN_PATH}
|
||||
image=${MEDIADEVICES_TOOLCHAIN_OWNER}/${MEDIADEVICES_TOOLCHAIN_PREFIX}-${platform}
|
||||
bin_path=${MEDIADEVICES_TOOLCHAIN_PATH}/${MEDIADEVICES_TOOLCHAIN_PREFIX}-${platform}
|
||||
docker run ${image} > ${bin_path}
|
||||
chmod +x ${bin_path}
|
||||
done
|
||||
}
|
||||
|
||||
build() {
|
||||
sub_builds=$(find pkg -type f -name "build.sh")
|
||||
for sub_build in ${sub_builds[@]}
|
||||
do
|
||||
sub_build=$(realpath ${sub_build})
|
||||
sub_build_dir=$(dirname ${sub_build})
|
||||
current_dir=${PWD}
|
||||
cd $sub_build_dir
|
||||
for platform in ${MEDIADEVICES_TARGET_PLATFORMS[@]}
|
||||
do
|
||||
export MEDIADEVICES_TOOLCHAIN_BIN=${MEDIADEVICES_TOOLCHAIN_PATH}/${MEDIADEVICES_TOOLCHAIN_PREFIX}-${platform}
|
||||
# convert '-' to '_' since '_' is more common in library names
|
||||
export MEDIADEVICES_TARGET_PLATFORM=${platform//-/_}
|
||||
export MEDIADEVICES_TARGET_OS=$(echo $MEDIADEVICES_TARGET_PLATFORM | cut -d'_' -f1)
|
||||
export MEDIADEVICES_TARGET_ARCH=${platform//${MEDIADEVICES_TARGET_OS}-/}
|
||||
|
||||
echo "Building ${sub_build_dir}:"
|
||||
echo " PLATFORM : ${MEDIADEVICES_TARGET_PLATFORM}"
|
||||
echo " OS : ${MEDIADEVICES_TARGET_OS}"
|
||||
echo " ARCH : ${MEDIADEVICES_TARGET_ARCH}"
|
||||
echo " TOOLCHAIN_BIN : ${MEDIADEVICES_TOOLCHAIN_BIN}"
|
||||
echo ""
|
||||
|
||||
${sub_build} &> ${MEDIADEVICES_OUTPUT}
|
||||
done
|
||||
cd ${current_dir}
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# > 0 && $1 != "all" ]]; then
|
||||
MEDIADEVICES_TARGET_PLATFORMS=($1)
|
||||
fi
|
||||
|
||||
install_toolchains
|
||||
build
|
2
codec.go
2
codec.go
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"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
|
||||
|
11
dockerfiles/Makefile
Normal file
11
dockerfiles/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
dockerfiles := $(wildcard *.Dockerfile)
|
||||
supported_platforms := $(dockerfiles:.Dockerfile=)
|
||||
|
||||
.PHONY: all
|
||||
all: $(supported_platforms)
|
||||
|
||||
%: %.Dockerfile guard-MEDIADEVICES_DOCKER_OWNER guard-MEDIADEVICES_DOCKER_PREFIX
|
||||
docker build -t "$(MEDIADEVICES_DOCKER_OWNER)/$(MEDIADEVICES_DOCKER_PREFIX)-$@" -f "$<" .
|
||||
|
||||
guard-%:
|
||||
@if [ -z ${$*} ]; then echo "$* is a required environment variable"; exit 1; fi
|
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd $(dirname $0)
|
||||
|
||||
OWNER=lherman
|
||||
PREFIX=cross
|
||||
IMAGES=$(ls *.Dockerfile)
|
||||
|
||||
for image in ${IMAGES[@]}
|
||||
do
|
||||
tag=${OWNER}/cross-${image//.Dockerfile/}
|
||||
docker build -t "${tag}" -f "$image" .
|
||||
done
|
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
examples/.gitignore
vendored
1
examples/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
go.sum
|
8
examples/Makefile
Normal file
8
examples/Makefile
Normal file
@@ -0,0 +1,8 @@
|
||||
examples := $(shell find * -maxdepth 0 -type d)
|
||||
examples := $(filter-out internal,$(examples))
|
||||
|
||||
.PHONY: all $(examples)
|
||||
all: $(examples)
|
||||
|
||||
$(examples):
|
||||
cd $@ && go build -mod=mod
|
@@ -43,7 +43,7 @@ func main() {
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
|
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
pigo "github.com/esimov/pigo/core"
|
||||
"github.com/pion/mediadevices"
|
||||
_ "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/driver/camera" // This is required to register camera adapter
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
@@ -77,7 +77,7 @@ func main() {
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormatExact(frame.FormatUYVY)
|
||||
c.FrameFormat = prop.FrameFormatOneOf{frame.FormatI420, frame.FormatYUY2}
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
@@ -97,7 +97,7 @@ func main() {
|
||||
frame, release, err := videoReader.Read()
|
||||
must(err)
|
||||
|
||||
// Since we asked the frame format to be exactly YUY2 in GetUserMedia, we can guarantee that it must be YCbCr
|
||||
// Since we asked the frame format to be exactly I420/YUY2 in GetUserMedia, we can guarantee that it must be YCbCr
|
||||
if detectFace(frame.(*image.YCbCr)) {
|
||||
log.Println("Detect a face")
|
||||
}
|
||||
|
@@ -1,9 +1,37 @@
|
||||
module github.com/pion/mediadevices/examples
|
||||
|
||||
go 1.14
|
||||
go 1.21
|
||||
|
||||
// Please don't commit require entries of examples.
|
||||
// `git checkout master examples/go.mod` to revert this file.
|
||||
require github.com/pion/mediadevices v0.0.0
|
||||
require (
|
||||
github.com/esimov/pigo v1.4.6
|
||||
github.com/pion/mediadevices v0.0.0
|
||||
github.com/pion/webrtc/v4 v4.1.4
|
||||
)
|
||||
|
||||
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.7 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/logging v0.2.4 // 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.21 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.15 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.7 // 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.1.1 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/pion/mediadevices v0.0.0 => ../
|
||||
|
67
examples/go.sum
Normal file
67
examples/go.sum
Normal file
@@ -0,0 +1,67 @@
|
||||
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
|
||||
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y=
|
||||
github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.15 h1:F0I1zds+K/+37ZrzdADmx2Q44OFDOPRLhPnNTaUX9hk=
|
||||
github.com/pion/sdp/v3 v3.0.15/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
|
||||
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
|
||||
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
1
examples/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)
|
||||
}
|
@@ -41,7 +41,7 @@ func main() {
|
||||
|
||||
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
|
1
examples/vnc/.gitignore
vendored
Normal file
1
examples/vnc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
vnc
|
46
examples/vnc/README.md
Normal file
46
examples/vnc/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Instructions
|
||||
|
||||
### Install required codecs
|
||||
|
||||
In this example, we'll be using x264 and opus as our video and audio codecs. Therefore, we need to make sure that these codecs are installed within our system.
|
||||
|
||||
Installation steps:
|
||||
|
||||
* [x264](https://github.com/pion/mediadevices#x264)
|
||||
|
||||
### Download vnc example
|
||||
|
||||
```
|
||||
git clone https://github.com/pion/mediadevices.git
|
||||
```
|
||||
|
||||
#### Compile vnc example
|
||||
|
||||
```
|
||||
cd mediadevices/examples/vnc && go build
|
||||
```
|
||||
|
||||
### Open example page
|
||||
|
||||
[jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button
|
||||
|
||||
### Run the webrtc example with your browsers SessionDescription as stdin
|
||||
|
||||
In the jsfiddle the top textarea is your browser, copy that, and store the session description in an environment variable, `export SDP=<put_the_sdp_here>`
|
||||
|
||||
Run `echo $SDP | ./vnc`
|
||||
|
||||
In Windows
|
||||
|
||||
```powershell
|
||||
type sdp.txt| .\vnc.exe
|
||||
```
|
||||
### Input webrtc's SessionDescription into your browser
|
||||
|
||||
Copy the text that `./webrtc` just emitted and copy into second text area
|
||||
|
||||
### Hit 'Start Session' in jsfiddle, enjoy your video!
|
||||
|
||||
A video should start playing in your browser above the input boxes, and will continue playing until you close the application.
|
||||
|
||||
Congrats, you have used pion-MediaDevices! Now start building something cool
|
127
examples/vnc/main.go
Normal file
127
examples/vnc/main.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
"github.com/pion/mediadevices/pkg/driver/vncdriver"
|
||||
|
||||
"github.com/pion/mediadevices"
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
||||
// If you don't like x264, you can also use vpx by importing as below
|
||||
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
// or you can also use openh264 for alternative h264 implementation
|
||||
// "github.com/pion/mediadevices/pkg/codec/openh264"
|
||||
// or if you use a raspberry pi like, you can use mmal for using its hardware encoder
|
||||
// "github.com/pion/mediadevices/pkg/codec/mmal"
|
||||
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder
|
||||
// Note: If you don't have a camera or microphone or your adapters are not supported,
|
||||
// you can always swap your adapters with our dummy adapters below.
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
// _ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config := webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
}
|
||||
driver.GetManager().Register(
|
||||
vncdriver.NewVnc("127.0.0.1:5900"),
|
||||
driver.Info{Label: "VNC", DeviceType: driver.Camera, Priority: driver.PriorityLow},
|
||||
)
|
||||
// Wait for the offer to be pasted
|
||||
offer := webrtc.SessionDescription{}
|
||||
signal.Decode(signal.MustReadStdin(), &offer)
|
||||
|
||||
// Create a new RTCPeerConnection
|
||||
x264Params, err := x264.NewParams()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
x264Params.BitRate = 500_000 // 500kbps
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
codecSelector := mediadevices.NewCodecSelector(
|
||||
mediadevices.WithVideoEncoders(&x264Params),
|
||||
)
|
||||
|
||||
mediaEngine := webrtc.MediaEngine{}
|
||||
codecSelector.Populate(&mediaEngine)
|
||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine))
|
||||
peerConnection, err := api.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set the handler for ICE connection state
|
||||
// This will notify you when the peer has connected/disconnected
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||
fmt.Printf("Connection State has changed %s \n", connectionState.String())
|
||||
})
|
||||
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
|
||||
},
|
||||
Codec: codecSelector,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, track := range s.GetTracks() {
|
||||
track.OnEnded(func(err error) {
|
||||
fmt.Printf("Track (ID: %s) ended with error: %v\n",
|
||||
track.ID(), err)
|
||||
})
|
||||
|
||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the remote SessionDescription
|
||||
err = peerConnection.SetRemoteDescription(offer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create an answer
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create channel that is blocked until ICE Gathering is complete
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
||||
|
||||
// Sets the LocalDescription, and starts our UDP listeners
|
||||
err = peerConnection.SetLocalDescription(answer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Block until ICE Gathering is complete, disabling trickle ICE
|
||||
// we do this because we only can exchange one signaling message
|
||||
// in a production application you should exchange ICE Candidates via OnICECandidate
|
||||
<-gatherComplete
|
||||
|
||||
// Output the answer in base64 so we can paste it in browser
|
||||
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/pion/mediadevices/examples/internal/signal"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"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
|
||||
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
|
||||
@@ -71,7 +71,7 @@ func main() {
|
||||
|
||||
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
|
||||
Video: func(c *mediadevices.MediaTrackConstraints) {
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
|
||||
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
|
||||
c.Width = prop.Int(640)
|
||||
c.Height = prop.Int(480)
|
||||
},
|
||||
@@ -90,7 +90,7 @@ func main() {
|
||||
})
|
||||
|
||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||
webrtc.RtpTransceiverInit{
|
||||
webrtc.RTPTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
@@ -111,13 +111,23 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create channel that is blocked until ICE Gathering is complete
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
||||
|
||||
// Sets the LocalDescription, and starts our UDP listeners
|
||||
err = peerConnection.SetLocalDescription(answer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Block until ICE Gathering is complete, disabling trickle ICE
|
||||
// we do this because we only can exchange one signaling message
|
||||
// in a production application you should exchange ICE Candidates via OnICECandidate
|
||||
<-gatherComplete
|
||||
|
||||
// Output the answer in base64 so we can paste it in browser
|
||||
fmt.Println(signal.Encode(answer))
|
||||
fmt.Println(signal.Encode(*peerConnection.LocalDescription()))
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
||||
|
53
go.mod
53
go.mod
@@ -1,20 +1,43 @@
|
||||
module github.com/pion/mediadevices
|
||||
|
||||
go 1.13
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd // indirect
|
||||
github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
|
||||
github.com/gen2brain/malgo v0.10.27
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55 // indirect
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/rtp v1.6.2
|
||||
github.com/pion/webrtc/v3 v3.0.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/blackjack/webcam v0.6.1
|
||||
github.com/gen2brain/malgo v0.11.23
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.21
|
||||
github.com/pion/webrtc/v4 v4.1.4
|
||||
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.7 // 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.15 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.7 // 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.1.1 // 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
|
||||
)
|
||||
|
196
go.sum
196
go.sum
@@ -1,148 +1,76 @@
|
||||
github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd h1:u7K2oMFMd8APDV3fM1j2rO3U/XJf1g1qC3DDTKou8iM=
|
||||
github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
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/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
|
||||
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gen2brain/malgo v0.10.27 h1:KlNitZIO8V4W2VnjtTM8AGMy/XBb2pN+fnIB5bEps8E=
|
||||
github.com/gen2brain/malgo v0.10.27/go.mod h1:zHSUNZAXfCeNsZou0RtQ6Zk7gDYLIcKOrUWtAdksnEs=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
|
||||
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f h1:5hWo+DzJQSOBl6X+TDac0SPWffRonuRJ2///OYtYRT8=
|
||||
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f/go.mod h1:f8GY5V3lRzakvEyr49P7hHRYoHtPr8zvj/7JodCoRzw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55 h1:4BxFx5XCtXc+nFtXDGDW+Uu5sPtsAbvPh6RObj3fG9o=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
|
||||
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
|
||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
||||
github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI=
|
||||
github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
|
||||
github.com/pion/ice/v2 v2.0.14 h1:FxXxauyykf89SWAtkQCfnHkno6G8+bhRkNguSh9zU+4=
|
||||
github.com/pion/ice/v2 v2.0.14/go.mod h1:wqaUbOq5ObDNU5ox1hRsEst0rWfsKuH1zXjQFEWiZwM=
|
||||
github.com/pion/interceptor v0.0.8 h1:qsVJv9RF7mPq/RUnUV5iZCzxwGizO880FuiFKkEGQaE=
|
||||
github.com/pion/interceptor v0.0.8/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
|
||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
||||
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
||||
github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
|
||||
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
||||
github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U=
|
||||
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||
github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE=
|
||||
github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||
github.com/pion/sdp/v3 v3.0.3 h1:gJK9hk+JFD2NGIM1nXmqNCq1DkVaIZ9dlA3u3otnkaw=
|
||||
github.com/pion/sdp/v3 v3.0.3/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
||||
github.com/pion/srtp/v2 v2.0.0-rc.3 h1:1fPiK1nJlNyh235tSGgBnXrPc99wK1/D707f6ntb3qY=
|
||||
github.com/pion/srtp/v2 v2.0.0-rc.3/go.mod h1:S6J9oY6ahAXdU3ni4nUwhWTJuBfssFjPxoB0u41TBpY=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
||||
github.com/pion/transport v0.12.0 h1:UFmOBBZkTZ3LgvLRf/NGrfWdZEubcU6zkLU3PsA9YvU=
|
||||
github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
|
||||
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
|
||||
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
|
||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
||||
github.com/pion/webrtc/v3 v3.0.0 h1:/eTiY3NbfpKj5op8cqtCZlpTv9/yumd17YRinDNOUX0=
|
||||
github.com/pion/webrtc/v3 v3.0.0/go.mod h1:/xwKHOAk1Y8dspJcxMwuTtxpi8t/Gzks37iB3W6hNuM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y=
|
||||
github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.15 h1:F0I1zds+K/+37ZrzdADmx2Q44OFDOPRLhPnNTaUX9hk=
|
||||
github.com/pion/sdp/v3 v3.0.15/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs=
|
||||
github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
||||
github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M=
|
||||
github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
24
ioreader.go
24
ioreader.go
@@ -1,5 +1,7 @@
|
||||
package mediadevices
|
||||
|
||||
import "github.com/pion/mediadevices/pkg/codec"
|
||||
|
||||
type EncodedBuffer struct {
|
||||
Data []byte
|
||||
Samples uint32
|
||||
@@ -8,11 +10,13 @@ type EncodedBuffer struct {
|
||||
type EncodedReadCloser interface {
|
||||
Read() (EncodedBuffer, func(), error)
|
||||
Close() error
|
||||
codec.Controllable
|
||||
}
|
||||
|
||||
type encodedReadCloserImpl struct {
|
||||
readFn func() (EncodedBuffer, func(), error)
|
||||
closeFn func() error
|
||||
readFn func() (EncodedBuffer, func(), error)
|
||||
closeFn func() error
|
||||
controllerFn func() codec.EncoderController
|
||||
}
|
||||
|
||||
func (r *encodedReadCloserImpl) Read() (EncodedBuffer, func(), error) {
|
||||
@@ -23,9 +27,14 @@ func (r *encodedReadCloserImpl) Close() error {
|
||||
return r.closeFn()
|
||||
}
|
||||
|
||||
func (r *encodedReadCloserImpl) Controller() codec.EncoderController {
|
||||
return r.controllerFn()
|
||||
}
|
||||
|
||||
type encodedIOReadCloserImpl struct {
|
||||
readFn func([]byte) (int, error)
|
||||
closeFn func() error
|
||||
readFn func([]byte) (int, error)
|
||||
closeFn func() error
|
||||
controller func() codec.EncoderController
|
||||
}
|
||||
|
||||
func newEncodedIOReadCloserImpl(reader EncodedReadCloser) *encodedIOReadCloserImpl {
|
||||
@@ -48,7 +57,8 @@ func newEncodedIOReadCloserImpl(reader EncodedReadCloser) *encodedIOReadCloserIm
|
||||
encoded.Data = encoded.Data[n:]
|
||||
return n, nil
|
||||
},
|
||||
closeFn: reader.Close,
|
||||
closeFn: reader.Close,
|
||||
controller: reader.Controller,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +69,7 @@ func (r *encodedIOReadCloserImpl) Read(b []byte) (int, error) {
|
||||
func (r *encodedIOReadCloserImpl) Close() error {
|
||||
return r.closeFn()
|
||||
}
|
||||
|
||||
func (r *encodedIOReadCloserImpl) Controller() codec.EncoderController {
|
||||
return r.controller()
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/audiotest"
|
||||
_ "github.com/pion/mediadevices/pkg/driver/videotest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
@@ -97,37 +98,145 @@ func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) {
|
||||
t.Fatal("expect to get at least 1 property")
|
||||
}
|
||||
expectedProp := driver.Properties()[0]
|
||||
// Since this is a continuous value, bestConstraints should be set with the value that user specified
|
||||
expectedProp.FrameRate = 30.0
|
||||
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: prop.VideoConstraints{
|
||||
// By reducing the width from the driver by a tiny amount, this property should be chosen.
|
||||
// At the same time, we'll be able to find out if the return constraints will be properly set
|
||||
// to the best constraints.
|
||||
Width: prop.Int(expectedProp.Width - 1),
|
||||
Height: prop.Int(expectedProp.Width),
|
||||
FrameFormat: prop.FrameFormat(expectedProp.FrameFormat),
|
||||
FrameRate: prop.Float(30.0),
|
||||
},
|
||||
// By reducing the value from the driver by a tiny amount, this property should be chosen.
|
||||
// At the same time, we'll be able to find out if the return constraints will be properly set
|
||||
// to the best constraints.
|
||||
cases := map[string]struct {
|
||||
width, height int
|
||||
frameFormat frame.Format
|
||||
frameRate float32
|
||||
}{
|
||||
"DifferentWidth": {
|
||||
width: expectedProp.Width - 1,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentHeight": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height - 1,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentFrameFormat": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: frame.FormatI420,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
}
|
||||
|
||||
bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
|
||||
for name, c := range cases {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var vc prop.VideoConstraints
|
||||
|
||||
if c.frameRate >= 0 {
|
||||
vc = prop.VideoConstraints{
|
||||
Width: prop.Int(c.width),
|
||||
Height: prop.Int(c.height),
|
||||
FrameFormat: prop.FrameFormat(c.frameFormat),
|
||||
FrameRate: prop.Float(c.frameRate),
|
||||
}
|
||||
} else {
|
||||
// do not specify the framerate
|
||||
vc = prop.VideoConstraints{
|
||||
Width: prop.Int(c.width),
|
||||
Height: prop.Int(c.height),
|
||||
FrameFormat: prop.FrameFormat(c.frameFormat),
|
||||
}
|
||||
}
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: vc,
|
||||
},
|
||||
}
|
||||
|
||||
bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if driver != bestDriver {
|
||||
t.Fatal("best driver is not expected")
|
||||
}
|
||||
|
||||
s := bestConstraints.selectedMedia
|
||||
if s.Width != expectedProp.Width ||
|
||||
s.Height != expectedProp.Height ||
|
||||
s.FrameFormat != expectedProp.FrameFormat ||
|
||||
s.FrameRate != expectedProp.FrameRate {
|
||||
t.Fatalf("failed to return best constraints\nexpected:\n%v\n\ngot:\n%v", expectedProp, bestConstraints.selectedMedia)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSelectBestDriverConstraintsNoFit(t *testing.T) {
|
||||
filterFn := driver.FilterVideoRecorder()
|
||||
drivers := driver.GetManager().Query(filterFn)
|
||||
if len(drivers) == 0 {
|
||||
t.Fatal("expect to get at least 1 driver")
|
||||
}
|
||||
|
||||
driver := drivers[0]
|
||||
err := driver.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatal("expect to open driver successfully")
|
||||
}
|
||||
defer driver.Close()
|
||||
|
||||
if len(driver.Properties()) == 0 {
|
||||
t.Fatal("expect to get at least 1 property")
|
||||
}
|
||||
expectedProp := driver.Properties()[0]
|
||||
|
||||
cases := map[string]struct {
|
||||
width, height int
|
||||
frameFormat frame.Format
|
||||
frameRate float32
|
||||
}{
|
||||
"DifferentWidth": {
|
||||
width: expectedProp.Width - 1,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentHeight": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height - 1,
|
||||
frameFormat: expectedProp.FrameFormat,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
"DifferentFrameFormat": {
|
||||
width: expectedProp.Width,
|
||||
height: expectedProp.Height,
|
||||
frameFormat: frame.FormatI420,
|
||||
frameRate: expectedProp.FrameRate,
|
||||
},
|
||||
}
|
||||
|
||||
if driver != bestDriver {
|
||||
t.Fatal("best driver is not expected")
|
||||
}
|
||||
for name, c := range cases {
|
||||
c := c
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wantConstraints := MediaTrackConstraints{
|
||||
MediaConstraints: prop.MediaConstraints{
|
||||
VideoConstraints: prop.VideoConstraints{
|
||||
Width: prop.IntExact(c.width),
|
||||
Height: prop.IntExact(c.height),
|
||||
FrameFormat: prop.FrameFormatExact(c.frameFormat),
|
||||
FrameRate: prop.FloatExact(c.frameRate),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := bestConstraints.selectedMedia
|
||||
if s.Width != expectedProp.Width ||
|
||||
s.Height != expectedProp.Height ||
|
||||
s.FrameFormat != expectedProp.FrameFormat ||
|
||||
s.FrameRate != expectedProp.FrameRate {
|
||||
t.Fatalf("failed to return best constraints\nexpected:\n%v\n\ngot:\n%v", expectedProp, bestConstraints.selectedMedia)
|
||||
_, _, err := selectBestDriver(filterFn, wantConstraints)
|
||||
if err == nil {
|
||||
t.Fatal("expect to not find a driver that fits the constraints")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package mediadevices
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// MediaStream is an interface that represents a collection of existing tracks.
|
||||
|
@@ -2,9 +2,11 @@ package mediadevices
|
||||
|
||||
import (
|
||||
"io"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
type mockMediaStreamTrack struct {
|
||||
@@ -19,6 +21,10 @@ func (track *mockMediaStreamTrack) StreamID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) RID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -57,6 +63,10 @@ func (track *mockMediaStreamTrack) NewEncodedIOReader(codecName string) (io.Read
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (track *mockMediaStreamTrack) EncoderController() codec.EncoderController {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMediaStreamFilters(t *testing.T) {
|
||||
audioTracks := []Track{
|
||||
&mockMediaStreamTrack{AudioInput},
|
||||
@@ -84,13 +94,7 @@ func TestMediaStreamFilters(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, a := range actual {
|
||||
found := false
|
||||
for _, e := range expected {
|
||||
if e == a {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
found := slices.Contains(expected, a)
|
||||
|
||||
if !found {
|
||||
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
||||
|
@@ -27,6 +27,7 @@
|
||||
#define MAX_DEVICES 8
|
||||
#define MAX_PROPERTIES 64
|
||||
#define MAX_DEVICE_UID_CHARS 64
|
||||
#define MAX_DEVICE_NAME_CHARS 64
|
||||
|
||||
typedef const char* STATUS;
|
||||
static STATUS STATUS_OK = (STATUS) NULL;
|
||||
@@ -45,7 +46,8 @@ typedef enum AVBindMediaType {
|
||||
typedef enum AVBindFrameFormat {
|
||||
AVBindFrameFormatI420,
|
||||
AVBindFrameFormatNV21,
|
||||
AVBindFrameFormatYUY2,
|
||||
AVBindFrameFormatNV12,
|
||||
AVBindFrameFormatYUYV,
|
||||
AVBindFrameFormatUYVY,
|
||||
} AVBindFrameFormat;
|
||||
|
||||
@@ -64,6 +66,7 @@ typedef struct AVBindSession AVBindSession, *PAVBindSession;
|
||||
|
||||
typedef struct {
|
||||
char uid[MAX_DEVICE_UID_CHARS + 1];
|
||||
char name[MAX_DEVICE_NAME_CHARS + 1];
|
||||
} AVBindDevice, *PAVBindDevice;
|
||||
|
||||
// AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static
|
||||
|
@@ -1,17 +1,17 @@
|
||||
// MIT License
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2019-2020 Pion
|
||||
//
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -46,6 +46,8 @@
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
static NSString *const UnrecognizedMacOSVersionException = @"UnrecognizedMacOSVersionException";
|
||||
|
||||
@interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
|
||||
|
||||
@property (readonly) AVBindDataCallback mCallback;
|
||||
@@ -74,32 +76,61 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
|
||||
!CMSampleBufferIsValid(sampleBuffer) ||
|
||||
!CMSampleBufferDataIsReady(sampleBuffer)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
|
||||
if (imageBuffer == NULL) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
imageBuffer = CVBufferRetain(imageBuffer);
|
||||
|
||||
CVBufferRetain(imageBuffer);
|
||||
CVReturn ret =
|
||||
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (ret != kCVReturnSuccess) {
|
||||
return;
|
||||
CVBufferRelease(imageBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t heightY = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
|
||||
size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
|
||||
|
||||
size_t heightUV = CVPixelBufferGetHeightOfPlane(imageBuffer, 1);
|
||||
size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
|
||||
|
||||
int len = (int)((heightY * bytesPerRowY) + (2 * heightUV * bytesPerRowUV));
|
||||
void *buf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
||||
_mCallback(_mPUserData, buf, len);
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
|
||||
// Handle NV12 special case
|
||||
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer);
|
||||
if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
|
||||
// Get actual dimensions of image (without padding)
|
||||
size_t width = CVPixelBufferGetWidth(imageBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(imageBuffer);
|
||||
size_t totalSize = /*Y plane*/ width * height + /*UV plane*/ width * height / 2;
|
||||
|
||||
size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
|
||||
size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
|
||||
|
||||
void *mergedBuffer = malloc(totalSize);
|
||||
if (!mergedBuffer) {
|
||||
NSLog(@"Failed to allocate memory for merged buffer");
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
CVBufferRelease(imageBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate data where we know it should end to strip padding
|
||||
void *yPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
memcpy(mergedBuffer + row * width, yPlaneBuf + row * bytesPerRowY, width);
|
||||
}
|
||||
|
||||
void *uvPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
|
||||
for (size_t row = 0; row < height / 2; ++row) {
|
||||
memcpy(mergedBuffer + width * height + row * width, uvPlaneBuf + row * bytesPerRowUV, width);
|
||||
}
|
||||
|
||||
_mCallback(_mPUserData, mergedBuffer, (int)totalSize);
|
||||
free(mergedBuffer);
|
||||
} else {
|
||||
void *buf = CVPixelBufferGetBaseAddress(imageBuffer);
|
||||
size_t dataSize = CVPixelBufferGetDataSize(imageBuffer);
|
||||
_mCallback(_mPUserData, buf, (int)dataSize);
|
||||
}
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
CVBufferRelease(imageBuffer);
|
||||
}
|
||||
|
||||
@@ -133,13 +164,21 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
|
||||
STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
// Useful mapping reference from ffmpeg:
|
||||
// https://github.com/FFmpeg/FFmpeg/blob/c810a9502cebe32e1dd08ee3d0d17053dde44aa9/libavdevice/avfoundation.m#L53-L80
|
||||
switch (format) {
|
||||
case AVBindFrameFormatNV21:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
|
||||
case AVBindFrameFormatI420:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8Planar;
|
||||
break;
|
||||
case AVBindFrameFormatNV12:
|
||||
*pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
|
||||
break;
|
||||
case AVBindFrameFormatUYVY:
|
||||
*pFourCC = kCVPixelFormatType_422YpCbCr8;
|
||||
break;
|
||||
case AVBindFrameFormatYUYV:
|
||||
*pFourCC = kCVPixelFormatType_422YpCbCr8_yuvs;
|
||||
break;
|
||||
// TODO: Add the rest of frame formats
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
@@ -150,15 +189,22 @@ STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
|
||||
STATUS frameFormatFromFourCC(FourCharCode fourCC, AVBindFrameFormat *pFormat) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
switch (fourCC) {
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
*pFormat = AVBindFrameFormatNV21;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8:
|
||||
*pFormat = AVBindFrameFormatUYVY;
|
||||
break;
|
||||
case kCVPixelFormatType_420YpCbCr8Planar:
|
||||
*pFormat = AVBindFrameFormatI420;
|
||||
break;
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
|
||||
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
|
||||
*pFormat = AVBindFrameFormatNV12;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8:
|
||||
*pFormat = AVBindFrameFormatUYVY;
|
||||
break;
|
||||
case kCVPixelFormatType_422YpCbCr8_yuvs:
|
||||
*pFormat = AVBindFrameFormatYUYV;
|
||||
break;
|
||||
// TODO: Add the rest of frame formats
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
default:
|
||||
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
|
||||
}
|
||||
return retStatus;
|
||||
}
|
||||
@@ -170,34 +216,53 @@ STATUS AVBindDevices(AVBindMediaType mediaType, PAVBindDevice *ppDevices, int *p
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(mediaType == AVBindMediaTypeVideo || mediaType == AVBindMediaTypeAudio, STATUS_UNSUPPORTED_MEDIA_TYPE);
|
||||
CHK(ppDevices != NULL && pLen != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
PAVBindDevice pDevice;
|
||||
AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio;
|
||||
NSArray *refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeBuiltInMicrophone,
|
||||
AVCaptureDeviceTypeExternalUnknown
|
||||
];
|
||||
|
||||
NSArray *refAllTypes;
|
||||
#if defined(MAC_OS_VERSION_14_0)
|
||||
if (@available(macOS 14.0, *)) {
|
||||
refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeMicrophone,
|
||||
AVCaptureDeviceTypeExternal,
|
||||
];
|
||||
} else {
|
||||
@throw [NSException exceptionWithName:UnrecognizedMacOSVersionException
|
||||
reason:@"Unrecognized or unsupported macOS version detected."
|
||||
userInfo:nil];
|
||||
}
|
||||
#else
|
||||
refAllTypes = @[
|
||||
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||
AVCaptureDeviceTypeBuiltInMicrophone,
|
||||
AVCaptureDeviceTypeExternalUnknown,
|
||||
];
|
||||
#endif
|
||||
|
||||
AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession
|
||||
discoverySessionWithDeviceTypes: refAllTypes
|
||||
mediaType: _mediaType
|
||||
position: AVCaptureDevicePositionUnspecified];
|
||||
|
||||
discoverySessionWithDeviceTypes: refAllTypes
|
||||
mediaType: _mediaType
|
||||
position: AVCaptureDevicePositionUnspecified];
|
||||
|
||||
int i = 0;
|
||||
for (AVCaptureDevice *refDevice in refSession.devices) {
|
||||
if (i >= MAX_DEVICES) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
pDevice = devices + i;
|
||||
strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS);
|
||||
pDevice->uid[MAX_DEVICE_UID_CHARS] = '\0';
|
||||
strncpy(pDevice->name, refDevice.localizedName.UTF8String, MAX_DEVICE_NAME_CHARS);
|
||||
pDevice->name[MAX_DEVICE_NAME_CHARS] = '\0';
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
*ppDevices = devices;
|
||||
*pLen = i;
|
||||
|
||||
|
||||
cleanup:
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
@@ -217,7 +282,7 @@ STATUS AVBindSessionInit(AVBindDevice device, PAVBindSession *ppSessionResult) {
|
||||
pSession->device = device;
|
||||
pSession->refCaptureSession = NULL;
|
||||
*ppSessionResult = pSession;
|
||||
|
||||
|
||||
cleanup:
|
||||
return retStatus;
|
||||
}
|
||||
@@ -244,15 +309,15 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
STATUS retStatus = STATUS_OK;
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(pSession != NULL && dataCallback != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
AVCaptureDeviceInput *refInput;
|
||||
NSError *refErr = NULL;
|
||||
NSString *refUID = [NSString stringWithUTF8String: pSession->device.uid];
|
||||
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refUID];
|
||||
|
||||
|
||||
refInput = [[AVCaptureDeviceInput alloc] initWithDevice: refDevice error: &refErr];
|
||||
CHK(refErr == NULL, STATUS_DEVICE_INIT_FAILED);
|
||||
|
||||
|
||||
AVCaptureSession *refCaptureSession = [[AVCaptureSession alloc] init];
|
||||
refCaptureSession.sessionPreset = AVCaptureSessionPresetMedium;
|
||||
[refCaptureSession addInput: refInput];
|
||||
@@ -261,7 +326,7 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
VideoDataDelegate *pDelegate = [[VideoDataDelegate alloc]
|
||||
init: dataCallback
|
||||
withUserData: pUserData];
|
||||
|
||||
|
||||
AVCaptureVideoDataOutput *pOutput = [[AVCaptureVideoDataOutput alloc] init];
|
||||
FourCharCode fourCC;
|
||||
CHK_STATUS(frameFormatToFourCC(property.frameFormat, &fourCC));
|
||||
@@ -279,10 +344,10 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
|
||||
} else {
|
||||
// TODO: implement audio pipeline
|
||||
}
|
||||
|
||||
|
||||
pSession->refCaptureSession = [refCaptureSession retain];
|
||||
[refCaptureSession startRunning];
|
||||
|
||||
|
||||
cleanup:
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
@@ -293,20 +358,30 @@ STATUS AVBindSessionClose(PAVBindSession pSession) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
CHK(pSession != NULL, STATUS_NULL_ARG);
|
||||
CHK(pSession->refCaptureSession != NULL, STATUS_OK);
|
||||
|
||||
|
||||
[pSession->refCaptureSession stopRunning];
|
||||
[pSession->refCaptureSession release];
|
||||
pSession->refCaptureSession = NULL;
|
||||
|
||||
|
||||
cleanup:
|
||||
return retStatus;
|
||||
}
|
||||
|
||||
static NSString* FourCCString(FourCharCode code) {
|
||||
NSString *result = [NSString stringWithFormat:@"%c%c%c%c",
|
||||
(code >> 24) & 0xff,
|
||||
(code >> 16) & 0xff,
|
||||
(code >> 8) & 0xff,
|
||||
code & 0xff];
|
||||
NSCharacterSet *characterSet = [NSCharacterSet whitespaceCharacterSet];
|
||||
return [result stringByTrimmingCharactersInSet:characterSet];
|
||||
}
|
||||
|
||||
STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *ppProperties, int *pLen) {
|
||||
STATUS retStatus = STATUS_OK;
|
||||
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
|
||||
CHK(pSession != NULL && ppProperties != NULL && pLen != NULL, STATUS_NULL_ARG);
|
||||
|
||||
|
||||
NSString *refDeviceUID = [NSString stringWithUTF8String: pSession->device.uid];
|
||||
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refDeviceUID];
|
||||
FourCharCode fourCC;
|
||||
@@ -319,15 +394,17 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
|
||||
for (AVCaptureDeviceFormat *refFormat in refDevice.formats) {
|
||||
// TODO: Probably gives a warn to the user
|
||||
if (len >= MAX_PROPERTIES) {
|
||||
NSLog(@"[WARNING] skipping the rest of properties due to MAX_PROPERTIES");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if ([refFormat.mediaType isEqual:AVMediaTypeVideo]) {
|
||||
fourCC = CMFormatDescriptionGetMediaSubType(refFormat.formatDescription);
|
||||
if (frameFormatFromFourCC(fourCC, &pProperty->frameFormat) != STATUS_OK) {
|
||||
NSLog(@"[WARNING] skipping %@ %dx%d since it's not supported", FourCCString(fourCC), videoDimensions.width, videoDimensions.height);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
videoFormat = (CMVideoFormatDescriptionRef) refFormat.formatDescription;
|
||||
videoDimensions = CMVideoFormatDescriptionGetDimensions(videoFormat);
|
||||
pProperty->height = videoDimensions.height;
|
||||
@@ -335,16 +412,16 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
|
||||
} else {
|
||||
// TODO: Get audio properties
|
||||
}
|
||||
|
||||
|
||||
pProperty++;
|
||||
len++;
|
||||
}
|
||||
|
||||
|
||||
*ppProperties = pSession->properties;
|
||||
*pLen = len;
|
||||
|
||||
|
||||
cleanup:
|
||||
|
||||
|
||||
[refPool drain];
|
||||
return retStatus;
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package avfoundation
|
||||
|
||||
// extern void onData(void*, void*, int);
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
@@ -18,11 +17,10 @@ type handleID int
|
||||
|
||||
//export onData
|
||||
func onData(userData unsafe.Pointer, buf unsafe.Pointer, length C.int) {
|
||||
data := C.GoBytes(buf, length)
|
||||
|
||||
handleNum := (*C.int)(userData)
|
||||
cb, ok := lookup(handleID(*handleNum))
|
||||
if ok {
|
||||
data := C.GoBytes(buf, length)
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
|
@@ -11,8 +11,10 @@ package avfoundation
|
||||
// }
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
@@ -32,6 +34,7 @@ type Device struct {
|
||||
// UID is a unique identifier for a device
|
||||
UID string
|
||||
cDevice C.AVBindDevice
|
||||
Name string
|
||||
}
|
||||
|
||||
func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
||||
@@ -40,8 +43,10 @@ func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
|
||||
return C.AVBindFrameFormatI420, true
|
||||
case frame.FormatNV21:
|
||||
return C.AVBindFrameFormatNV21, true
|
||||
case frame.FormatYUY2:
|
||||
return C.AVBindFrameFormatYUY2, true
|
||||
case frame.FormatNV12:
|
||||
return C.AVBindFrameFormatNV12, true
|
||||
case frame.FormatYUYV:
|
||||
return C.AVBindFrameFormatYUYV, true
|
||||
case frame.FormatUYVY:
|
||||
return C.AVBindFrameFormatUYVY, true
|
||||
default:
|
||||
@@ -55,8 +60,10 @@ func frameFormatFromAVBind(f C.AVBindFrameFormat) (frame.Format, bool) {
|
||||
return frame.FormatI420, true
|
||||
case C.AVBindFrameFormatNV21:
|
||||
return frame.FormatNV21, true
|
||||
case C.AVBindFrameFormatYUY2:
|
||||
return frame.FormatYUY2, true
|
||||
case C.AVBindFrameFormatNV12:
|
||||
return frame.FormatNV12, true
|
||||
case C.AVBindFrameFormatYUYV:
|
||||
return frame.FormatYUYV, true
|
||||
case C.AVBindFrameFormatUYVY:
|
||||
return frame.FormatUYVY, true
|
||||
default:
|
||||
@@ -81,6 +88,7 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
||||
for i := range devices {
|
||||
devices[i].UID = C.GoString(&cDevices[i].uid[0])
|
||||
devices[i].cDevice = cDevices[i]
|
||||
devices[i].Name = C.GoString(&cDevices[i].name[0])
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
@@ -89,9 +97,13 @@ func Devices(mediaType MediaType) ([]Device, error) {
|
||||
// ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the
|
||||
// the underlying callback can be retrieved by calling Read.
|
||||
type ReadCloser struct {
|
||||
dataChan chan []byte
|
||||
id handleID
|
||||
onClose func()
|
||||
dataChan chan []byte
|
||||
id handleID
|
||||
onClose func()
|
||||
cancelCtx context.Context
|
||||
cancelFunc func()
|
||||
closeWG sync.WaitGroup
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newReadCloser(onClose func()) *ReadCloser {
|
||||
@@ -99,12 +111,25 @@ func newReadCloser(onClose func()) *ReadCloser {
|
||||
rc.dataChan = make(chan []byte, 1)
|
||||
rc.onClose = onClose
|
||||
rc.id = register(rc.dataCb)
|
||||
cancelCtx, cancelFunc := context.WithCancel(context.Background())
|
||||
rc.cancelCtx = cancelCtx
|
||||
rc.cancelFunc = cancelFunc
|
||||
return &rc
|
||||
}
|
||||
|
||||
func (rc *ReadCloser) dataCb(data []byte) {
|
||||
rc.closeWG.Add(1)
|
||||
defer rc.closeWG.Done()
|
||||
|
||||
// TODO: add a policy for slow reader
|
||||
rc.dataChan <- data
|
||||
if rc.cancelCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
// Use the Done channel to avoid waiting for new data from closed camera
|
||||
case <-rc.cancelCtx.Done():
|
||||
case rc.dataChan <- data:
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads raw data, the format is determined by the media type and property:
|
||||
@@ -120,17 +145,28 @@ func (rc *ReadCloser) Read() ([]byte, func(), error) {
|
||||
|
||||
// Close closes the capturing session, and no data will flow anymore
|
||||
func (rc *ReadCloser) Close() {
|
||||
rc.lock.Lock()
|
||||
defer rc.lock.Unlock()
|
||||
|
||||
if rc.cancelCtx.Err() != nil {
|
||||
return // already closed
|
||||
}
|
||||
|
||||
if rc.onClose != nil {
|
||||
rc.onClose()
|
||||
}
|
||||
close(rc.dataChan)
|
||||
rc.cancelFunc()
|
||||
unregister(rc.id)
|
||||
rc.closeWG.Wait()
|
||||
close(rc.dataChan)
|
||||
}
|
||||
|
||||
// Session represents a capturing session.
|
||||
type Session struct {
|
||||
device Device
|
||||
cSession C.PAVBindSession
|
||||
lock sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewSession creates a new capturing session
|
||||
@@ -148,6 +184,13 @@ func NewSession(device Device) (*Session, error) {
|
||||
|
||||
// Close stops capturing session and frees up resources
|
||||
func (session *Session) Close() error {
|
||||
session.lock.Lock()
|
||||
defer session.lock.Unlock()
|
||||
if session.closed {
|
||||
return nil
|
||||
}
|
||||
session.closed = true
|
||||
|
||||
if session.cSession == nil {
|
||||
return nil
|
||||
}
|
||||
|
48
pkg/codec/bitrate_tracker.go
Normal file
48
pkg/codec/bitrate_tracker.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BitrateTracker struct {
|
||||
windowSize time.Duration
|
||||
buffer []int
|
||||
times []time.Time
|
||||
}
|
||||
|
||||
func NewBitrateTracker(windowSize time.Duration) *BitrateTracker {
|
||||
return &BitrateTracker{
|
||||
windowSize: windowSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (bt *BitrateTracker) AddFrame(sizeBytes int, timestamp time.Time) {
|
||||
bt.buffer = append(bt.buffer, sizeBytes)
|
||||
bt.times = append(bt.times, timestamp)
|
||||
|
||||
// Remove old entries outside the window
|
||||
cutoff := timestamp.Add(-bt.windowSize)
|
||||
i := 0
|
||||
for ; i < len(bt.times); i++ {
|
||||
if bt.times[i].After(cutoff) {
|
||||
break
|
||||
}
|
||||
}
|
||||
bt.buffer = bt.buffer[i:]
|
||||
bt.times = bt.times[i:]
|
||||
}
|
||||
|
||||
func (bt *BitrateTracker) GetBitrate() float64 {
|
||||
if len(bt.times) < 2 {
|
||||
return 0
|
||||
}
|
||||
totalBytes := 0
|
||||
for _, b := range bt.buffer {
|
||||
totalBytes += b
|
||||
}
|
||||
duration := bt.times[len(bt.times)-1].Sub(bt.times[0]).Seconds()
|
||||
if duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(totalBytes*8) / duration // bits per second
|
||||
}
|
19
pkg/codec/bitrate_tracker_test.go
Normal file
19
pkg/codec/bitrate_tracker_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBitrateTracker(t *testing.T) {
|
||||
packetSize := 1000
|
||||
bt := NewBitrateTracker(time.Second)
|
||||
bt.AddFrame(packetSize, time.Now())
|
||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*100))
|
||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*999))
|
||||
eps := float64(packetSize*8) / 10
|
||||
if got, want := bt.GetBitrate(), float64(packetSize*8)*3; math.Abs(got-want) > eps {
|
||||
t.Fatalf("GetBitrate() = %v, want %v (|diff| <= %v)", got, want, eps)
|
||||
}
|
||||
}
|
@@ -1,18 +1,23 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/rtp/codecs"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future.
|
||||
type RTPCodec struct {
|
||||
webrtc.RTPCodecParameters
|
||||
rtp.Payloader
|
||||
|
||||
// Latency of static frame size codec.
|
||||
Latency time.Duration
|
||||
}
|
||||
|
||||
// NewRTPH264Codec is a helper to create an H264 codec
|
||||
@@ -32,6 +37,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
|
||||
func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
@@ -66,6 +88,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
|
||||
func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
|
||||
return &RTPCodec{
|
||||
@@ -107,15 +146,43 @@ type VideoEncoderBuilder interface {
|
||||
BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error)
|
||||
}
|
||||
|
||||
// ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame
|
||||
// ReadCloser is an io.ReadCloser with a controller
|
||||
type ReadCloser interface {
|
||||
Read() (b []byte, release func(), err error)
|
||||
Close() error
|
||||
Controllable
|
||||
}
|
||||
|
||||
// EncoderController is the interface allowing to control the encoder behaviour after it's initialisation.
|
||||
// It will possibly have common control method in the future.
|
||||
// A controller can have optional methods represented by *Controller interfaces
|
||||
type EncoderController any
|
||||
|
||||
// Controllable is a interface representing a encoder which can be controlled
|
||||
// after it's initialisation with an EncoderController
|
||||
type Controllable interface {
|
||||
Controller() EncoderController
|
||||
}
|
||||
|
||||
// KeyFrameController is a interface representing an encoder that can be forced to produce key frame on demand
|
||||
type KeyFrameController interface {
|
||||
EncoderController
|
||||
// ForceKeyFrame forces the next frame to be a keyframe, aka intra-frame.
|
||||
ForceKeyFrame() error
|
||||
}
|
||||
|
||||
// BitRateController is a interface representing an encoder which can have a variable bit rate
|
||||
type BitRateController interface {
|
||||
EncoderController
|
||||
// SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted
|
||||
// but this also means that the quality will also be lower.
|
||||
SetBitRate(int) error
|
||||
// ForceKeyFrame forces the next frame to be a keyframe, aka intra-frame.
|
||||
ForceKeyFrame() error
|
||||
}
|
||||
|
||||
type QPController interface {
|
||||
EncoderController
|
||||
// DynamicQPControl adjusts the QP of the encoder based on the current and target bitrate
|
||||
DynamicQPControl(currentBitrate int, targetBitrate int) error
|
||||
}
|
||||
|
||||
// BaseParams represents an codec's encoding properties
|
||||
|
159
pkg/codec/internal/codectest/codectest.go
Normal file
159
pkg/codec/internal/codectest/codectest.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Package codectest provides shared test for codec implementations.
|
||||
package codectest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices/pkg/wave"
|
||||
)
|
||||
|
||||
func assertNoPanic(t *testing.T, fn func() error, msg string) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("panic: %v: %s", r, msg)
|
||||
}
|
||||
}()
|
||||
return fn()
|
||||
}
|
||||
|
||||
func AudioEncoderSimpleReadTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media, w wave.Audio) {
|
||||
var eof bool
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
if eof {
|
||||
return nil, nil, io.EOF
|
||||
}
|
||||
return w, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 16; i++ {
|
||||
b, release, err := enc.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
t.Fatal("Encoded frame is empty")
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
eof = true
|
||||
if _, _, err := enc.Read(); err != io.EOF {
|
||||
t.Fatalf("Expected EOF, got %v", err)
|
||||
}
|
||||
|
||||
if err := enc.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderSimpleReadTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media, img image.Image) {
|
||||
var eof bool
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
if eof {
|
||||
return nil, nil, io.EOF
|
||||
}
|
||||
return img, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 16; i++ {
|
||||
b, release, err := enc.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
t.Errorf("Encoded frame is empty (%d)", i)
|
||||
}
|
||||
release()
|
||||
}
|
||||
|
||||
eof = true
|
||||
if _, _, err := enc.Read(); err != io.EOF {
|
||||
t.Fatalf("Expected EOF, got %v", err)
|
||||
}
|
||||
|
||||
if err := enc.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AudioEncoderCloseTwiceTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media) {
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
return nil, nil, io.EOF
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on first Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, enc.Close, "on second Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderCloseTwiceTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media) {
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
return nil, nil, io.EOF
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on first Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, enc.Close, "on second Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AudioEncoderReadAfterCloseTest(t *testing.T, c codec.AudioEncoderBuilder, p prop.Media, w wave.Audio) {
|
||||
enc, err := c.BuildAudioEncoder(audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
||||
return w, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, func() error {
|
||||
_, _, err := enc.Read()
|
||||
return err
|
||||
}, "on Read()"); err != io.EOF {
|
||||
t.Fatalf("Expected: %v, got: %v", io.EOF, err)
|
||||
}
|
||||
}
|
||||
|
||||
func VideoEncoderReadAfterCloseTest(t *testing.T, c codec.VideoEncoderBuilder, p prop.Media, img image.Image) {
|
||||
enc, err := c.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
return img, nil, nil
|
||||
}), p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := assertNoPanic(t, enc.Close, "on Close()"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := assertNoPanic(t, func() error {
|
||||
_, _, err := enc.Read()
|
||||
return err
|
||||
}, "on Read()"); err != io.EOF {
|
||||
t.Fatalf("Expected: %v, got: %v", io.EOF, err)
|
||||
}
|
||||
}
|
@@ -64,10 +64,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
imgReal := img.(*image.YCbCr)
|
||||
var y, cb, cr C.Slice
|
||||
y.data = (*C.uchar)(&imgReal.Y[0])
|
||||
@@ -91,12 +92,8 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
}
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
func (e *encoder) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
|
84
pkg/codec/mmal/mmal_test.go
Normal file
84
pkg/codec/mmal/mmal_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package mmal
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderReadAfterCloseTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement bit rate control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement key frame control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
47
pkg/codec/openh264/Makefile
Normal file
47
pkg/codec/openh264/Makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
git_url := https://github.com/cisco/openh264.git
|
||||
version := v2.1.1
|
||||
src_root_dir := src
|
||||
lib_dir := lib
|
||||
include_dir := include/openh264
|
||||
lib_prefix := libopenh264
|
||||
src_dir := $(src_root_dir)/$(MEDIADEVICES_TARGET_PLATFORM)
|
||||
output_path := $(lib_dir)/$(lib_prefix)-$(MEDIADEVICES_TARGET_PLATFORM).a
|
||||
|
||||
# OS and Arch mapping to OpenH264 parameters
|
||||
ifeq (windows,$(MEDIADEVICES_TARGET_OS))
|
||||
os := mingw_nt
|
||||
else
|
||||
os := $(MEDIADEVICES_TARGET_OS)
|
||||
endif
|
||||
|
||||
ifneq (,$(findstring $(MEDIADEVICES_TARGET_ARCH),armv6 armv7 armv8))
|
||||
arch := arm
|
||||
else ifeq (x64,$(MEDIADEVICES_TARGET_ARCH))
|
||||
arch := x86_64
|
||||
else
|
||||
arch := $(MEDIADEVICES_TARGET_ARCH)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: guard-MEDIADEVICES_TARGET_PLATFORM guard-MEDIADEVICES_TARGET_PLATFORM \
|
||||
guard-MEDIADEVICES_TARGET_OS guard-MEDIADEVICES_TARGET_ARCH \
|
||||
$(output_path) headers
|
||||
|
||||
headers: | $(src_dir) $(include_dir)
|
||||
@cp $(src_dir)/codec/api/svc/*.h $(include_dir)
|
||||
|
||||
$(output_path): $(src_dir)/$(lib_prefix).a | $(lib_dir)
|
||||
@cp $< $@
|
||||
|
||||
$(src_dir)/$(lib_prefix).a: | $(src_dir)
|
||||
$(MEDIADEVICES_TOOLCHAIN_BIN) make --directory=$(src_dir) $(lib_prefix).a \
|
||||
OS=$(os) ARCH=$(arch)
|
||||
|
||||
$(src_dir): | $(src_root_dir)
|
||||
git clone --depth=1 --branch=$(version) $(git_url) $@
|
||||
|
||||
$(src_root_dir) $(lib_dir) $(include_dir):
|
||||
@mkdir -p $@
|
||||
|
||||
guard-%:
|
||||
@if [ -z ${$*} ]; then echo "$* is a required environment variable"; exit 1; fi
|
@@ -21,29 +21,26 @@ Encoder *enc_new(const EncoderOptions opts, int *eresult) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// TODO: Remove hardcoded values
|
||||
params.iUsageType = CAMERA_VIDEO_REAL_TIME;
|
||||
params.iUsageType = opts.usage_type;
|
||||
params.iPicWidth = opts.width;
|
||||
params.iPicHeight = opts.height;
|
||||
params.iTargetBitrate = opts.target_bitrate;
|
||||
params.iMaxBitrate = opts.target_bitrate;
|
||||
params.iRCMode = RC_BITRATE_MODE;
|
||||
params.iRCMode = opts.rc_mode;
|
||||
params.fMaxFrameRate = opts.max_fps;
|
||||
params.bEnableFrameSkip = true;
|
||||
params.uiMaxNalSize = 0;
|
||||
params.uiIntraPeriod = 30;
|
||||
// set to 0, so that it'll automatically use multi threads when needed
|
||||
params.iMultipleThreadIdc = 0;
|
||||
params.bEnableFrameSkip = opts.enable_frame_skip;
|
||||
params.uiMaxNalSize = opts.max_nal_size;
|
||||
params.uiIntraPeriod = opts.intra_period;
|
||||
params.iMultipleThreadIdc = opts.multiple_thread_idc;
|
||||
// The base spatial layer 0 is the only one we use.
|
||||
params.sSpatialLayers[0].iVideoWidth = params.iPicWidth;
|
||||
params.sSpatialLayers[0].iVideoHeight = params.iPicHeight;
|
||||
params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate;
|
||||
params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate;
|
||||
params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate;
|
||||
// Single NAL unit mode
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = 1;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SIZELIMITED_SLICE;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = 12800;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = opts.slice_num;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = opts.slice_mode;
|
||||
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = opts.slice_size_constraint;
|
||||
|
||||
rv = engine->InitializeExt(¶ms);
|
||||
if (rv != 0) {
|
||||
@@ -72,6 +69,16 @@ void enc_free(Encoder *e, int *eresult) {
|
||||
free(e);
|
||||
}
|
||||
|
||||
void enc_set_bitrate(Encoder *e, int bitrate) {
|
||||
SEncParamExt encParamExt;
|
||||
e->engine->GetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
|
||||
encParamExt.iTargetBitrate=bitrate;
|
||||
encParamExt.iMaxBitrate=bitrate;
|
||||
encParamExt.sSpatialLayers[0].iSpatialBitrate = bitrate;
|
||||
encParamExt.sSpatialLayers[0].iMaxSpatialBitrate = bitrate;
|
||||
e->engine->SetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
|
||||
}
|
||||
|
||||
// There's a good reference from ffmpeg in using the encode_frame
|
||||
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
|
||||
Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
||||
@@ -80,12 +87,16 @@ Slice enc_encode(Encoder *e, Frame f, int *eresult) {
|
||||
SFrameBSInfo info = {0};
|
||||
Slice payload = {0};
|
||||
|
||||
if(e->force_key_frame == 1) {
|
||||
e->engine->ForceIntraFrame(true);
|
||||
e->force_key_frame = 0;
|
||||
}
|
||||
|
||||
pic.iPicWidth = f.width;
|
||||
pic.iPicHeight = f.height;
|
||||
pic.iColorFormat = videoFormatI420;
|
||||
// We always received I420 format
|
||||
pic.iStride[0] = pic.iPicWidth;
|
||||
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth / 2;
|
||||
pic.iStride[0] = f.ystride;
|
||||
pic.iStride[1] = pic.iStride[2] = f.cstride;
|
||||
pic.pData[0] = (unsigned char *)f.y;
|
||||
pic.pData[1] = (unsigned char *)f.u;
|
||||
pic.pData[2] = (unsigned char *)f.v;
|
||||
|
@@ -12,6 +12,8 @@ typedef struct Slice {
|
||||
|
||||
typedef struct Frame {
|
||||
void *y, *u, *v;
|
||||
int ystride;
|
||||
int cstride;
|
||||
int height;
|
||||
int width;
|
||||
} Frame;
|
||||
@@ -20,6 +22,15 @@ typedef struct EncoderOptions {
|
||||
int width, height;
|
||||
int target_bitrate;
|
||||
float max_fps;
|
||||
EUsageType usage_type;
|
||||
RC_MODES rc_mode;
|
||||
bool enable_frame_skip;
|
||||
unsigned int max_nal_size;
|
||||
unsigned int intra_period;
|
||||
int multiple_thread_idc;
|
||||
unsigned int slice_num;
|
||||
SliceModeEnum slice_mode;
|
||||
unsigned int slice_size_constraint;
|
||||
} EncoderOptions;
|
||||
|
||||
typedef struct Encoder {
|
||||
@@ -27,11 +38,13 @@ typedef struct Encoder {
|
||||
ISVCEncoder *engine;
|
||||
unsigned char *buff;
|
||||
int buff_size;
|
||||
int force_key_frame;
|
||||
} Encoder;
|
||||
|
||||
Encoder *enc_new(const EncoderOptions params, int *eresult);
|
||||
void enc_free(Encoder *e, int *eresult);
|
||||
Slice enc_encode(Encoder *e, Frame f, int *eresult);
|
||||
void enc_set_bitrate(Encoder *e, int bitrate);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@@ -1,36 +0,0 @@
|
||||
GIT_URL=https://github.com/cisco/openh264.git
|
||||
VERSION=v2.1.1
|
||||
SRC_DIR=src
|
||||
LIB_DIR=lib
|
||||
INCLUDE_DIR=include/openh264
|
||||
ROOT_DIR=${PWD}
|
||||
LIB_PREFIX=libopenh264
|
||||
|
||||
OS=${MEDIADEVICES_TARGET_OS}
|
||||
ARCH=${MEDIADEVICES_TARGET_ARCH}
|
||||
|
||||
case ${MEDIADEVICES_TARGET_OS} in
|
||||
windows)
|
||||
OS=mingw_nt
|
||||
;;
|
||||
esac
|
||||
|
||||
case ${MEDIADEVICES_TARGET_ARCH} in
|
||||
armv6 | armv7 | armv8)
|
||||
ARCH=arm
|
||||
;;
|
||||
x64)
|
||||
ARCH=x86_64
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p ${LIB_DIR} ${INCLUDE_DIR}
|
||||
|
||||
git clone --depth=1 --branch=${VERSION} ${GIT_URL} ${SRC_DIR}
|
||||
cd ${SRC_DIR}
|
||||
${MEDIADEVICES_TOOLCHAIN_BIN} make -j2 ${LIB_PREFIX}.a OS=${OS} ARCH=${ARCH}
|
||||
${MEDIADEVICES_TOOLCHAIN_BIN} echo $PATH
|
||||
mv ${LIB_PREFIX}.a ${ROOT_DIR}/${LIB_DIR}/${LIB_PREFIX}_${MEDIADEVICES_TARGET_PLATFORM}.a
|
||||
mkdir -p ${ROOT_DIR}/${INCLUDE_DIR}
|
||||
cp codec/api/svc/*.h ${ROOT_DIR}/${INCLUDE_DIR}
|
||||
make clean
|
@@ -4,12 +4,12 @@
|
||||
|
||||
#include "codec_app_def.h"
|
||||
|
||||
static const OpenH264Version g_stCodecVersion = {2, 1, 0, 2002};
|
||||
static const char* const g_strCodecVer = "OpenH264 version:2.1.0.2002";
|
||||
static const OpenH264Version g_stCodecVersion = {2, 1, 1, 2005};
|
||||
static const char* const g_strCodecVer = "OpenH264 version:2.1.1.2005";
|
||||
|
||||
#define OPENH264_MAJOR (2)
|
||||
#define OPENH264_MINOR (1)
|
||||
#define OPENH264_REVISION (0)
|
||||
#define OPENH264_RESERVED (2002)
|
||||
#define OPENH264_REVISION (1)
|
||||
#define OPENH264_RESERVED (2005)
|
||||
|
||||
#endif // CODEC_VER_H
|
||||
|
BIN
pkg/codec/openh264/lib/libopenh264-darwin-arm64.a
Normal file
BIN
pkg/codec/openh264/lib/libopenh264-darwin-arm64.a
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,10 +33,19 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser,
|
||||
|
||||
var rv C.int
|
||||
cEncoder := C.enc_new(C.EncoderOptions{
|
||||
width: C.int(p.Width),
|
||||
height: C.int(p.Height),
|
||||
target_bitrate: C.int(params.BitRate),
|
||||
max_fps: C.float(p.FrameRate),
|
||||
width: C.int(p.Width),
|
||||
height: C.int(p.Height),
|
||||
target_bitrate: C.int(params.BitRate),
|
||||
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)
|
||||
if err := errResult(rv); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
bounds := yuvImg.Bounds()
|
||||
var rv C.int
|
||||
s := C.enc_encode(e.engine, C.Frame{
|
||||
y: unsafe.Pointer(&yuvImg.Y[0]),
|
||||
u: unsafe.Pointer(&yuvImg.Cb[0]),
|
||||
v: unsafe.Pointer(&yuvImg.Cr[0]),
|
||||
height: C.int(bounds.Max.Y - bounds.Min.Y),
|
||||
width: C.int(bounds.Max.X - bounds.Min.X),
|
||||
y: unsafe.Pointer(&yuvImg.Y[0]),
|
||||
u: unsafe.Pointer(&yuvImg.Cb[0]),
|
||||
v: unsafe.Pointer(&yuvImg.Cr[0]),
|
||||
ystride: C.int(yuvImg.YStride),
|
||||
cstride: C.int(yuvImg.CStride),
|
||||
height: C.int(bounds.Max.Y - bounds.Min.Y),
|
||||
width: C.int(bounds.Max.X - bounds.Min.X),
|
||||
}, &rv)
|
||||
if err := errResult(rv); err != nil {
|
||||
return nil, func() {}, fmt.Errorf("failed in encoding: %v", err)
|
||||
@@ -79,18 +91,28 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, nil
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
e.engine.force_key_frame = C.int(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
func (e *encoder) SetBitRate(bitrate int) error {
|
||||
C.enc_set_bitrate(e.engine, C.int(bitrate))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.closed = true
|
||||
|
||||
var rv C.int
|
||||
|
@@ -4,9 +4,10 @@ package openh264
|
||||
|
||||
//#cgo CFLAGS: -I${SRCDIR}/include
|
||||
//#cgo CXXFLAGS: -I${SRCDIR}/include
|
||||
//#cgo linux,arm LDFLAGS: ${SRCDIR}/lib/libopenh264_linux_armv7.a
|
||||
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264_linux_arm64.a
|
||||
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264_linux_x64.a
|
||||
//#cgo darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264_darwin_x64.a
|
||||
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264_windows_x64.a -lssp
|
||||
//#cgo linux,arm LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-armv7.a
|
||||
//#cgo linux,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-arm64.a
|
||||
//#cgo linux,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-linux-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
|
||||
import "C"
|
||||
|
82
pkg/codec/openh264/openh264_test.go
Normal file
82
pkg/codec/openh264/openh264_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package openh264
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement bit rate control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderReadAfterCloseTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
@@ -1,5 +1,8 @@
|
||||
package openh264
|
||||
|
||||
// #include <openh264/codec_api.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
@@ -9,14 +12,62 @@ import (
|
||||
// Params stores libopenh264 specific encoding parameters.
|
||||
type Params struct {
|
||||
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.
|
||||
func NewParams() (Params, error) {
|
||||
return Params{
|
||||
BaseParams: codec.BaseParams{
|
||||
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
|
||||
}
|
||||
|
||||
|
31
pkg/codec/opus/Makefile
Normal file
31
pkg/codec/opus/Makefile
Normal file
@@ -0,0 +1,31 @@
|
||||
git_url := https://github.com/xiph/opus.git
|
||||
version := v1.3.1
|
||||
src_root_dir := src
|
||||
lib_dir := lib
|
||||
include_dir := include
|
||||
lib_prefix := libopus
|
||||
src_dir := $(src_root_dir)/$(MEDIADEVICES_TARGET_PLATFORM)
|
||||
output_path := $(lib_dir)/$(lib_prefix)-$(MEDIADEVICES_TARGET_PLATFORM).a
|
||||
|
||||
.PHONY: all
|
||||
all: guard-MEDIADEVICES_TARGET_PLATFORM guard-MEDIADEVICES_TOOLCHAIN_BIN $(output_path) headers
|
||||
|
||||
headers: | $(src_dir) $(include_dir)
|
||||
@cp $(src_dir)/include/*.h $(include_dir)
|
||||
|
||||
$(output_path): $(src_dir)/$(lib_prefix).a | $(lib_dir)
|
||||
@cp $< $@
|
||||
|
||||
$(src_dir)/$(lib_prefix).a: | $(src_dir)
|
||||
cd $(src_dir) && \
|
||||
$(MEDIADEVICES_TOOLCHAIN_BIN) cmake -DOPUS_STACK_PROTECTOR=OFF -DCMAKE_C_FLAGS="-fpic" && \
|
||||
$(MEDIADEVICES_TOOLCHAIN_BIN) make VERBOSE=1
|
||||
|
||||
$(src_dir): | $(src_root_dir)
|
||||
git clone --depth=1 --branch=$(version) $(git_url) $@
|
||||
|
||||
$(src_root_dir) $(lib_dir) $(include_dir):
|
||||
@mkdir -p $@
|
||||
|
||||
guard-%:
|
||||
@if [ -z ${$*} ]; then echo "$* is a required environment variable"; exit 1; fi
|
@@ -1,19 +0,0 @@
|
||||
GIT_URL=https://github.com/xiph/opus.git
|
||||
VERSION=v1.3.1
|
||||
SRC_DIR=src
|
||||
LIB_DIR=lib
|
||||
INCLUDE_DIR=include
|
||||
ROOT_DIR=${PWD}
|
||||
LIB_PREFIX=libopus
|
||||
|
||||
mkdir -p ${LIB_DIR} ${INCLUDE_DIR}
|
||||
|
||||
git clone --depth=1 --branch=${VERSION} ${GIT_URL} ${SRC_DIR}
|
||||
cd ${SRC_DIR}
|
||||
${MEDIADEVICES_TOOLCHAIN_BIN} cmake -DOPUS_STACK_PROTECTOR=OFF .
|
||||
${MEDIADEVICES_TOOLCHAIN_BIN} make -j2
|
||||
mv ${LIB_PREFIX}.a ${ROOT_DIR}/${LIB_DIR}/${LIB_PREFIX}_${MEDIADEVICES_TARGET_PLATFORM}.a
|
||||
mkdir -p ${ROOT_DIR}/${INCLUDE_DIR}
|
||||
cp include/*.h ${ROOT_DIR}/${INCLUDE_DIR}
|
||||
git clean -dfx
|
||||
git reset --hard
|
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.
Binary file not shown.
Binary file not shown.
BIN
pkg/codec/opus/lib/libopus-linux-armv7.a
Normal file
BIN
pkg/codec/opus/lib/libopus-linux-armv7.a
Normal file
Binary file not shown.
BIN
pkg/codec/opus/lib/libopus-windows-x64.a
Normal file
BIN
pkg/codec/opus/lib/libopus-windows-x64.a
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,8 @@ package opus
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
/*
|
||||
#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));
|
||||
}
|
||||
@@ -26,9 +27,9 @@ type encoder struct {
|
||||
inBuff wave.Audio
|
||||
reader audio.Reader
|
||||
engine *C.OpusEncoder
|
||||
}
|
||||
|
||||
var latencies = []float64{5, 10, 20, 40, 60}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
||||
var cerror C.int
|
||||
@@ -37,10 +38,6 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
|
||||
return nil, fmt.Errorf("opus: inProp.SampleRate is required")
|
||||
}
|
||||
|
||||
if p.Latency == 0 {
|
||||
p.Latency = 20
|
||||
}
|
||||
|
||||
if params.BitRate == 0 {
|
||||
params.BitRate = 32000
|
||||
}
|
||||
@@ -49,19 +46,8 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
|
||||
params.ChannelMixer = &mixer.MonoMixer{}
|
||||
}
|
||||
|
||||
// Select the nearest supported latency
|
||||
var targetLatency float64
|
||||
// TODO: use p.Latency.Milliseconds() after Go 1.12 EOL
|
||||
latencyInMS := float64(p.Latency.Nanoseconds() / 1000000)
|
||||
nearestDist := math.Inf(+1)
|
||||
for _, latency := range latencies {
|
||||
dist := math.Abs(latency - latencyInMS)
|
||||
if dist >= nearestDist {
|
||||
break
|
||||
}
|
||||
|
||||
nearestDist = dist
|
||||
targetLatency = latency
|
||||
if !params.Latency.Validate() {
|
||||
return nil, fmt.Errorf("opus: unsupported latency %v", params.Latency)
|
||||
}
|
||||
|
||||
channels := p.ChannelCount
|
||||
@@ -77,7 +63,7 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser,
|
||||
}
|
||||
|
||||
rMix := audio.NewChannelMixer(channels, params.ChannelMixer)
|
||||
rBuf := audio.NewBuffer(int(targetLatency * float64(p.SampleRate) / 1000))
|
||||
rBuf := audio.NewBuffer(params.Latency.samples(p.SampleRate))
|
||||
e := encoder{
|
||||
engine: engine,
|
||||
reader: rMix(rBuf(r)),
|
||||
@@ -97,6 +83,12 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.engine == nil {
|
||||
return nil, nil, io.EOF
|
||||
}
|
||||
|
||||
encoded := make([]byte, 1024)
|
||||
var n C.opus_int32
|
||||
switch b := buff.(type) {
|
||||
@@ -128,7 +120,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(bitRate int) error {
|
||||
cerror := C.bridge_encoder_set_bitrate(
|
||||
cerror := C.pion_set_encoder_bitrate(
|
||||
e.engine,
|
||||
C.int(bitRate),
|
||||
)
|
||||
@@ -139,11 +131,16 @@ func (e *encoder) SetBitRate(bitRate int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
func (e *encoder) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.engine == nil {
|
||||
return nil
|
||||
}
|
||||
C.opus_encoder_destroy(e.engine)
|
||||
e.engine = nil
|
||||
return nil
|
||||
|
@@ -4,9 +4,10 @@ package opus
|
||||
|
||||
//#cgo CFLAGS: -I${SRCDIR}/include
|
||||
//#cgo CXXFLAGS: -I${SRCDIR}/include
|
||||
//#cgo linux,arm LDFLAGS: ${SRCDIR}/lib/libopus_linux_armv7.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 darwin,amd64 LDFLAGS: ${SRCDIR}/lib/libopus_darwin_x64.a
|
||||
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopus_windows_x64.a
|
||||
//#cgo linux,arm LDFLAGS: ${SRCDIR}/lib/libopus-linux-armv7.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 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
|
||||
import "C"
|
||||
|
79
pkg/codec/opus/opus_test.go
Normal file
79
pkg/codec/opus/opus_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package opus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices/pkg/wave"
|
||||
)
|
||||
|
||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement key frame control
|
||||
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.AudioEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Audio: prop.Audio{
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
},
|
||||
},
|
||||
wave.NewInt16Interleaved(wave.ChunkInfo{
|
||||
Len: 960,
|
||||
SamplingRate: 48000,
|
||||
Channels: 2,
|
||||
}),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.AudioEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Audio: prop.Audio{
|
||||
SampleRate: 48000,
|
||||
ChannelCount: 2,
|
||||
},
|
||||
})
|
||||
})
|
||||
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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
@@ -1,27 +1,69 @@
|
||||
package opus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/pion/mediadevices/pkg/wave/mixer"
|
||||
)
|
||||
|
||||
// Latency is a type of OPUS codec frame duration.
|
||||
type Latency time.Duration
|
||||
|
||||
// Latency values available in OPUS codec.
|
||||
const (
|
||||
Latency2500us Latency = Latency(2500 * time.Microsecond)
|
||||
Latency5ms Latency = Latency(5 * time.Millisecond)
|
||||
Latency10ms Latency = Latency(10 * time.Millisecond)
|
||||
Latency20ms Latency = Latency(20 * time.Millisecond)
|
||||
Latency40ms Latency = Latency(40 * time.Millisecond)
|
||||
Latency60ms Latency = Latency(60 * time.Millisecond)
|
||||
)
|
||||
|
||||
// Validate that the Latency is allowed in OPUS.
|
||||
func (l Latency) Validate() bool {
|
||||
switch l {
|
||||
case Latency2500us, Latency5ms, Latency10ms, Latency20ms, Latency40ms, Latency60ms:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Duration returns latency in time.Duration.
|
||||
func (l Latency) Duration() time.Duration {
|
||||
return time.Duration(l)
|
||||
}
|
||||
|
||||
// samples returns number of samples for given sample rate.
|
||||
func (l Latency) samples(sampleRate int) int {
|
||||
return int(l.Duration() * time.Duration(sampleRate) / time.Second)
|
||||
}
|
||||
|
||||
// Params stores opus specific encoding parameters.
|
||||
type Params struct {
|
||||
codec.BaseParams
|
||||
// ChannelMixer is a mixer to be used if number of given and expected channels differ.
|
||||
ChannelMixer mixer.ChannelMixer
|
||||
|
||||
// Expected latency of the codec.
|
||||
Latency Latency
|
||||
}
|
||||
|
||||
// NewParams returns default opus codec specific parameters.
|
||||
func NewParams() (Params, error) {
|
||||
return Params{}, nil
|
||||
return Params{
|
||||
Latency: Latency20ms,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RTPCodec represents the codec metadata
|
||||
func (p *Params) RTPCodec() *codec.RTPCodec {
|
||||
return codec.NewRTPOpusCodec(48000)
|
||||
c := codec.NewRTPOpusCodec(48000)
|
||||
c.Latency = time.Duration(p.Latency)
|
||||
return c
|
||||
}
|
||||
|
||||
// BuildAudioEncoder builds opus encoder with given params
|
||||
|
49
pkg/codec/opus/params_test.go
Normal file
49
pkg/codec/opus/params_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package opus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLatency_Validate(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
for _, l := range []Latency{
|
||||
Latency2500us, Latency5ms, Latency10ms, Latency20ms, Latency40ms, Latency60ms,
|
||||
} {
|
||||
if !l.Validate() {
|
||||
t.Errorf("Defined Latency(%v) must be valid", l)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
for _, l := range []Latency{
|
||||
0, Latency(time.Second),
|
||||
} {
|
||||
if l.Validate() {
|
||||
t.Errorf("Latency(%v) must be valid", l)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLatency_samples(t *testing.T) {
|
||||
testCases := []struct {
|
||||
latency Latency
|
||||
sampleRate int
|
||||
samples int
|
||||
}{
|
||||
{Latency5ms, 48000, 240},
|
||||
{Latency20ms, 16000, 320},
|
||||
{Latency20ms, 48000, 960},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(fmt.Sprintf("%v_%d", time.Duration(testCase.latency), testCase.sampleRate), func(t *testing.T) {
|
||||
samples := testCase.latency.samples(testCase.sampleRate)
|
||||
if samples != testCase.samples {
|
||||
t.Errorf("Expected samples: %d, got: %d", testCase.samples, samples)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
// Package vaapi implements hardware accelerated codecs.
|
||||
|
89
pkg/codec/vaapi/vaapi_test.go
Normal file
89
pkg/codec/vaapi/vaapi_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package vaapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
if _, err := os.Stat("/dev/dri/card0"); errors.Is(err, os.ErrNotExist) {
|
||||
t.Skip("/dev/dri/card0 not found")
|
||||
}
|
||||
|
||||
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()
|
||||
return &p, err
|
||||
},
|
||||
} {
|
||||
factory := factory
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := factory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderSimpleReadTest(t, p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := factory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderCloseTwiceTest(t, p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := 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,
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package vaapi
|
||||
@@ -61,6 +62,7 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
@@ -99,6 +101,8 @@ type encoderVP8 struct {
|
||||
|
||||
rate *framerateDetector
|
||||
|
||||
forceKeyFrame atomic.Bool
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
@@ -303,10 +307,11 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
|
||||
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
||||
@@ -314,7 +319,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
||||
|
||||
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
||||
|
||||
if kf {
|
||||
if kf || e.forceKeyFrame.CompareAndSwap(true, false) {
|
||||
// Key frame
|
||||
C.setForceKFFlagVP8(&e.picParam, 1)
|
||||
C.setFrameTypeFlagVP8(&e.picParam, 0)
|
||||
@@ -540,18 +545,22 @@ func (e *encoderVP8) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoderVP8) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
func (e *encoderVP8) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoderVP8) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
func (e *encoderVP8) ForceKeyFrame() {
|
||||
e.forceKeyFrame.Store(true)
|
||||
}
|
||||
|
||||
func (e *encoderVP8) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
C.vaDestroySurfaces(e.display, &e.surfs[0], C.int(len(e.surfs)))
|
||||
C.vaDestroyContext(e.display, e.ctxID)
|
||||
C.vaDestroyConfig(e.display, e.confID)
|
||||
|
25
pkg/codec/vaapi/vp8_test.go
Normal file
25
pkg/codec/vaapi/vp8_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package vaapi
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVP8ShouldImplementBitRateControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement bit rate control
|
||||
|
||||
e := &encoderVP8{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVP8ShouldImplementKeyFrameControl(t *testing.T) {
|
||||
e := &encoderVP8{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package vaapi
|
||||
@@ -44,6 +45,7 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
@@ -91,6 +93,8 @@ type encoderVP9 struct {
|
||||
|
||||
rate *framerateDetector
|
||||
|
||||
forceKeyFrame atomic.Bool
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
@@ -292,10 +296,11 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
|
||||
kf := e.frameCnt%e.params.KeyFrameInterval == 0
|
||||
@@ -303,7 +308,7 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
||||
|
||||
e.frParam.data.framerate = C.uint(e.rate.Calc())
|
||||
|
||||
if kf {
|
||||
if kf || e.forceKeyFrame.CompareAndSwap(true, false) {
|
||||
C.setForceKFFlag9(&e.picParam, 1)
|
||||
C.setFrameTypeFlagVP9(&e.picParam, 0)
|
||||
e.picParam.refresh_frame_flags = 0
|
||||
@@ -475,18 +480,22 @@ func (e *encoderVP9) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoderVP9) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
func (e *encoderVP9) Controller() codec.EncoderController {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoderVP9) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
func (e *encoderVP9) ForceKeyFrame() {
|
||||
e.forceKeyFrame.Store(true)
|
||||
}
|
||||
|
||||
func (e *encoderVP9) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
C.vaDestroySurfaces(e.display, &e.surfs[0], C.int(len(e.surfs)))
|
||||
C.vaDestroyContext(e.display, e.ctxID)
|
||||
C.vaDestroyConfig(e.display, e.confID)
|
||||
|
25
pkg/codec/vaapi/vp9_test.go
Normal file
25
pkg/codec/vaapi/vp9_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package vaapi
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVP9ShouldImplementBitRateControl(t *testing.T) {
|
||||
t.SkipNow() // TODO: Implement bit rate control
|
||||
|
||||
e := &encoderVP9{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestVP9ShouldImplementKeyFrameControl(t *testing.T) {
|
||||
e := &encoderVP9{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ type Params struct {
|
||||
RateControlOvershootPercent uint
|
||||
RateControlMinQuantizer uint
|
||||
RateControlMaxQuantizer uint
|
||||
LagInFrames uint
|
||||
ErrorResilient ErrorResilientMode
|
||||
}
|
||||
|
||||
|
@@ -23,6 +23,9 @@ package vpx
|
||||
// int pktSz(vpx_codec_cx_pkt_t *pkt) {
|
||||
// return pkt->data.frame.sz;
|
||||
// }
|
||||
// vpx_codec_frame_flags_t pktFrameFlags(vpx_codec_cx_pkt_t *pkt) {
|
||||
// return pkt->data.frame.flags;
|
||||
// }
|
||||
//
|
||||
// // Alloc helpers
|
||||
// vpx_codec_ctx_t *newCtx() {
|
||||
@@ -51,6 +54,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -61,20 +65,29 @@ import (
|
||||
)
|
||||
|
||||
type encoder struct {
|
||||
codec *C.vpx_codec_ctx_t
|
||||
raw *C.vpx_image_t
|
||||
cfg *C.vpx_codec_enc_cfg_t
|
||||
r video.Reader
|
||||
frameIndex int
|
||||
tStart int
|
||||
tLastFrame int
|
||||
frame []byte
|
||||
deadline int
|
||||
codec *C.vpx_codec_ctx_t
|
||||
raw *C.vpx_image_t
|
||||
cfg *C.vpx_codec_enc_cfg_t
|
||||
r video.Reader
|
||||
frameIndex int
|
||||
tStart time.Time
|
||||
tLastFrame time.Time
|
||||
frame []byte
|
||||
deadline int
|
||||
requireKeyFrame bool
|
||||
targetBitrate int
|
||||
isKeyFrame bool
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
const (
|
||||
kRateControlThreshold = 0.15
|
||||
kMinQuantizer = 20
|
||||
kMaxQuantizer = 63
|
||||
)
|
||||
|
||||
// VP8Params is codec specific paramaters
|
||||
type VP8Params struct {
|
||||
Params
|
||||
@@ -141,6 +154,7 @@ func newParams(codecIface *C.vpx_codec_iface_t) (Params, error) {
|
||||
RateControlOvershootPercent: uint(cfg.rc_overshoot_pct),
|
||||
RateControlMinQuantizer: uint(cfg.rc_min_quantizer),
|
||||
RateControlMaxQuantizer: uint(cfg.rc_max_quantizer),
|
||||
LagInFrames: uint(cfg.g_lag_in_frames),
|
||||
ErrorResilient: ErrorResilientMode(cfg.g_error_resilient),
|
||||
}, nil
|
||||
}
|
||||
@@ -171,6 +185,7 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
|
||||
cfg.g_h = C.uint(p.Height)
|
||||
cfg.g_timebase.num = 1
|
||||
cfg.g_timebase.den = 1000
|
||||
cfg.g_lag_in_frames = C.uint(params.LagInFrames)
|
||||
cfg.rc_target_bitrate = C.uint(params.BitRate) / 1000
|
||||
cfg.kf_max_dist = C.uint(params.KeyFrameInterval)
|
||||
|
||||
@@ -191,16 +206,17 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
|
||||
); ec != 0 {
|
||||
return nil, fmt.Errorf("vpx_codec_enc_init failed (%d)", ec)
|
||||
}
|
||||
t0 := time.Now().Nanosecond() / 1000000
|
||||
t0 := time.Now()
|
||||
return &encoder{
|
||||
r: video.ToI420(r),
|
||||
codec: codec,
|
||||
raw: rawNoBuffer,
|
||||
cfg: cfg,
|
||||
tStart: t0,
|
||||
tLastFrame: t0,
|
||||
deadline: int(params.Deadline / time.Microsecond),
|
||||
frame: make([]byte, 1024),
|
||||
r: video.ToI420(r),
|
||||
codec: codec,
|
||||
raw: rawNoBuffer,
|
||||
cfg: cfg,
|
||||
tStart: t0,
|
||||
tLastFrame: t0,
|
||||
deadline: int(params.Deadline / time.Microsecond),
|
||||
frame: make([]byte, 1024),
|
||||
targetBitrate: params.BitRate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -212,10 +228,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
bounds := yuvImg.Bounds()
|
||||
height := C.int(bounds.Dy())
|
||||
@@ -225,19 +242,30 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
e.raw.stride[1] = 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) {
|
||||
e.cfg.g_w, e.cfg.g_h = C.uint(width), C.uint(height)
|
||||
if ec := C.vpx_codec_enc_config_set(e.codec, e.cfg); ec != C.VPX_CODEC_OK {
|
||||
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec)
|
||||
|
||||
newCodec := C.newCtx()
|
||||
if ec := C.vpx_codec_enc_init_ver(
|
||||
newCodec, e.codec.iface, e.cfg, 0, C.VPX_ENCODER_ABI_VERSION,
|
||||
); ec != 0 {
|
||||
return nil, func() {}, fmt.Errorf("vpx_codec_enc_init failed (%d)", ec)
|
||||
}
|
||||
C.free(unsafe.Pointer(e.codec))
|
||||
e.codec = newCodec
|
||||
|
||||
e.raw.w, e.raw.h = C.uint(width), C.uint(height)
|
||||
e.raw.r_w, e.raw.r_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.
|
||||
// 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
|
||||
@@ -246,15 +274,29 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
if duration == 0 {
|
||||
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
|
||||
if e.requireKeyFrame {
|
||||
flags = flags | C.VPX_EFLAG_FORCE_KF
|
||||
}
|
||||
if ec := C.encode_wrapper(
|
||||
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]),
|
||||
); ec != C.VPX_CODEC_OK {
|
||||
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
|
||||
}
|
||||
|
||||
e.requireKeyFrame = false
|
||||
e.frameIndex++
|
||||
e.tLastFrame = t
|
||||
|
||||
@@ -266,6 +308,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
break
|
||||
}
|
||||
if pkt.kind == C.VPX_CODEC_CX_FRAME_PKT {
|
||||
e.isKeyFrame = C.pktFrameFlags(pkt)&C.VPX_FRAME_IS_KEY == C.VPX_FRAME_IS_KEY
|
||||
encoded := C.GoBytes(unsafe.Pointer(C.pktBuf(pkt)), C.pktSz(pkt))
|
||||
e.frame = append(e.frame, encoded...)
|
||||
}
|
||||
@@ -276,18 +319,50 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.requireKeyFrame = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
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 {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.closed = true
|
||||
|
||||
C.free(unsafe.Pointer(e.raw))
|
||||
|
427
pkg/codec/vpx/vpx_test.go
Normal file
427
pkg/codec/vpx/vpx_test.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package vpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncoder(t *testing.T) {
|
||||
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()
|
||||
p.LagInFrames = 0
|
||||
return &p, err
|
||||
},
|
||||
} {
|
||||
factory := factory
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := factory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderSimpleReadTest(t, p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := factory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codectest.VideoEncoderCloseTwiceTest(t, p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := 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,
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageSizeChange(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
for name, testCase := range map[string]struct {
|
||||
initialWidth, initialHeight int
|
||||
width, height int
|
||||
}{
|
||||
"NoChange": {
|
||||
320, 240,
|
||||
320, 240,
|
||||
},
|
||||
"Enlarge": {
|
||||
320, 240,
|
||||
640, 480,
|
||||
},
|
||||
"Shrink": {
|
||||
640, 480,
|
||||
320, 240,
|
||||
},
|
||||
} {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var cnt uint32
|
||||
r, err := param.BuildVideoEncoder(
|
||||
video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
i := atomic.AddUint32(&cnt, 1)
|
||||
if i == 1 {
|
||||
return image.NewYCbCr(
|
||||
image.Rect(0, 0, testCase.width, testCase.height),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
), func() {}, nil
|
||||
}
|
||||
return nil, nil, io.EOF
|
||||
}),
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: testCase.initialWidth,
|
||||
Height: testCase.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.Read()
|
||||
if err != io.EOF {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestKeyFrame(t *testing.T) {
|
||||
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()
|
||||
r.Controller().(codec.KeyFrameController).ForceKeyFrame()
|
||||
_, rel, err = r.Read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.(*encoder).isKeyFrame {
|
||||
t.Fatal("Not a key frame")
|
||||
}
|
||||
rel()
|
||||
_, _, err = r.Read()
|
||||
if err != io.EOF {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetBitrate(t *testing.T) {
|
||||
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{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
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)))
|
||||
})
|
||||
}
|
@@ -8,6 +8,7 @@
|
||||
#define ERR_ALLOC_PICTURE -3
|
||||
#define ERR_OPEN_ENGINE -4
|
||||
#define ERR_ENCODE -5
|
||||
#define ERR_BITRATE_RECONFIG -6
|
||||
|
||||
typedef struct Slice {
|
||||
unsigned char *data;
|
||||
@@ -18,6 +19,7 @@ typedef struct Encoder {
|
||||
x264_t *h;
|
||||
x264_picture_t pic_in;
|
||||
x264_param_t param;
|
||||
int force_key_frame;
|
||||
} Encoder;
|
||||
|
||||
Encoder *enc_new(x264_param_t param, char *preset, int *rc) {
|
||||
@@ -77,6 +79,22 @@ fail:
|
||||
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) {
|
||||
x264_nal_t *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[1] = cb;
|
||||
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);
|
||||
e->force_key_frame = 0;
|
||||
Slice s = {.data_len = frame_size};
|
||||
if (frame_size <= 0) {
|
||||
*rc = ERR_ENCODE;
|
||||
|
@@ -39,6 +39,8 @@ func (e cerror) Error() string {
|
||||
return errOpenEngine.Error()
|
||||
case C.ERR_ENCODE:
|
||||
return errEncode.Error()
|
||||
case C.ERR_BITRATE_RECONFIG:
|
||||
return errSetBitrate.Error()
|
||||
default:
|
||||
return "unknown error"
|
||||
}
|
||||
@@ -58,6 +60,7 @@ var (
|
||||
errAllocPicture = fmt.Errorf("failed to alloc picture")
|
||||
errOpenEngine = fmt.Errorf("failed to open x264")
|
||||
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) {
|
||||
@@ -102,10 +105,11 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
img, _, err := e.r.Read()
|
||||
img, release, err := e.r.Read()
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
defer release()
|
||||
yuvImg := img.(*image.YCbCr)
|
||||
|
||||
var rc C.int
|
||||
@@ -124,12 +128,26 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
||||
return encoded, func() {}, err
|
||||
}
|
||||
|
||||
func (e *encoder) SetBitRate(b int) error {
|
||||
panic("SetBitRate is not implemented")
|
||||
}
|
||||
// TODO: Implement bit rate control
|
||||
//var _ codec.BitRateController = (*encoder)(nil)
|
||||
|
||||
func (e *encoder) ForceKeyFrame() error {
|
||||
panic("ForceKeyFrame is not implemented")
|
||||
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 {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
|
146
pkg/codec/x264/x264_test.go
Normal file
146
pkg/codec/x264/x264_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package x264
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/codec"
|
||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"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) {
|
||||
t.Run("SimpleRead", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.BitRate = 200000
|
||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
||||
prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 256,
|
||||
Height: 144,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
},
|
||||
image.NewYCbCr(
|
||||
image.Rect(0, 0, 256, 144),
|
||||
image.YCbCrSubsampleRatio420,
|
||||
),
|
||||
)
|
||||
})
|
||||
t.Run("CloseTwice", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p.BitRate = 200000
|
||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameRate: 30,
|
||||
FrameFormat: frame.FormatI420,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
||||
p, err := NewParams()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
e := &encoder{}
|
||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoErrorOnForceKeyFrame(t *testing.T) {
|
||||
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{}
|
||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
||||
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
|
||||
}
|
@@ -1 +1,16 @@
|
||||
/*
|
||||
Package camera provides a video camera driver.
|
||||
|
||||
Device Label Generation Rules
|
||||
|
||||
On Linux, the device label will be in the format of:
|
||||
pci-0000:00:00.0-usb-0:0:0.0-video-index0;video0
|
||||
If /dev/v4l/by-path/* is not available (for example in a docker container without
|
||||
bindings in /dev/v4l/by-path/), it will be:
|
||||
video0;video0
|
||||
*/
|
||||
package camera
|
||||
|
||||
// LabelSeparator is used to separate labels for a driver that
|
||||
// is found from multiple locations on a host.
|
||||
const LabelSeparator = ";"
|
||||
|
@@ -56,7 +56,7 @@ func BenchmarkRead(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := r.Read()
|
||||
_, _, err := r.Read()
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to read: %v", err)
|
||||
}
|
||||
|
@@ -13,9 +13,15 @@ import (
|
||||
type camera struct {
|
||||
device avfoundation.Device
|
||||
session *avfoundation.Session
|
||||
rcClose func()
|
||||
}
|
||||
|
||||
func init() {
|
||||
Initialize()
|
||||
}
|
||||
|
||||
// Initialize finds and registers camera devices. This is part of an experimental API.
|
||||
func Initialize() {
|
||||
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -26,6 +32,7 @@ func init() {
|
||||
driver.GetManager().Register(cam, driver.Info{
|
||||
Label: device.UID,
|
||||
DeviceType: driver.Camera,
|
||||
Name: device.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,6 +50,9 @@ func (cam *camera) Open() error {
|
||||
}
|
||||
|
||||
func (cam *camera) Close() error {
|
||||
if cam.rcClose != nil {
|
||||
cam.rcClose()
|
||||
}
|
||||
return cam.session.Close()
|
||||
}
|
||||
|
||||
@@ -56,6 +66,7 @@ func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cam.rcClose = rc.Close
|
||||
r := video.ReaderFunc(func() (image.Image, func(), error) {
|
||||
frame, _, err := rc.Read()
|
||||
if err != nil {
|
||||
|
63
pkg/driver/camera/camera_darwin_test.go
Normal file
63
pkg/driver/camera/camera_darwin_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// +build darwin
|
||||
|
||||
// $ go test -v . -tags darwin -run="^TestCameraFrameFormatSupport$"
|
||||
|
||||
package camera
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/avfoundation"
|
||||
"github.com/pion/mediadevices/pkg/frame"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
)
|
||||
|
||||
func TestCameraFrameFormatSupport(t *testing.T) {
|
||||
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(devices) > 0 {
|
||||
c := newCamera(devices[0])
|
||||
if err := c.Open(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
supportedFormats := make(map[frame.Format]struct{})
|
||||
for _, p := range c.Properties() {
|
||||
supportedFormats[p.FrameFormat] = struct{}{}
|
||||
}
|
||||
|
||||
for _, format := range []frame.Format{
|
||||
frame.FormatI420,
|
||||
frame.FormatNV12,
|
||||
frame.FormatNV21,
|
||||
frame.FormatYUY2,
|
||||
frame.FormatUYVY,
|
||||
} {
|
||||
if _, ok := supportedFormats[format]; !ok {
|
||||
t.Logf("[%v] UNSUPPORTED", format)
|
||||
continue
|
||||
}
|
||||
r, err := c.VideoRecord(prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
FrameFormat: format,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Logf("[%v] Failed to capture image: %v", format, err)
|
||||
continue
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
_, _, err := r.Read()
|
||||
if err != nil {
|
||||
t.Logf("[%v] Failed to read: %v", format, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
t.Logf("[%v] OK", format)
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,9 +8,15 @@ import (
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||
|
||||
"github.com/blackjack/webcam"
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
@@ -59,6 +65,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
const bufCount = 2
|
||||
|
||||
// Camera implementation using v4l2
|
||||
// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev
|
||||
type camera struct {
|
||||
@@ -69,55 +77,83 @@ type camera struct {
|
||||
started bool
|
||||
mutex sync.Mutex
|
||||
cancel func()
|
||||
prevFrameTime time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
discovered := make(map[string]struct{})
|
||||
Initialize()
|
||||
}
|
||||
|
||||
discover := func(pattern string) {
|
||||
devices, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
// No v4l device.
|
||||
return
|
||||
}
|
||||
for _, device := range devices {
|
||||
label := filepath.Base(device)
|
||||
reallink, err := os.Readlink(device)
|
||||
if err != nil {
|
||||
reallink = label
|
||||
} else {
|
||||
reallink = filepath.Base(reallink)
|
||||
}
|
||||
|
||||
if _, ok := discovered[reallink]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
discovered[reallink] = struct{}{}
|
||||
cam := newCamera(device)
|
||||
priority := driver.PriorityNormal
|
||||
if label == prioritizedDevice {
|
||||
priority = driver.PriorityHigh
|
||||
}
|
||||
driver.GetManager().Register(cam, driver.Info{
|
||||
Label: label,
|
||||
DeviceType: driver.Camera,
|
||||
Priority: priority,
|
||||
})
|
||||
}
|
||||
// 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{})
|
||||
discover(discovered, "/dev/v4l/by-id/*")
|
||||
discover(discovered, "/dev/v4l/by-path/*")
|
||||
discover(discovered, "/dev/video*")
|
||||
}
|
||||
|
||||
discover("/dev/v4l/by-path/*")
|
||||
discover("/dev/video*")
|
||||
func discover(discovered map[string]struct{}, pattern string) {
|
||||
devices, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
// No v4l device.
|
||||
return
|
||||
}
|
||||
for _, device := range devices {
|
||||
label := filepath.Base(device)
|
||||
reallink, err := os.Readlink(device)
|
||||
if err != nil {
|
||||
reallink = label
|
||||
} else {
|
||||
reallink = filepath.Base(reallink)
|
||||
}
|
||||
if _, ok := discovered[reallink]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
discovered[reallink] = struct{}{}
|
||||
cam := newCamera(device)
|
||||
priority := driver.PriorityNormal
|
||||
if reallink == prioritizedDevice {
|
||||
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{
|
||||
// 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,
|
||||
DeviceType: driver.Camera,
|
||||
Priority: priority,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCamera(path string) *camera {
|
||||
formats := map[webcam.PixelFormat]frame.Format{
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_YUV420): frame.FormatI420,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_NV21): frame.FormatNV21,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV12,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY): frame.FormatUYVY,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG,
|
||||
webcam.PixelFormat(C.V4L2_PIX_FMT_Z16): frame.FormatZ16,
|
||||
}
|
||||
|
||||
reversedFormats := make(map[frame.Format]webcam.PixelFormat)
|
||||
@@ -133,14 +169,32 @@ func newCamera(path string) *camera {
|
||||
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 {
|
||||
cam, err := webcam.Open(c.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Late frames should be discarded. Buffering should be handled in higher level.
|
||||
cam.SetBufferCount(1)
|
||||
// Buffering should be handled in higher level.
|
||||
err = cam.SetBufferCount(bufCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.prevFrameTime = time.Now()
|
||||
c.cam = cam
|
||||
return nil
|
||||
}
|
||||
@@ -179,12 +233,21 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cam := c.cam
|
||||
|
||||
readTimeoutSec := getCameraReadTimeout()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.cancel = cancel
|
||||
var buf []byte
|
||||
@@ -200,7 +263,14 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||
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) {
|
||||
case nil:
|
||||
case *webcam.Timeout:
|
||||
@@ -216,6 +286,10 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
if p.DiscardFramesOlderThan != 0 {
|
||||
c.prevFrameTime = time.Now()
|
||||
}
|
||||
|
||||
// Frame is empty.
|
||||
// Retry reading and return errEmptyFrame if it exceeds maxEmptyFrameCount.
|
||||
if len(b) == 0 {
|
||||
@@ -249,13 +323,31 @@ func (c *camera) Properties() []prop.Media {
|
||||
}
|
||||
|
||||
if frameSize.StepWidth == 0 || frameSize.StepHeight == 0 {
|
||||
properties = append(properties, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: int(frameSize.MaxWidth),
|
||||
Height: int(frameSize.MaxHeight),
|
||||
FrameFormat: supportedFormat,
|
||||
},
|
||||
})
|
||||
framerates := c.cam.GetSupportedFramerates(format, uint32(frameSize.MaxWidth), uint32(frameSize.MaxHeight))
|
||||
// If the camera doesn't support framerate, we just add the resolution and format
|
||||
if len(framerates) == 0 {
|
||||
properties = append(properties, prop.Media{
|
||||
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 {
|
||||
// FIXME: we should probably use a custom data structure to capture all of the supported resolutions
|
||||
for _, supportedResolution := range supportedResolutions {
|
||||
@@ -274,16 +366,100 @@ func (c *camera) Properties() []prop.Media {
|
||||
continue
|
||||
}
|
||||
|
||||
properties = append(properties, prop.Media{
|
||||
Video: prop.Video{
|
||||
Width: width,
|
||||
Height: height,
|
||||
FrameFormat: supportedFormat,
|
||||
},
|
||||
})
|
||||
framerates := c.cam.GetSupportedFramerates(format, uint32(width), uint32(height))
|
||||
if len(framerates) == 0 {
|
||||
properties = append(properties, prop.Media{
|
||||
Video: prop.Video{
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
193
pkg/driver/camera/camera_linux_test.go
Normal file
193
pkg/driver/camera/camera_linux_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/mediadevices/pkg/driver"
|
||||
)
|
||||
|
||||
func TestDiscover(t *testing.T) {
|
||||
const (
|
||||
shortName = "unittest-video0"
|
||||
shortName2 = "unittest-video1"
|
||||
longName = "unittest-long-device-name:0:1:2:3"
|
||||
)
|
||||
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
byPathDir := filepath.Join(dir, "v4l", "by-path")
|
||||
if err := os.MkdirAll(byPathDir, 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(byPathDir, longName),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
discovered := make(map[string]struct{})
|
||||
discover(discovered, filepath.Join(byPathDir, "*"))
|
||||
discover(discovered, filepath.Join(dir, "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, "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 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 "_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.
|
||||
// This is for debugging.
|
||||
void printErr(HRESULT hr)
|
||||
@@ -76,6 +88,7 @@ int listCamera(cameraList* list, const char** errstr)
|
||||
{
|
||||
list->name[i] = getCameraName(moniker);
|
||||
moniker->Release();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -32,6 +32,11 @@ type camera struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Initialize()
|
||||
}
|
||||
|
||||
// Initialize finds and registers camera devices. This is part of an experimental API.
|
||||
func Initialize() {
|
||||
C.CoInitializeEx(nil, C.COINIT_MULTITHREADED)
|
||||
|
||||
var list C.cameraList
|
||||
|
@@ -40,15 +40,9 @@ int listResolution(camera* cam, const char** errstr);
|
||||
int listCamera(cameraList* list, const char** errstr);
|
||||
int freeCameraList(cameraList* list, const char** errstr);
|
||||
|
||||
inline imageProp* getProp(camera* cam, int i)
|
||||
{
|
||||
return &cam->props[i];
|
||||
}
|
||||
imageProp* getProp(camera* cam, int i);
|
||||
|
||||
inline char* getName(cameraList* list, int i)
|
||||
{
|
||||
return list->name[i];
|
||||
}
|
||||
char* getName(cameraList* list, int i);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||
"github.com/pion/mediadevices/pkg/io/audio"
|
||||
"github.com/pion/mediadevices/pkg/io/video"
|
||||
"github.com/pion/mediadevices/pkg/prop"
|
||||
@@ -30,6 +31,7 @@ type Info struct {
|
||||
Label string
|
||||
DeviceType DeviceType
|
||||
Priority Priority
|
||||
Name string
|
||||
}
|
||||
|
||||
type Adapter interface {
|
||||
@@ -44,3 +46,14 @@ type Driver interface {
|
||||
Info() Info
|
||||
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
|
||||
|
||||
import "sync"
|
||||
|
||||
// FilterFn is being used to decide if a driver should be included in the
|
||||
// query result.
|
||||
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
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
drivers map[string]Driver
|
||||
}
|
||||
|
||||
@@ -69,6 +72,8 @@ func GetManager() *Manager {
|
||||
|
||||
// Register registers adapter to be discoverable by Query
|
||||
func (m *Manager) Register(a Adapter, info Info) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
d := wrapAdapter(a, info)
|
||||
m.drivers[d.ID()] = d
|
||||
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.
|
||||
func (m *Manager) Query(f FilterFn) []Driver {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
results := make([]Driver, 0)
|
||||
for _, d := range m.drivers {
|
||||
if ok := f(d); ok {
|
||||
@@ -85,3 +92,10 @@ func (m *Manager) Query(f FilterFn) []Driver {
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func filterTrue(d Driver) bool {
|
||||
func filterTrue(_ Driver) bool {
|
||||
return true
|
||||
}
|
||||
func filterFalse(d Driver) bool {
|
||||
func filterFalse(_ Driver) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -40,3 +44,71 @@ func TestFilterAnd(t *testing.T) {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -32,10 +34,16 @@ var (
|
||||
|
||||
type microphone struct {
|
||||
malgo.DeviceInfo
|
||||
chunkChan chan []byte
|
||||
chunkChan chan []byte
|
||||
deviceCloseFunc func()
|
||||
}
|
||||
|
||||
func init() {
|
||||
Initialize()
|
||||
}
|
||||
|
||||
// Initialize finds and registers active playback or capture devices. This is part of an experimental API.
|
||||
func Initialize() {
|
||||
var err error
|
||||
ctx, err = malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
|
||||
logger.Debugf("%v\n", message)
|
||||
@@ -60,6 +68,7 @@ func init() {
|
||||
Label: device.ID.String(),
|
||||
DeviceType: driver.Microphone,
|
||||
Priority: priority,
|
||||
Name: info.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -87,9 +96,8 @@ func (m *microphone) Open() error {
|
||||
}
|
||||
|
||||
func (m *microphone) Close() error {
|
||||
if m.chunkChan != nil {
|
||||
close(m.chunkChan)
|
||||
m.chunkChan = nil
|
||||
if m.deviceCloseFunc != nil {
|
||||
m.deviceCloseFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -111,6 +119,9 @@ func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
||||
config.PerformanceProfile = malgo.LowLatency
|
||||
config.Capture.Channels = uint32(inputProp.ChannelCount)
|
||||
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 {
|
||||
config.Capture.Format = malgo.FormatF32
|
||||
} else if inputProp.SampleSize == 2 && !inputProp.IsFloat {
|
||||
@@ -119,26 +130,44 @@ func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
||||
return nil, errUnsupportedFormat
|
||||
}
|
||||
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
onRecvChunk := func(_, chunk []byte, framecount uint32) {
|
||||
m.chunkChan <- chunk
|
||||
select {
|
||||
case <-cancelCtx.Done():
|
||||
case m.chunkChan <- chunk:
|
||||
}
|
||||
}
|
||||
callbacks.Data = onRecvChunk
|
||||
|
||||
device, err := malgo.InitDevice(ctx.Context, config, callbacks)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = device.Start()
|
||||
if err != nil {
|
||||
cancel()
|
||||
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) {
|
||||
chunk, ok := <-m.chunkChan
|
||||
if !ok {
|
||||
device.Stop()
|
||||
device.Uninit()
|
||||
m.deviceCloseFunc()
|
||||
return nil, func() {}, io.EOF
|
||||
}
|
||||
|
||||
@@ -155,9 +184,6 @@ func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
||||
return decodedChunk, func() {}, err
|
||||
})
|
||||
|
||||
// FIXME: The current audio detection and audio encoder can only work with a static latency. Since the latency from the driver
|
||||
// can fluctuate, we need to stabilize it. Maybe there's a better way for doing this?
|
||||
reader = audio.NewBuffer(int(inputProp.Latency.Seconds() * float64(inputProp.SampleRate)))(reader)
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
@@ -171,36 +197,39 @@ func (m *microphone) Properties() []prop.Media {
|
||||
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.
|
||||
// for sampleRate := m.MinSampleRate; sampleRate <= m.MaxSampleRate; sampleRate += sampleRateStep {
|
||||
sampleRate := 48000
|
||||
for i := 0; i < int(m.FormatCount); i++ {
|
||||
format := m.Formats[i]
|
||||
|
||||
supportedProp := prop.Media{
|
||||
Audio: prop.Audio{
|
||||
ChannelCount: int(ch),
|
||||
SampleRate: int(sampleRate),
|
||||
IsBigEndian: isBigEndian,
|
||||
// miniaudio only supports interleaved at the moment
|
||||
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)
|
||||
supportedProp := prop.Media{
|
||||
Audio: prop.Audio{
|
||||
ChannelCount: int(format.Channels),
|
||||
SampleRate: int(sampleRate),
|
||||
IsBigEndian: isBigEndian,
|
||||
// miniaudio only supports interleaved at the moment
|
||||
IsInterleaved: true,
|
||||
// FIXME: should change this to a less discrete value
|
||||
Latency: time.Millisecond * 20,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
@@ -20,6 +20,11 @@ type screen struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Initialize()
|
||||
}
|
||||
|
||||
// Initialize finds and registers active displays. This is part of an experimental API.
|
||||
func Initialize() {
|
||||
activeDisplays := screenshot.NumActiveDisplays()
|
||||
for i := 0; i < activeDisplays; i++ {
|
||||
priority := driver.PriorityNormal
|
||||
|
@@ -22,6 +22,11 @@ func deviceID(num int) string {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Initialize()
|
||||
}
|
||||
|
||||
// Initialize finds and registers active displays. This is part of an experimental API.
|
||||
func Initialize() {
|
||||
dp, err := openDisplay()
|
||||
if err != nil {
|
||||
// No x11 display available.
|
||||
|
@@ -2,6 +2,7 @@ package screen
|
||||
|
||||
// #cgo pkg-config: x11 xext
|
||||
// #include <stdint.h>
|
||||
// #include <string.h>
|
||||
// #include <sys/shm.h>
|
||||
// #include <X11/Xlib.h>
|
||||
// #define XUTIL_DEFINE_FUNCTIONS
|
||||
@@ -125,6 +126,14 @@ func (c colorFunc) RGBA() (r, g, b, a uint32) {
|
||||
|
||||
func (s *shmImage) At(x, y int) color.Color {
|
||||
switch s.pixFmt {
|
||||
case pixFmtRGB24:
|
||||
addr := (x + y*int(s.img.width)) * 4
|
||||
r := uint32(s.b[addr]) * 0x100
|
||||
g := uint32(s.b[addr+1]) * 0x100
|
||||
b := uint32(s.b[addr+2]) * 0x100
|
||||
return colorFunc(func() (_, _, _, _ uint32) {
|
||||
return r, g, b, 0xFFFF
|
||||
})
|
||||
case pixFmtBGR24:
|
||||
addr := (x + y*int(s.img.width)) * 4
|
||||
b := uint32(s.b[addr]) * 0x100
|
||||
@@ -133,6 +142,15 @@ func (s *shmImage) At(x, y int) color.Color {
|
||||
return colorFunc(func() (_, _, _, _ uint32) {
|
||||
return r, g, b, 0xFFFF
|
||||
})
|
||||
case pixFmtRGB16:
|
||||
addr := (x + y*int(s.img.width)) * 2
|
||||
b1, b2 := s.b[addr], s.b[addr+1]
|
||||
r := uint32(b1>>3) * 0x100
|
||||
g := uint32((b1&0x7)<<3|(b2&0xE0)>>5) * 0x100
|
||||
b := uint32(b2&0x1F) * 0x100
|
||||
return colorFunc(func() (_, _, _, _ uint32) {
|
||||
return r, g, b, 0xFFFF
|
||||
})
|
||||
case pixFmtBGR16:
|
||||
addr := (x + y*int(s.img.width)) * 2
|
||||
b1, b2 := s.b[addr], s.b[addr+1]
|
||||
@@ -149,12 +167,25 @@ func (s *shmImage) At(x, y int) color.Color {
|
||||
|
||||
func (s *shmImage) RGBAAt(x, y int) color.RGBA {
|
||||
switch s.pixFmt {
|
||||
case pixFmtRGB24:
|
||||
addr := (x + y*int(s.img.width)) * 4
|
||||
r := s.b[addr]
|
||||
g := s.b[addr+1]
|
||||
b := s.b[addr+2]
|
||||
return color.RGBA{R: r, G: g, B: b, A: 0xFF}
|
||||
case pixFmtBGR24:
|
||||
addr := (x + y*int(s.img.width)) * 4
|
||||
b := s.b[addr]
|
||||
g := s.b[addr+1]
|
||||
r := s.b[addr+2]
|
||||
return color.RGBA{R: r, G: g, B: b, A: 0xFF}
|
||||
case pixFmtRGB16:
|
||||
addr := (x + y*int(s.img.width)) * 2
|
||||
b1, b2 := s.b[addr], s.b[addr+1]
|
||||
r := b1 >> 3
|
||||
g := (b1&0x7)<<3 | (b2&0xE0)>>5
|
||||
b := b2 & 0x1F
|
||||
return color.RGBA{R: r, G: g, B: b, A: 0xFF}
|
||||
case pixFmtBGR16:
|
||||
addr := (x + y*int(s.img.width)) * 2
|
||||
b1, b2 := s.b[addr], s.b[addr+1]
|
||||
@@ -178,11 +209,17 @@ func (s *shmImage) ToRGBA(dst *image.RGBA) *image.RGBA {
|
||||
dst.Pix = dst.Pix[:l]
|
||||
}
|
||||
switch s.pixFmt {
|
||||
case pixFmtRGB24:
|
||||
C.memcpy(unsafe.Pointer(&dst.Pix[0]), unsafe.Pointer(s.img.data), C.size_t(len(dst.Pix)))
|
||||
return dst
|
||||
case pixFmtBGR24:
|
||||
C.copyBGR24(unsafe.Pointer(&dst.Pix[0]), s.img.data, C.ulong(len(dst.Pix)))
|
||||
C.copyBGR24(unsafe.Pointer(&dst.Pix[0]), s.img.data, C.size_t(len(dst.Pix)))
|
||||
return dst
|
||||
case pixFmtRGB16:
|
||||
C.memcpy(unsafe.Pointer(&dst.Pix[0]), unsafe.Pointer(s.img.data), C.size_t(len(dst.Pix)))
|
||||
return dst
|
||||
case pixFmtBGR16:
|
||||
C.copyBGR16(unsafe.Pointer(&dst.Pix[0]), s.img.data, C.ulong(len(dst.Pix)))
|
||||
C.copyBGR16(unsafe.Pointer(&dst.Pix[0]), s.img.data, C.size_t(len(dst.Pix)))
|
||||
return dst
|
||||
default:
|
||||
panic("unsupported pixel format")
|
||||
@@ -199,8 +236,12 @@ func newShmImage(dp *C.Display, screen int) (*shmImage, error) {
|
||||
s := &shmImage{dp: dp}
|
||||
|
||||
switch {
|
||||
case v.red_mask == 0xFF && v.green_mask == 0xFF00 && v.blue_mask == 0xFF0000:
|
||||
s.pixFmt = pixFmtRGB24
|
||||
case v.red_mask == 0xFF0000 && v.green_mask == 0xFF00 && v.blue_mask == 0xFF:
|
||||
s.pixFmt = pixFmtBGR24
|
||||
case v.red_mask == 0x1F && v.green_mask == 0x7E0 && v.blue_mask == 0xF800:
|
||||
s.pixFmt = pixFmtRGB16
|
||||
case v.red_mask == 0xF800 && v.green_mask == 0x7E0 && v.blue_mask == 0x1F:
|
||||
s.pixFmt = pixFmtBGR16
|
||||
default:
|
||||
@@ -209,7 +250,7 @@ func newShmImage(dp *C.Display, screen int) (*shmImage, error) {
|
||||
return nil, errors.New("unsupported pixel format")
|
||||
}
|
||||
|
||||
s.shm.shmid = C.shmget(C.IPC_PRIVATE, C.ulong(w*h*4+8), C.IPC_CREAT|0600)
|
||||
s.shm.shmid = C.shmget(C.IPC_PRIVATE, C.size_t(w*h*4+8), C.IPC_CREAT|0600)
|
||||
if s.shm.shmid == -1 {
|
||||
return nil, errors.New("failed to get shared memory")
|
||||
}
|
||||
@@ -279,5 +320,5 @@ func (r *reader) Close() {
|
||||
|
||||
// cAlign64 is fot testing
|
||||
func cAlign64(ptr uintptr) uintptr {
|
||||
return uintptr(C.align64ForTest(C.ulong(uintptr(ptr))))
|
||||
return uintptr(C.align64ForTest(C.size_t(uintptr(ptr))))
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user