Compare commits

..

2 Commits

Author SHA1 Message Date
Lukas Herman
dda8d2502f Use square instead 2020-11-05 21:03:22 -08:00
Lukas Herman
d593404e39 WIP 2020-11-04 22:34:17 -08:00
199 changed files with 1254 additions and 12393 deletions

View File

@@ -11,18 +11,14 @@ jobs:
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false
matrix: matrix:
go: go: [ '1.15', '1.14' ]
- '1.21' # oldest version this package supports
- '1.22' # oldstable Go version
- '1.23' # stable Go version
name: Linux Go ${{ matrix.go }} name: Linux Go ${{ matrix.go }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Install dependencies - name: Install dependencies
@@ -32,54 +28,54 @@ jobs:
libopus-dev \ libopus-dev \
libva-dev \ libva-dev \
libvpx-dev \ libvpx-dev \
libx11-dev \ libx264-dev
libx264-dev \ - name: go vet
libxext-dev run: go vet $(go list ./... | grep -v mmal)
- name: Run Test Suite - name: go build
run: make test run: go build $(go list ./... | grep -v mmal)
- uses: codecov/codecov-action@v5 - name: go build without CGO
with: run: go build . pkg/...
token: ${{ secrets.CODECOV_TOKEN }} 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
build-darwin: build-darwin:
strategy:
fail-fast: false
matrix:
go:
- '1.22'
- '1.23'
runs-on: macos-latest runs-on: macos-latest
strategy:
matrix:
go: [ '1.15', '1.14' ]
name: Darwin Go ${{ matrix.go }} name: Darwin Go ${{ matrix.go }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Install dependencies - name: Install dependencies
run: | run: |
which brew
brew install \ brew install \
pkg-config \ pkg-config \
opus \ opus \
libvpx \ libvpx \
x264 x264
- name: Run Test Suite - name: go vet
run: make test run: go vet $(go list ./... | grep -v mmal)
- uses: codecov/codecov-action@v5 - name: go build
with: run: go build $(go list ./... | grep -v mmal)
token: ${{ secrets.CODECOV_TOKEN }} - name: go build without CGO
check-licenses: run: go build . pkg/...
runs-on: ubuntu-latest env:
name: Check Licenses CGO_ENABLED: 0
steps: - name: go test
- name: Checkout run: go test -v -race $(go list ./... | grep -v mmal)
uses: actions/checkout@v4 - name: go test without CGO
- name: Setup Go run: go test . pkg/... -v
uses: actions/setup-go@v5 env:
with: CGO_ENABLED: 0
go-version: stable
- name: Installing go-licenses
run: go install github.com/google/go-licenses@latest
- name: Checking licenses
run: go-licenses check ./...

View File

@@ -1,157 +0,0 @@
name: pkg/codec/ffmpeg CI
on:
pull_request:
branches:
- master
paths:
- pkg/codec/ffmepg
push:
branches:
- master
paths:
- pkg/codec/ffmepg
jobs:
build-linux:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
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@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install --no-install-recommends -y \
libopus-dev \
libva-dev \
libvpx-dev \
libx11-dev \
libx264-dev \
libxext-dev \
nasm \
yasm
- name: Cache FFmpeg build
uses: actions/cache@v4
id: ffmpeg-cache
with:
path: |
pkg/codec/ffmpeg/tmp/n7.0
key: ffmpeg-linux-n7.0-${{ hashFiles('pkg/codec/ffmpeg/Makefile') }}
restore-keys: |
ffmpeg-linux-n7.0-
- name: Check if FFmpeg libraries exist
id: ffmpeg-check
working-directory: pkg/codec/ffmpeg
run: |
echo "=== Checking FFmpeg cache status ==="
if [ -f "tmp/n7.0/lib/libavcodec.a" ] && [ -f "tmp/n7.0/lib/pkgconfig/libavcodec.pc" ]; then
echo "FFmpeg libraries found in cache"
echo "ffmpeg_exists=true" >> $GITHUB_OUTPUT
else
echo "FFmpeg libraries missing or incomplete"
ls -la tmp/ 2>/dev/null || echo "tmp directory does not exist"
ls -la tmp/n7.0/ 2>/dev/null || echo "n7.0 directory does not exist"
ls -la tmp/n7.0/lib/ 2>/dev/null || echo "lib directory does not exist"
echo "ffmpeg_exists=false" >> $GITHUB_OUTPUT
fi
- name: Build FFmpeg (if not cached or incomplete)
if: steps.ffmpeg-cache.outputs.cache-hit != 'true' || steps.ffmpeg-check.outputs.ffmpeg_exists != 'true'
working-directory: pkg/codec/ffmpeg
run: make build-ffmpeg
- name: Verify FFmpeg installation
working-directory: pkg/codec/ffmpeg
run: |
ls -la tmp/n7.0/
ls -la tmp/n7.0/lib/ || echo "lib directory not found"
ls -la tmp/n7.0/include/ || echo "include directory not found"
pkg-config --exists --print-errors libavcodec || echo "pkg-config check failed"
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/pkg/codec/ffmpeg/tmp/n7.0/lib/pkgconfig
- name: Run pkg/codec/ffmpeg Test Suite
working-directory: pkg/codec/ffmpeg
run: |
make test
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/pkg/codec/ffmpeg/tmp/n7.0/lib/pkgconfig
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
build-darwin:
strategy:
fail-fast: false
matrix:
go:
- '1.22'
- '1.23'
runs-on: macos-latest
name: Darwin Go ${{ matrix.go }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Install dependencies
run: |
brew install \
pkg-config \
opus \
libvpx \
x264
- name: Cache FFmpeg build
uses: actions/cache@v4
id: ffmpeg-cache
with:
path: |
pkg/codec/ffmpeg/tmp/n7.0
key: ffmpeg-darwin-n7.0-${{ hashFiles('pkg/codec/ffmpeg/Makefile') }}
restore-keys: |
ffmpeg-darwin-n7.0-
- name: Check if FFmpeg libraries exist
id: ffmpeg-check
working-directory: pkg/codec/ffmpeg
run: |
if [ -f "tmp/n7.0/lib/libavcodec.a" ] && [ -f "tmp/n7.0/lib/pkgconfig/libavcodec.pc" ]; then
echo "ffmpeg_exists=true" >> $GITHUB_OUTPUT
else
echo "ffmpeg_exists=false" >> $GITHUB_OUTPUT
fi
- name: Build FFmpeg (if not cached or incomplete)
if: steps.ffmpeg-cache.outputs.cache-hit != 'true' || steps.ffmpeg-check.outputs.ffmpeg_exists != 'true'
working-directory: pkg/codec/ffmpeg
run: make build-ffmpeg
- name: Run Test Suite
working-directory: pkg/codec/ffmpeg
run: |
make test
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/pkg/codec/ffmpeg/tmp/n7.0/lib/pkgconfig
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
check-licenses:
runs-on: ubuntu-latest
name: Check Licenses
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Installing go-licenses
run: go install github.com/google/go-licenses@latest
- name: Checking licenses
run: go-licenses check ./...

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
fetch-depth: 2 fetch-depth: 2
- name: fix - name: fix

5
.gitignore vendored
View File

@@ -10,8 +10,3 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
scripts/cross
coverage.txt
.idea

3
.gitmodules vendored
View File

@@ -0,0 +1,3 @@
[submodule "cvendor/src/openh264"]
path = cvendor/src/openh264
url = https://github.com/cisco/openh264.git

127
Makefile
View File

@@ -1,83 +1,56 @@
docker_owner := lherman vendor_dir = cvendor
docker_prefix := cross src_dir = $(vendor_dir)/src
toolchain_dockerfiles := dockerfiles lib_dir = $(vendor_dir)/lib
script_path := $(realpath scripts) include_dir = $(vendor_dir)/include
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 make_args.x86_64-windows = \
ifneq (,$$(findstring $(2)-$(3),$$(supported_platforms))) CC=x86_64-w64-mingw32-gcc \
$$(cmd_build)-$(1)-$(2)-$(3): toolchain-$(2)-$(3) CXX=x86_64-w64-mingw32-g++ \
$$(MAKE) --directory=$$(codec_dir)/$(1) \ ARCH=x86_64 \
MEDIADEVICES_TOOLCHAIN_BIN=$$(toolchain_path)/$(docker_prefix)-$(2)-$(3) \ OS=mingw_nt
MEDIADEVICES_TARGET_PLATFORM=$(2)-$(3) \ make_args.x86_64-darwin = \
MEDIADEVICES_TARGET_OS=$(2) \ CC=o64-clang \
MEDIADEVICES_TARGET_ARCH=$(3) CXX=o64-clang++ \
endif AR=llvm-ar \
endef ARCH=x86_64 \
OS=darwin
.PHONY: all .PHONY: vendor
all: $(cmd_test) $(cmd_build) vendor: \
$(include_dir)/openh264 \
cross-libraries
# Subcommand: $(include_dir)/openh264: $(src_dir)/openh264
# make build[-<codec_name>-<os>-<arch>] mkdir -p $@
# cp $^/codec/api/svc/*.h $@
# 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) $(lib_dir)/openh264/libopenh264.x86_64-linux.a: $(src_dir)/openh264
$(MAKE) --directory=$< "$*" \ $(MAKE) -C $^ clean \
MEDIADEVICES_DOCKER_OWNER=$(docker_owner) \ && $(MAKE) -C $^ libraries
MEDIADEVICES_DOCKER_PREFIX=$(docker_prefix) mkdir -p $(dir $@)
@mkdir -p $(toolchain_path) cp $^/libopenh264.a $@
@docker run $(docker_owner)/$(docker_prefix)-$* > \
$(toolchain_path)/$(docker_prefix)-$*
@chmod +x $(toolchain_path)/$(docker_prefix)-$*
$(foreach codec, $(codec_list), \ $(lib_dir)/openh264/libopenh264.x86_64-windows.a: $(src_dir)/openh264
$(foreach os, $(os_list), \ $(MAKE) -C $^ clean \
$(foreach arch, $(arch_list), \ && $(MAKE) -C $^ $(make_args.x86_64-windows) libraries
$(eval $(call BUILD_TEMPLATE,$(codec),$(os),$(arch)))))) mkdir -p $(dir $@)
cp $^/libopenh264.a $@
# Subcommand: $(lib_dir)/openh264/libopenh264.x86_64-darwin.a: $(src_dir)/openh264
# make test $(MAKE) -C $^ clean \
# && $(MAKE) -C $^ $(make_args.x86_64-darwin) libraries
# Description: mkdir -p $(dir $@)
# Run a series of tests cp $^/libopenh264.a $@
$(cmd_test):
go vet $(pkgs_without_ext_device) .PHONY: cross-libraries
go build $(pkgs_without_ext_device) cross-libraries:
# go build without CGO docker build -t mediadevices-libs-builder -f libs-builder.Dockerfile .
CGO_ENABLED=0 go build $(pkgs_without_cgo) docker run --rm \
# go build with CGO -v $(CURDIR):/go/src/github.com/pion/mediadevices \
CGO_ENABLED=1 go build $(pkgs_without_ext_device) mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-linux.a
$(MAKE) --directory=$(examples_dir) docker run --rm \
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(pkgs_without_ext_device) -v $(CURDIR):/go/src/github.com/pion/mediadevices \
mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-windows.a
docker run --rm \
-v $(CURDIR):/go/src/github.com/pion/mediadevices \
mediadevices-libs-builder make $(lib_dir)/openh264/libopenh264.x86_64-darwin.a

270
README.md
View File

@@ -5,246 +5,82 @@
</h1> </h1>
<h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4> <h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4>
<p align="center"> <p align="center">
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a> <a href="https://twitter.com/_pion?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40_pion&style=social&url=https%3A%2F%2Ftwitter.com%2F_pion" alt="Twitter Widget"></a> <a href="https://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>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/mediadevices/test.yaml"> <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://pkg.go.dev/badge/github.com/pion/mediadevices.svg" alt="Go Reference"></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://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a> <a href="https://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p> </p>
<br> <br>
`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! ![](img/demo.gif)
### Install ## Interfaces
```bash | Interface | Linux | Mac | Windows |
go get -u github.com/pion/mediadevices
```
### Usage
The following snippet shows how to capture a camera stream and store a frame as a jpeg image:
```go
package main
import (
"image/jpeg"
"os"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/prop"
// This is required to register camera adapter
_ "github.com/pion/mediadevices/pkg/driver/camera"
// Note: If you don't have a camera or your adapters are not supported,
// you can always swap your adapters with our dummy adapters below.
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
)
func main() {
stream, _ := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(constraint *mediadevices.MediaTrackConstraints) {
// Query for ideal resolutions
constraint.Width = prop.Int(600)
constraint.Height = prop.Int(400)
},
})
// Since track can represent audio as well, we need to cast it to
// *mediadevices.VideoTrack to get video specific functionalities
track := stream.GetVideoTracks()[0]
videoTrack := track.(*mediadevices.VideoTrack)
defer videoTrack.Close()
// Create a new video reader to get the decoded frames. Release is used
// to return the buffer to hold frame back to the source so that the buffer
// can be reused for the next frames.
videoReader := videoTrack.NewReader(false)
frame, release, _ := videoReader.Read()
defer release()
// Since frame is the standard image.Image, it's compatible with Go standard
// library. For example, capturing the first frame and store it as a jpeg image.
output, _ := os.Create("frame.jpg")
jpeg.Encode(output, frame, nil)
}
```
### More Examples
* [Webrtc](/examples/webrtc) - Use Webrtc to create a realtime peer-to-peer video call
* [Face Detection](/examples/facedetection) - Use a machine learning algorithm to detect faces in a camera stream
* [RTP Stream](examples/rtp) - Capture camera stream, encode it in H264/VP8/VP9, and send it to a RTP server
* [HTTP Broadcast](/examples/http) - Broadcast camera stream through HTTP with MJPEG
* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera
### Available Media Inputs
| Input | Linux | Mac | Windows |
| :--------: | :---: | :-: | :-----: | | :--------: | :---: | :-: | :-----: |
| Camera | ✔️ | ✔️ | ✔️ | | Camera | ✔️ | ✔️ | ✔️ |
| Microphone | ✔️ | | ✔️ | | Microphone | ✔️ | | ✔️ |
| Screen | ✔️ | ✔️ | ✔️ | | Screen | ✔️ | ✖️ | ✖️ |
By default, there's no media input registered. This decision was made to allow you to play only what you need. Therefore, you need to import the associated packages for the media inputs. For example, if you want to use a camera, you need to import the camera package as a side effect: ### Camera
```go | OS | Library/Interface |
import ( | :-----: | :---------------------------------------------------------------------: |
... | Linux | [Video4Linux](https://en.wikipedia.org/wiki/Video4Linux) |
_ "github.com/pion/mediadevices/pkg/driver/camera" | Mac | [AVFoundation](https://developer.apple.com/av-foundation/) |
) | Windows | [DirectShow](https://docs.microsoft.com/en-us/windows/win32/directshow) |
```
### Available Codecs | Pixel Format | Linux | Mac | Windows |
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`: | :---------------------------------------------------: | :---: | :-: | :-----: |
| [YUY2](https://www.fourcc.org/pixel-format/yuv-yuy2/) | ✔️ | ✖️ | ✔️ |
| [UYVY](https://www.fourcc.org/pixel-format/yuv-uyvy/) | ✔️ | ✔️ | ✖️ |
| [I420](https://www.fourcc.org/pixel-format/yuv-i420/) | ✔️ | ✖️ | ✖️ |
| [NV21](https://www.fourcc.org/pixel-format/yuv-nv21/) | ✔️ | ✔️ | ✖️ |
| [MJPEG](https://www.fourcc.org/mjpg/) | ✔️ | ✖️ | ✖️ |
```go ### Microphone
package main
import ( | OS | Library/Interface |
"github.com/pion/mediadevices" | :-----: | :---------------------------------------------------------------------: |
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder | Linux | [PulseAudio](https://en.wikipedia.org/wiki/PulseAudio) |
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter | Mac | N/A |
) | Windows | [waveIn](https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/) |
func main() { ### Screen casting
// configure codec specific parameters
x264Params, _ := x264.NewParams()
x264Params.Preset = x264.PresetMedium
x264Params.BitRate = 1_000_000 // 1mbps
codecSelector := mediadevices.NewCodecSelector( | OS | Library/Interface |
mediadevices.WithVideoEncoders(&x264Params), | :-----: | :---------------------------------------------------------------------: |
) | Linux | [X11](https://en.wikipedia.org/wiki/X_Window_System) |
| Mac | N/A |
mediaStream, _ := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ | Windows | N/A |
Video: func(c *mediadevices.MediaTrackConstraints) {},
Codec: codecSelector, // let GetUsermedia know available codecs
})
}
```
Since `mediadevices` doesn't implement the video/audio codecs, it needs to call the codec libraries from the system through cgo. Therefore, you're required to install the codec libraries before you can use them in `mediadevices`. In the next section, it shows a list of available codecs, where the packages are defined (documentation linked), and installation instructions. ## Codecs
Note: we do not provide recommendations on choosing one codec or another as it is very complex and can be subjective. | Audio Codec | Library/Interface |
| :---------: | :------------------------------------------------------: |
| OPUS | [libopus](http://opus-codec.org/) |
#### Video Codecs | Video Codec | Library/Interface |
| :---------: | :------------------------------------------------------: |
| H.264 | [OpenH264](https://www.openh264.org/) |
| VP8 | [libvpx](https://www.webmproject.org/code/) |
| VP9 | [libvpx](https://www.webmproject.org/code/) |
##### x264 ## Usage
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) [Wiki](https://github.com/pion/mediadevices/wiki)
* Installation:
* Mac: `brew install x264`
* Ubuntu: `apt install libx264-dev`
##### mmal
A framework to enable H264 hardware encoding for Raspberry Pi or boards that use VideoCore GPUs.
* Package: [github.com/pion/mediadevices/pkg/codec/mmal](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/mmal) ## Contributing
* Installation: no installation needed, mmal should come built in Raspberry Pi devices
##### openh264 - [Lukas Herman](https://github.com/lherman-cs) - _Original Author_
A codec library which supports H.264 encoding and decoding. It is suitable for use in real time applications. * [Atsushi Watanabe](https://github.com/at-wat) - _VP8, Screencast, etc._
* Package: [github.com/pion/mediadevices/pkg/codec/openh264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/openh264) ## Project Status
* Installation: no installation needed, included as a static binary
##### vpx [![Stargazers over time](https://starchart.cc/pion/mediadevices.svg)](https://starchart.cc/pion/mediadevices)
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) ## References
* Installation:
* Mac: `brew install libvpx`
* Ubuntu: `apt install libvpx-dev`
##### vaapi
An open source API that allows applications such as VLC media player or GStreamer to use hardware video acceleration capabilities (currently support VP8/VP9).
* Package: [github.com/pion/mediadevices/pkg/codec/vaapi](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/vaapi) - https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs
* Installation: - https://tools.ietf.org/html/rfc7742
* Ubuntu: `apt install libva-dev`
#### Video codecs implemented using ffmpeg
* Package: [github.com/pion/mediadevices/pkg/codec/ffmpeg](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/ffmpeg)
* Installation: You need to enable CGO, and provide the ffmpeg headers and libraries when compiling. For more detail, checkout
https://github.com/asticode/go-astiav?tab=readme-ov-file#install-ffmpeg-from-source.
* NVENC: If you want to use nvenc, you need to install [FFmpeg/nv-codec-headers](https://github.com/FFmpeg/nv-codec-headers) too.
Make sure that your driver's version is supported by the nv-codec-headers version you are installing.
To install it, clone the repo, checkout to wanted version, and `sudo make install`.
> Currently, only ffmpeg n7.0 and n7.1 are supported.
##### nvenc
Requires ffmpeg build with `--enable-nonfree --enable-nvenc`.
##### x264
Requires ffmpeg build with `--enable-libx264 --enable-gpl`.
#### Audio Codecs
##### opus
A totally open, royalty-free, highly versatile audio codec.
* Package: [github.com/pion/mediadevices/pkg/codec/opus](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/opus)
* Installation:
* Mac: `brew install opus`
* Ubuntu: `apt install libopus-dev`
### Benchmark
Result as of Nov 4, 2020 with Go 1.14 on a Raspberry pi 3, `mediadevices` can produce video, encode, send across network, and decode at **720p, 30 fps with < 500 ms latency**.
The test was taken by capturing a camera stream, decoding the raw frames, encoding the video stream with mmal, and sending the stream through Webrtc.
### FAQ
#### Failed to find the best driver that fits the constraints
`mediadevices` provides an automated driver discovery through `GetUserMedia` and `GetDisplayMedia`. The driver discover algorithm works something like:
1. Open all registered drivers
2. Get all properties (property describes what a driver is capable of, e.g. resolution, frame rate, etc.) from opened drivers
3. Find the best property that meets the criteria
So, when `mediadevices` returns `failed to find the best driver that fits the constraints` error, one of the following conditions might have occured:
* Driver was not imported as a side effect in your program, e.g. `import _ github.com/pion/mediadevices/pkg/driver/camera`
* Your constraint is too strict that there's no driver can fullfil your requirements. In this case, you can try to turn up the debug level by specifying the following environment variable: `export PION_LOG_DEBUG=all` to see what was too strict and tune that.
* Your driver is not supported/implemented. In this case, you can either let us know (file an issue) and wait for the maintainers to implement it. Or, you can implement it yourself and register it through `RegisterDriverAdapter`
* If trying to use `import _ github.com/pion/mediadevices/pkg/driver/screen` note that you will need to use `GetDisplayMedia` instead of `GetUserMedia`
#### Failed to find vpx/x264/mmal/opus codecs
Since `mediadevices` uses cgo to access video/audio codecs, it needs to find these libraries from the system. To accomplish this, [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) is used for library discovery.
If you see the following error message at compile time:
```bash
# pkg-config --cflags -- vpx
Package vpx was not found in the pkg-config search path.
Perhaps you should add the directory containing `vpx.pc'
to the PKG_CONFIG_PATH environment variable
No package 'vpx' found
pkg-config: exit status 1
```
There are 2 common problems:
* The required codec library is not installed (vpx in this example). In this case, please refer to the [available codecs](#available-codecs).
* Pkg-config fails to find the `.pc` files for this codec ([reference](https://people.freedesktop.org/~dbn/pkg-config-guide.html#using)). In this case, you need to find where the codec library's `.pc` is stored, and let pkg-config knows with: `export PKG_CONFIG_PATH=/path/to/directory`.
### Roadmap
The library can be used with our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
### Community
Pion has an active community on [Discord](https://discord.gg/PngbdqpFbt).
Follow the [Pion Twitter](https://twitter.com/_pion) and the [Pion Bluesky](https://bsky.app/profile/pion.ly) for project updates and important WebRTC news.
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible
### License
MIT License - see [LICENSE](LICENSE) for full text

View File

@@ -9,7 +9,7 @@ import (
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v2"
) )
// CodecSelector is a container of video and audio encoder builders, which later will be used // CodecSelector is a container of video and audio encoder builders, which later will be used
@@ -50,15 +50,14 @@ func NewCodecSelector(opts ...CodecSelectorOption) *CodecSelector {
// Populate lets the webrtc engine be aware of supported codecs that are contained in CodecSelector // Populate lets the webrtc engine be aware of supported codecs that are contained in CodecSelector
func (selector *CodecSelector) Populate(setting *webrtc.MediaEngine) { func (selector *CodecSelector) Populate(setting *webrtc.MediaEngine) {
for _, encoder := range selector.videoEncoders { for _, encoder := range selector.videoEncoders {
setting.RegisterCodec(encoder.RTPCodec().RTPCodecParameters, webrtc.RTPCodecTypeVideo) setting.RegisterCodec(encoder.RTPCodec().RTPCodec)
} }
for _, encoder := range selector.audioEncoders { for _, encoder := range selector.audioEncoders {
setting.RegisterCodec(encoder.RTPCodec().RTPCodecParameters, webrtc.RTPCodecTypeAudio) setting.RegisterCodec(encoder.RTPCodec().RTPCodec)
} }
} }
// selectVideoCodecByNames selects a single codec that can be built and matched. codecNames can be formatted as "video/<codecName>" or "<codecName>"
func (selector *CodecSelector) selectVideoCodecByNames(reader video.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) { func (selector *CodecSelector) selectVideoCodecByNames(reader video.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) {
var selectedEncoder codec.VideoEncoderBuilder var selectedEncoder codec.VideoEncoderBuilder
var encodedReader codec.ReadCloser var encodedReader codec.ReadCloser
@@ -67,10 +66,8 @@ func (selector *CodecSelector) selectVideoCodecByNames(reader video.Reader, inpu
outer: outer:
for _, wantCodec := range codecNames { for _, wantCodec := range codecNames {
wantCodecLower := strings.ToLower(wantCodec)
for _, encoder := range selector.videoEncoders { for _, encoder := range selector.videoEncoders {
// MimeType is formated as "video/<codecName>" if encoder.RTPCodec().Name == wantCodec {
if strings.HasSuffix(strings.ToLower(encoder.RTPCodec().MimeType), wantCodecLower) {
encodedReader, err = encoder.BuildVideoEncoder(reader, inputProp) encodedReader, err = encoder.BuildVideoEncoder(reader, inputProp)
if err == nil { if err == nil {
selectedEncoder = encoder selectedEncoder = encoder
@@ -78,7 +75,7 @@ outer:
} }
} }
errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().MimeType, err)) errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().Name, err))
} }
} }
@@ -89,17 +86,16 @@ outer:
return encodedReader, selectedEncoder.RTPCodec(), nil return encodedReader, selectedEncoder.RTPCodec(), nil
} }
func (selector *CodecSelector) selectVideoCodec(reader video.Reader, inputProp prop.Media, codecs ...webrtc.RTPCodecParameters) (codec.ReadCloser, *codec.RTPCodec, error) { func (selector *CodecSelector) selectVideoCodec(reader video.Reader, inputProp prop.Media, codecs ...*webrtc.RTPCodec) (codec.ReadCloser, *codec.RTPCodec, error) {
var codecNames []string var codecNames []string
for _, codec := range codecs { for _, codec := range codecs {
codecNames = append(codecNames, codec.MimeType) codecNames = append(codecNames, codec.Name)
} }
return selector.selectVideoCodecByNames(reader, inputProp, codecNames...) return selector.selectVideoCodecByNames(reader, inputProp, codecNames...)
} }
// selectAudioCodecByNames selects a single codec that can be built and matched. codecNames can be formatted as "audio/<codecName>" or "<codecName>"
func (selector *CodecSelector) selectAudioCodecByNames(reader audio.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) { func (selector *CodecSelector) selectAudioCodecByNames(reader audio.Reader, inputProp prop.Media, codecNames ...string) (codec.ReadCloser, *codec.RTPCodec, error) {
var selectedEncoder codec.AudioEncoderBuilder var selectedEncoder codec.AudioEncoderBuilder
var encodedReader codec.ReadCloser var encodedReader codec.ReadCloser
@@ -108,10 +104,8 @@ func (selector *CodecSelector) selectAudioCodecByNames(reader audio.Reader, inpu
outer: outer:
for _, wantCodec := range codecNames { for _, wantCodec := range codecNames {
wantCodecLower := strings.ToLower(wantCodec)
for _, encoder := range selector.audioEncoders { for _, encoder := range selector.audioEncoders {
// MimeType is formated as "audio/<codecName>" if encoder.RTPCodec().Name == wantCodec {
if strings.HasSuffix(strings.ToLower(encoder.RTPCodec().MimeType), wantCodecLower) {
encodedReader, err = encoder.BuildAudioEncoder(reader, inputProp) encodedReader, err = encoder.BuildAudioEncoder(reader, inputProp)
if err == nil { if err == nil {
selectedEncoder = encoder selectedEncoder = encoder
@@ -119,7 +113,7 @@ outer:
} }
} }
errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().MimeType, err)) errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().Name, err))
} }
} }
@@ -130,11 +124,11 @@ outer:
return encodedReader, selectedEncoder.RTPCodec(), nil return encodedReader, selectedEncoder.RTPCodec(), nil
} }
func (selector *CodecSelector) selectAudioCodec(reader audio.Reader, inputProp prop.Media, codecs ...webrtc.RTPCodecParameters) (codec.ReadCloser, *codec.RTPCodec, error) { func (selector *CodecSelector) selectAudioCodec(reader audio.Reader, inputProp prop.Media, codecs ...*webrtc.RTPCodec) (codec.ReadCloser, *codec.RTPCodec, error) {
var codecNames []string var codecNames []string
for _, codec := range codecs { for _, codec := range codecs {
codecNames = append(codecNames, codec.MimeType) codecNames = append(codecNames, codec.Name)
} }
return selector.selectAudioCodecByNames(reader, inputProp, codecNames...) return selector.selectAudioCodecByNames(reader, inputProp, codecNames...)

View File

@@ -167,8 +167,8 @@ typedef enum {
DECODER_OPTION_LEVEL, ///< get current AU level info,only is used in GetOption DECODER_OPTION_LEVEL, ///< get current AU level info,only is used in GetOption
DECODER_OPTION_STATISTICS_LOG_INTERVAL,///< set log output interval DECODER_OPTION_STATISTICS_LOG_INTERVAL,///< set log output interval
DECODER_OPTION_IS_REF_PIC, ///< feedback current frame is ref pic or not DECODER_OPTION_IS_REF_PIC, ///< feedback current frame is ref pic or not
DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER, ///< number of frames remaining in decoder buffer when pictures are required to re-ordered into display-order. DECODER_OPTION_NUM_OF_FRAMES_REMAINING_IN_BUFFER ///< number of frames remaining in decoder buffer when pictures are required to re-ordered into display-order.
DECODER_OPTION_NUM_OF_THREADS, ///< number of decoding threads. The maximum thread count is equal or less than lesser of (cpu core counts and 16).
} DECODER_OPTION; } DECODER_OPTION;
/** /**

View File

@@ -201,7 +201,6 @@ typedef struct TagBufferInfo {
union { union {
SSysMEMBuffer sSystemBuffer; ///< memory info for one picture SSysMEMBuffer sSystemBuffer; ///< memory info for one picture
} UsrData; ///< output buffer info } UsrData; ///< output buffer info
unsigned char* pDst[3]; //point to picture YUV data
} SBufferInfo; } SBufferInfo;

View File

@@ -0,0 +1,15 @@
//The current file is auto-generated by script: generate_codec_ver.sh
#ifndef CODEC_VER_H
#define CODEC_VER_H
#include "codec_app_def.h"
static const OpenH264Version g_stCodecVersion = {2, 0, 0, 1905};
static const char* const g_strCodecVer = "OpenH264 version:2.0.0.1905";
#define OPENH264_MAJOR (2)
#define OPENH264_MINOR (0)
#define OPENH264_REVISION (0)
#define OPENH264_RESERVED (1905)
#endif // CODEC_VER_H

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
cvendor/src/openh264 Submodule

Submodule cvendor/src/openh264 added at 71374015cd

View File

@@ -1,11 +0,0 @@
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

View File

@@ -1,47 +0,0 @@
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}

View File

@@ -1,8 +0,0 @@
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})

View File

@@ -1,23 +0,0 @@
FROM dockcross/base
ENV OSX_CROSS_PATH=/osxcross
COPY --from=dockercore/golang-cross "${OSX_CROSS_PATH}/." "${OSX_CROSS_PATH}/"
ENV PATH=${OSX_CROSS_PATH}/target/bin:$PATH
COPY init.sh /tmp/init.sh
RUN bash /tmp/init.sh
ENV CC=x86_64-apple-darwin14-clang \
CXX=x86_64-apple-darwin14-clang++ \
CPP=x86_64-apple-darwin14-clang++ \
AR=x86_64-apple-darwin14-ar \
AS=x86_64-apple-darwin14-as \
LD=x86_64-apple-darwin14-ld
COPY darwin-x64.cmake ${OSX_CROSS_PATH}/
ENV CMAKE_TOOLCHAIN_FILE ${OSX_CROSS_PATH}/darwin-x64.cmake
ARG IMAGE=lherman/cross-darwin-x64
ARG VERSION=latest
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}

View File

@@ -1,8 +0,0 @@
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_VERSION 1)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER $ENV{CC})
set(CMAKE_CXX_COMPILER $ENV{CXX})
set(CMAKE_AR $ENV{AR})
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})

View File

@@ -1,7 +0,0 @@
#!/bin/bash
apt-get update
apt-get install -y nasm clang llvm
curl -L https://golang.org/dl/go1.15.6.linux-amd64.tar.gz | tar -C /usr/local -xzf -
ln -s /usr/local/go/bin/go /usr/local/bin/go

View File

@@ -1,8 +0,0 @@
FROM dockcross/linux-arm64
ARG IMAGE=lherman/cross-linux-arm64
ARG VERSION=latest
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
COPY init.sh /tmp/init.sh
RUN bash /tmp/init.sh

View File

@@ -1,8 +0,0 @@
FROM dockcross/linux-armv7
ARG IMAGE=lherman/cross-linux-armv7
ARG VERSION=latest
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
COPY init.sh /tmp/init.sh
RUN bash /tmp/init.sh

View File

@@ -1,8 +0,0 @@
FROM dockcross/linux-x64
ARG IMAGE=lherman/cross-linux-x64
ARG VERSION=latest
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
COPY init.sh /tmp/init.sh
RUN bash /tmp/init.sh

View File

@@ -1,11 +0,0 @@
FROM dockcross/windows-static-x64-posix
ARG IMAGE=lherman/cross-windows-x64
ARG VERSION=latest
ENV DEFAULT_DOCKCROSS_IMAGE ${IMAGE}:${VERSION}
COPY init.sh /tmp/init.sh
RUN bash /tmp/init.sh
RUN ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-gcc /usr/bin/x86_64-w64-mingw32-gcc && \
ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-g++ /usr/bin/x86_64-w64-mingw32-g++ && \
ln -s /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static.posix-ar /usr/bin/x86_64-w64-mingw32-ar

1
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
go.sum

View File

@@ -1,8 +0,0 @@
examples := $(shell find * -maxdepth 0 -type d)
examples := $(filter-out internal,$(examples))
.PHONY: all $(examples)
all: $(examples)
$(examples):
cd $@ && go build -mod=mod

View File

@@ -1 +0,0 @@
archive

View File

@@ -1,38 +0,0 @@
## Instructions
### Install required codecs
In this example, we'll be using x264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
Installation steps:
* [x264](https://github.com/pion/mediadevices#x264)
### Download archive examplee
```
git clone https://github.com/pion/mediadevices.git
```
### Run archive example
Run `cd mediadevices/examples/archive && go build && ./archive recorded.h264`
To stop recording, press `Ctrl+c` or send a SIGINT signal.
### Playback recorded video
Install GStreamer and run:
```
gst-launch-1.0 playbin uri=file://${PWD}/recorded.h264
```
Or run VLC media plyer:
```
vlc recorded.h264
```
A video should start playing in your GStreamer or VLC window.
Congrats, you have used pion-MediaDevices! Now start building something cool

View File

@@ -1,82 +0,0 @@
package main
import (
"fmt"
"image"
"io"
"os"
"os/signal"
"syscall"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Printf("usage: %s <path/to/file.h264>\n", os.Args[0])
return
}
dest := os.Args[1]
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT)
x264Params, err := x264.NewParams()
must(err)
x264Params.Preset = x264.PresetMedium
x264Params.BitRate = 1_000_000 // 1mbps
codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&x264Params),
)
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatI420)
c.Width = prop.Int(640)
c.Height = prop.Int(480)
},
Codec: codecSelector,
})
must(err)
videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack)
defer videoTrack.Close()
videoTrack.Transform(video.TransformFunc(func(r video.Reader) video.Reader {
return video.ReaderFunc(func() (img image.Image, release func(), err error) {
// we send io.EOF signal to the encoder reader to stop reading. Therefore, io.Copy
// will finish its execution and the program will finish
select {
case <-sigs:
return nil, func() {}, io.EOF
default:
}
return r.Read()
})
}))
reader, err := videoTrack.NewEncodedIOReader(x264Params.RTPCodec().MimeType)
must(err)
defer reader.Close()
out, err := os.Create(dest)
must(err)
fmt.Println("Recording... Press Ctrl+c to stop")
_, err = io.Copy(out, reader)
must(err)
fmt.Println("Your video has been recorded to", dest)
}

View File

@@ -1 +0,0 @@
facedetection

View File

@@ -1,15 +0,0 @@
## Instructions
### Download facedetection example
```
git clone https://github.com/pion/mediadevices.git
```
### Compile and Run facedetection
Run `cd mediadevices/examples/facedetection && go build && ./facedetection`
You should be able to see some loggings when it can see faces.
Congrats, you have used pion-MediaDevices! Now start building something cool

View File

@@ -1,20 +1,26 @@
package main package main
import ( import (
"fmt"
"image" "image"
"io/ioutil" "io/ioutil"
"log" "log"
"time" "net"
"os"
pigo "github.com/esimov/pigo/core" pigo "github.com/esimov/pigo/core"
"github.com/pion/mediadevices" "github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use h264 video encoder
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter _ "github.com/pion/mediadevices/pkg/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" "github.com/pion/mediadevices/pkg/prop"
) )
const ( const (
confidenceLevel = 5.0 confidenceLevel = 9.5
mtu = 1000
thickness = 5
) )
var ( var (
@@ -28,7 +34,7 @@ func must(err error) {
} }
} }
func detectFace(frame *image.YCbCr) bool { func detectFaces(frame *image.YCbCr) []pigo.Detection {
bounds := frame.Bounds() bounds := frame.Bounds()
cascadeParams := pigo.CascadeParams{ cascadeParams := pigo.CascadeParams{
MinSize: 100, MinSize: 100,
@@ -49,17 +55,77 @@ func detectFace(frame *image.YCbCr) bool {
// Calculate the intersection over union (IoU) of two clusters. // Calculate the intersection over union (IoU) of two clusters.
dets = classifier.ClusterDetections(dets, 0) dets = classifier.ClusterDetections(dets, 0)
return dets
}
for _, det := range dets { func drawRect(frame *image.YCbCr, x0, y0, size int) {
if det.Q >= confidenceLevel { if x0 < 0 {
return true x0 = 0
}
if y0 < 0 {
y0 = 0
}
width := frame.Bounds().Dx()
height := frame.Bounds().Dy()
x1 := x0 + size
y1 := y0 + size
if x1 >= width {
x1 = width - 1
}
if y1 >= height {
y1 = height - 1
}
convert := func(x, y int) int {
return y*width + x
}
for x := x0; x < x1; x++ {
for t := 0; t < thickness; t++ {
frame.Y[convert(x, y0+t)] = 0
frame.Y[convert(x, y1-t)] = 0
} }
} }
return false for y := y0; y < y1; y++ {
for t := 0; t < thickness; t++ {
frame.Y[convert(x0+t, y)] = 0
frame.Y[convert(x1-t, y)] = 0
}
}
}
func detectFace(r video.Reader) video.Reader {
return video.ReaderFunc(func() (img image.Image, release func(), err error) {
img, release, err = r.Read()
if err != nil {
return
}
yuv := img.(*image.YCbCr)
dets := detectFaces(yuv)
for _, det := range dets {
if det.Q < confidenceLevel {
continue
}
drawRect(yuv, det.Col-det.Scale/2, det.Row-det.Scale/2, det.Scale)
}
return
})
} }
func main() { func main() {
if len(os.Args) != 2 {
fmt.Printf("usage: %s host:port\n", os.Args[0])
return
}
dest := os.Args[1]
// prepare face detector // prepare face detector
var err error var err error
cascade, err = ioutil.ReadFile("facefinder") cascade, err = ioutil.ReadFile("facefinder")
@@ -75,12 +141,21 @@ func main() {
log.Fatalf("Error unpacking the cascade file: %s", err) log.Fatalf("Error unpacking the cascade file: %s", err)
} }
vp8Params, err := vpx.NewVP8Params()
must(err)
vp8Params.BitRate = 1_000_000 // 100kbps
codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&vp8Params),
)
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) { Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormatOneOf{frame.FormatI420, frame.FormatYUY2} c.FrameFormat = prop.FrameFormatExact(frame.FormatUYVY)
c.Width = prop.Int(640) c.Width = prop.Int(640)
c.Height = prop.Int(480) c.Height = prop.Int(480)
}, },
Codec: codecSelector,
}) })
must(err) must(err)
@@ -88,18 +163,27 @@ func main() {
videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack) videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack)
defer videoTrack.Close() defer videoTrack.Close()
videoReader := videoTrack.NewReader(false) videoTrack.Transform(detectFace)
// To save resources, we can simply use 4 fps to detect faces.
ticker := time.NewTicker(time.Millisecond * 250)
defer ticker.Stop()
for range ticker.C { rtpReader, err := videoTrack.NewRTPReader(vp8Params.RTPCodec().Name, mtu)
frame, release, err := videoReader.Read() must(err)
addr, err := net.ResolveUDPAddr("udp", dest)
must(err)
conn, err := net.DialUDP("udp", nil, addr)
must(err)
buff := make([]byte, mtu)
for {
pkts, release, err := rtpReader.Read()
must(err) must(err)
// Since we asked the frame format to be exactly I420/YUY2 in GetUserMedia, we can guarantee that it must be YCbCr for _, pkt := range pkts {
if detectFace(frame.(*image.YCbCr)) { n, err := pkt.MarshalTo(buff)
log.Println("Detect a face") must(err)
_, err = conn.Write(buff[:n])
must(err)
} }
release() release()

View File

@@ -1,37 +1,9 @@
module github.com/pion/mediadevices/examples module github.com/pion/mediadevices/examples
go 1.21 go 1.14
require ( // Please don't commit require entries of examples.
github.com/esimov/pigo v1.4.6 // `git checkout master examples/go.mod` to revert this file.
github.com/pion/mediadevices v0.0.0 require github.com/pion/mediadevices v0.0.0
github.com/pion/webrtc/v4 v4.1.2
)
require (
github.com/blackjack/webcam v0.6.1 // indirect
github.com/gen2brain/malgo v0.11.23 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)
replace github.com/pion/mediadevices v0.0.0 => ../ replace github.com/pion/mediadevices v0.0.0 => ../

View File

@@ -1,67 +0,0 @@
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1 +0,0 @@
http

View File

@@ -1,19 +0,0 @@
## Instructions
### Download http example
```
git clone https://github.com/pion/mediadevices.git
```
### Compile and Run HTTP server
Run `cd mediadevices/examples/http && go build && ./http :1313`
### Access the camera stream from the browser
Go to "http://localhost:1313"
Congrats, you have used pion-MediaDevices! Now start building something cool

View File

@@ -11,7 +11,6 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/textproto" "net/textproto"
"os"
"github.com/pion/mediadevices" "github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
@@ -29,13 +28,7 @@ func must(err error) {
} }
func main() { func main() {
if len(os.Args) != 2 { s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
fmt.Printf("usage: %s host:port\n", os.Args[0])
return
}
dest := os.Args[1]
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(constraint *mediadevices.MediaTrackConstraints) { Video: func(constraint *mediadevices.MediaTrackConstraints) {
constraint.Width = prop.Int(600) constraint.Width = prop.Int(600)
constraint.Height = prop.Int(400) constraint.Height = prop.Int(400)
@@ -43,9 +36,8 @@ func main() {
}) })
must(err) must(err)
track := mediaStream.GetVideoTracks()[0] t := s.GetVideoTracks()[0]
videoTrack := track.(*mediadevices.VideoTrack) videoTrack := t.(*mediadevices.VideoTrack)
defer videoTrack.Close()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var buf bytes.Buffer var buf bytes.Buffer
@@ -80,6 +72,6 @@ func main() {
} }
}) })
fmt.Printf("listening on %s\n", dest) fmt.Println("listening on http://localhost:1313")
log.Println(http.ListenAndServe(dest, nil)) log.Println(http.ListenAndServe("localhost:1313", nil))
} }

View File

@@ -1 +0,0 @@
openh264

View File

@@ -1,22 +0,0 @@
## 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.

View File

@@ -1,96 +0,0 @@
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(&params),
)
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)
}

View File

@@ -1 +0,0 @@
rtp

View File

@@ -1,35 +1,27 @@
## Instructions ## Instructions
### Install required codecs ### Download rtpexample
In this example, we'll be using x264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
Installation steps:
* [x264](https://github.com/pion/mediadevices#x264)
### Download rtp example
``` ```
git clone https://github.com/pion/mediadevices.git go get github.com/pion/mediadevices/examples/rtp
``` ```
### Listen RTP ### Listen RTP
Install GStreamer and run: Install GStreamer and run:
``` ```
gst-launch-1.0 udpsrc port=5000 caps=application/x-rtp,encode-name=H264 \ gst-launch-1.0 udpsrc port=5000 caps=application/x-rtp,encode-name=VP8 \
! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink ! rtpvp8depay ! vp8dec ! videoconvert ! autovideosink
``` ```
Or run VLC media plyer: Or run VLC media plyer:
``` ```
vlc ./h264.sdp vlc ./vp8.sdp
``` ```
### Run rtp ### Run rtp
Run `cd mediadevices/examples/archive && go build && ./rtp localhost:5000` Run `rtp localhost:5000`
A video should start playing in your GStreamer or VLC window. A video should start playing in your GStreamer or VLC window.
It's not WebRTC, but pure RTP. It's not WebRTC, but pure RTP.

View File

@@ -2,12 +2,11 @@ package main
import ( import (
"fmt" "fmt"
"math/rand"
"net" "net"
"os" "os"
"github.com/pion/mediadevices" "github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter _ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
"github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
@@ -30,18 +29,17 @@ func main() {
} }
dest := os.Args[1] dest := os.Args[1]
x264Params, err := x264.NewParams() vp8Params, err := vpx.NewVP8Params()
must(err) must(err)
x264Params.Preset = x264.PresetMedium vp8Params.BitRate = 100000 // 100kbps
x264Params.BitRate = 1_000_000 // 1mbps
codecSelector := mediadevices.NewCodecSelector( codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&x264Params), mediadevices.WithVideoEncoders(&vp8Params),
) )
mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) { Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatI420) c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Width = prop.Int(640) c.Width = prop.Int(640)
c.Height = prop.Int(480) c.Height = prop.Int(480)
}, },
@@ -52,7 +50,7 @@ func main() {
videoTrack := mediaStream.GetVideoTracks()[0] videoTrack := mediaStream.GetVideoTracks()[0]
defer videoTrack.Close() defer videoTrack.Close()
rtpReader, err := videoTrack.NewRTPReader(x264Params.RTPCodec().MimeType, rand.Uint32(), mtu) rtpReader, err := videoTrack.NewRTPReader(vp8Params.RTPCodec().Name, mtu)
must(err) must(err)
addr, err := net.ResolveUDPAddr("udp", dest) addr, err := net.ResolveUDPAddr("udp", dest)

View File

@@ -6,4 +6,4 @@ c=IN IP4 0.0.0.0
t=0 0 t=0 0
a=recvonly a=recvonly
m=video 5000 RTP/AVP 100 m=video 5000 RTP/AVP 100
a=rtpmap:100 H264/90000 a=rtpmap:100 VP8/90000

View File

@@ -1 +0,0 @@
vnc

View File

@@ -1,46 +0,0 @@
## 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

View File

@@ -1,127 +0,0 @@
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 {}
}

View File

@@ -1 +0,0 @@
webrtc

View File

@@ -1,42 +1,29 @@
## Instructions ## Instructions
### Install required codecs ### Download gstreamer-send
In this example, we'll be using x264 and opus as our video and audio codecs. Therefore, we need to make sure that these codecs are installed within our system.
Installation steps:
* [x264](https://github.com/pion/mediadevices#x264)
* [opus](https://github.com/pion/mediadevices#opus)
### Download webrtc example
``` ```
git clone https://github.com/pion/mediadevices.git go get github.com/pion/mediadevices/examples/webrtc
```
#### Compile webrtc example
```
cd mediadevices/examples/webrtc && go build
``` ```
### Open example page ### 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 [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 ### Run webrtc 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>` In the jsfiddle the top textarea is your browser, copy that and:
Run `echo $SDP | ./webrtc` #### Linux
Run `echo $BROWSER_SDP | webrtc`
### Input webrtc's SessionDescription into your browser ### Input webrtc's SessionDescription into your browser
Copy the text that `./webrtc` just emitted and copy into second text area Copy the text that `webrtc` just emitted and copy into second text area
### Hit 'Start Session' in jsfiddle, enjoy your video! ### 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. 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 Congrats, you have used pion-WebRTC! Now start building something cool

View File

@@ -7,7 +7,7 @@ import (
"github.com/pion/mediadevices/examples/internal/signal" "github.com/pion/mediadevices/examples/internal/signal"
"github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v2"
// If you don't like x264, you can also use vpx by importing as below // If you don't like x264, you can also use vpx by importing as below
// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder // "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder
@@ -26,6 +26,10 @@ import (
_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter _ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
) )
const (
videoCodecName = webrtc.VP8
)
func main() { func main() {
config := webrtc.Configuration{ config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{ ICEServers: []webrtc.ICEServer{
@@ -57,7 +61,10 @@ func main() {
mediaEngine := webrtc.MediaEngine{} mediaEngine := webrtc.MediaEngine{}
codecSelector.Populate(&mediaEngine) codecSelector.Populate(&mediaEngine)
api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine)) if err := mediaEngine.PopulateFromSDP(offer); err != nil {
panic(err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(config) peerConnection, err := api.NewPeerConnection(config)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -71,7 +78,7 @@ func main() {
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) { Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatI420) c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Width = prop.Int(640) c.Width = prop.Int(640)
c.Height = prop.Int(480) c.Height = prop.Int(480)
}, },
@@ -83,14 +90,20 @@ func main() {
panic(err) panic(err)
} }
for _, track := range s.GetTracks() { for _, tracker := range s.GetTracks() {
track.OnEnded(func(err error) { tracker.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s) ended with error: %v\n", fmt.Printf("Track (ID: %s) ended with error: %v\n",
track.ID(), err) tracker.ID(), err)
}) })
_, err = peerConnection.AddTransceiverFromTrack(track, // In Pion/webrtc v3, bind will be called automatically after SDP negotiation
webrtc.RTPTransceiverInit{ webrtcTrack, err := tracker.Bind(peerConnection)
if err != nil {
panic(err)
}
_, err = peerConnection.AddTransceiverFromTrack(webrtcTrack,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly, Direction: webrtc.RTPTransceiverDirectionSendonly,
}, },
) )
@@ -111,23 +124,13 @@ func main() {
panic(err) panic(err)
} }
// Create channel that is blocked until ICE Gathering is complete
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
// Sets the LocalDescription, and starts our UDP listeners // Sets the LocalDescription, and starts our UDP listeners
err = peerConnection.SetLocalDescription(answer) err = peerConnection.SetLocalDescription(answer)
if err != nil { if err != nil {
panic(err) 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 // Output the answer in base64 so we can paste it in browser
fmt.Println(signal.Encode(*peerConnection.LocalDescription())) fmt.Println(signal.Encode(answer))
// Block forever
select {} select {}
} }

48
go.mod
View File

@@ -1,43 +1,15 @@
module github.com/pion/mediadevices module github.com/pion/mediadevices
go 1.21 go 1.13
require ( require (
github.com/blackjack/webcam v0.6.1 github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539
github.com/gen2brain/malgo v0.11.23 github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93
github.com/google/uuid v1.6.0 github.com/lherman-cs/opus v0.0.2
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 github.com/pion/logging v0.2.2
github.com/pion/interceptor v0.1.40 github.com/pion/rtp v1.6.0
github.com/pion/logging v0.2.4 github.com/pion/webrtc/v2 v2.2.26
github.com/pion/rtcp v1.2.15 github.com/satori/go.uuid v1.2.0
github.com/pion/rtp v1.8.19 golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
github.com/pion/webrtc/v4 v4.1.2 golang.org/x/sys v0.0.0-20201029080932-201ba4db2418
github.com/stretchr/testify v1.10.0
golang.org/x/image v0.23.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gen2brain/shm v0.1.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

187
go.sum
View File

@@ -1,76 +1,129 @@
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE= github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 h1:1aIqYfg9s9RETAJHGfVKZW4ok0b22p4QTwk8MsdRtPs=
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI= github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93 h1:gDcaH96SZ7q1JU6hj0tSv8FiuqadFERU17lLxhphLa8=
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lherman-cs/opus v0.0.2 h1:fE9Du3NKXDBztqvoTd6P2y9eJ9vgIHahGK8yQostnhA=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lherman-cs/opus v0.0.2/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk=
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo= github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM= github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/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-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
img/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

View File

@@ -2,18 +2,12 @@ package codec
import ( import (
"io" "io"
"runtime"
"sync" "sync"
"testing" "testing"
"time" "time"
) )
func TestMeasureBitRateStatic(t *testing.T) { func TestMeasureBitRateStatic(t *testing.T) {
// https://github.com/pion/mediadevices/issues/198
if runtime.GOOS == "darwin" {
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
}
r, w := io.Pipe() r, w := io.Pipe()
const ( const (
dataSize = 1000 dataSize = 1000
@@ -60,11 +54,6 @@ func TestMeasureBitRateStatic(t *testing.T) {
} }
func TestMeasureBitRateDynamic(t *testing.T) { func TestMeasureBitRateDynamic(t *testing.T) {
// https://github.com/pion/mediadevices/issues/198
if runtime.GOOS == "darwin" {
t.Skip("Skipping because Darwin CI is not reliable for timing related tests.")
}
r, w := io.Pipe() r, w := io.Pipe()
const ( const (
dataSize = 1000 dataSize = 1000

View File

@@ -1,75 +0,0 @@
package mediadevices
import "github.com/pion/mediadevices/pkg/codec"
type EncodedBuffer struct {
Data []byte
Samples uint32
}
type EncodedReadCloser interface {
Read() (EncodedBuffer, func(), error)
Close() error
codec.Controllable
}
type encodedReadCloserImpl struct {
readFn func() (EncodedBuffer, func(), error)
closeFn func() error
controllerFn func() codec.EncoderController
}
func (r *encodedReadCloserImpl) Read() (EncodedBuffer, func(), error) {
return r.readFn()
}
func (r *encodedReadCloserImpl) Close() error {
return r.closeFn()
}
func (r *encodedReadCloserImpl) Controller() codec.EncoderController {
return r.controllerFn()
}
type encodedIOReadCloserImpl struct {
readFn func([]byte) (int, error)
closeFn func() error
controller func() codec.EncoderController
}
func newEncodedIOReadCloserImpl(reader EncodedReadCloser) *encodedIOReadCloserImpl {
var encoded EncodedBuffer
release := func() {}
return &encodedIOReadCloserImpl{
readFn: func(b []byte) (int, error) {
var err error
if len(encoded.Data) == 0 {
release()
encoded, release, err = reader.Read()
if err != nil {
reader.Close()
return 0, err
}
}
n := copy(b, encoded.Data)
encoded.Data = encoded.Data[n:]
return n, nil
},
closeFn: reader.Close,
controller: reader.Controller,
}
}
func (r *encodedIOReadCloserImpl) Read(b []byte) (int, error) {
return r.readFn(b)
}
func (r *encodedIOReadCloserImpl) Close() error {
return r.closeFn()
}
func (r *encodedIOReadCloserImpl) Controller() codec.EncoderController {
return r.controller()
}

10
libs-builder.Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM dockercore/golang-cross
RUN apt-get update -qq \
&& apt-get install -y \
g++-mingw-w64 \
nasm \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
VOLUME /go/src/github.com/pion/mediadevices
WORKDIR /go/src/github.com/pion/mediadevices

View File

@@ -8,7 +8,6 @@ import (
"github.com/pion/mediadevices/pkg/driver" "github.com/pion/mediadevices/pkg/driver"
_ "github.com/pion/mediadevices/pkg/driver/audiotest" _ "github.com/pion/mediadevices/pkg/driver/audiotest"
_ "github.com/pion/mediadevices/pkg/driver/videotest" _ "github.com/pion/mediadevices/pkg/driver/videotest"
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
) )
@@ -98,145 +97,37 @@ func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) {
t.Fatal("expect to get at least 1 property") t.Fatal("expect to get at least 1 property")
} }
expectedProp := driver.Properties()[0] expectedProp := driver.Properties()[0]
// Since this is a continuous value, bestConstraints should be set with the value that user specified
expectedProp.FrameRate = 30.0
// By reducing the value from the driver by a tiny amount, this property should be chosen. wantConstraints := MediaTrackConstraints{
// At the same time, we'll be able to find out if the return constraints will be properly set MediaConstraints: prop.MediaConstraints{
// to the best constraints. VideoConstraints: prop.VideoConstraints{
cases := map[string]struct { // By reducing the width from the driver by a tiny amount, this property should be chosen.
width, height int // At the same time, we'll be able to find out if the return constraints will be properly set
frameFormat frame.Format // to the best constraints.
frameRate float32 Width: prop.Int(expectedProp.Width - 1),
}{ Height: prop.Int(expectedProp.Width),
"DifferentWidth": { FrameFormat: prop.FrameFormat(expectedProp.FrameFormat),
width: expectedProp.Width - 1, FrameRate: prop.Float(30.0),
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,
}, },
} }
for name, c := range cases { bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
c := c
t.Run(name, func(t *testing.T) {
var vc prop.VideoConstraints
if c.frameRate >= 0 {
vc = prop.VideoConstraints{
Width: prop.Int(c.width),
Height: prop.Int(c.height),
FrameFormat: prop.FrameFormat(c.frameFormat),
FrameRate: prop.Float(c.frameRate),
}
} else {
// do not specify the framerate
vc = prop.VideoConstraints{
Width: prop.Int(c.width),
Height: prop.Int(c.height),
FrameFormat: prop.FrameFormat(c.frameFormat),
}
}
wantConstraints := MediaTrackConstraints{
MediaConstraints: prop.MediaConstraints{
VideoConstraints: vc,
},
}
bestDriver, bestConstraints, err := selectBestDriver(filterFn, wantConstraints)
if err != nil {
t.Fatal(err)
}
if driver != bestDriver {
t.Fatal("best driver is not expected")
}
s := bestConstraints.selectedMedia
if s.Width != expectedProp.Width ||
s.Height != expectedProp.Height ||
s.FrameFormat != expectedProp.FrameFormat ||
s.FrameRate != expectedProp.FrameRate {
t.Fatalf("failed to return best constraints\nexpected:\n%v\n\ngot:\n%v", expectedProp, bestConstraints.selectedMedia)
}
})
}
}
func TestSelectBestDriverConstraintsNoFit(t *testing.T) {
filterFn := driver.FilterVideoRecorder()
drivers := driver.GetManager().Query(filterFn)
if len(drivers) == 0 {
t.Fatal("expect to get at least 1 driver")
}
driver := drivers[0]
err := driver.Open()
if err != nil { if err != nil {
t.Fatal("expect to open driver successfully") t.Fatal(err)
}
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,
},
} }
for name, c := range cases { if driver != bestDriver {
c := c t.Fatal("best driver is not expected")
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),
},
},
}
_, _, err := selectBestDriver(filterFn, wantConstraints) s := bestConstraints.selectedMedia
if err == nil { if s.Width != expectedProp.Width ||
t.Fatal("expect to not find a driver that fits the constraints") 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)
} }
} }

View File

@@ -2,8 +2,6 @@ package mediadevices
import ( import (
"sync" "sync"
"github.com/pion/webrtc/v4"
) )
// MediaStream is an interface that represents a collection of existing tracks. // MediaStream is an interface that represents a collection of existing tracks.
@@ -25,7 +23,7 @@ type mediaStream struct {
l sync.RWMutex l sync.RWMutex
} }
const trackTypeDefault webrtc.RTPCodecType = 0 const trackTypeDefault MediaDeviceType = 0
// NewMediaStream creates a MediaStream interface that's defined in // NewMediaStream creates a MediaStream interface that's defined in
// https://w3c.github.io/mediacapture-main/#dom-mediastream // https://w3c.github.io/mediacapture-main/#dom-mediastream
@@ -42,11 +40,11 @@ func NewMediaStream(tracks ...Track) (MediaStream, error) {
} }
func (m *mediaStream) GetAudioTracks() []Track { func (m *mediaStream) GetAudioTracks() []Track {
return m.queryTracks(webrtc.RTPCodecTypeAudio) return m.queryTracks(AudioInput)
} }
func (m *mediaStream) GetVideoTracks() []Track { func (m *mediaStream) GetVideoTracks() []Track {
return m.queryTracks(webrtc.RTPCodecTypeVideo) return m.queryTracks(VideoInput)
} }
func (m *mediaStream) GetTracks() []Track { func (m *mediaStream) GetTracks() []Track {
@@ -55,7 +53,7 @@ func (m *mediaStream) GetTracks() []Track {
// queryTracks returns all tracks that are the same kind as t. // queryTracks returns all tracks that are the same kind as t.
// If t is 0, which is the default, queryTracks will return all the tracks. // If t is 0, which is the default, queryTracks will return all the tracks.
func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []Track { func (m *mediaStream) queryTracks(t MediaDeviceType) []Track {
m.l.RLock() m.l.RLock()
defer m.l.RUnlock() defer m.l.RUnlock()

View File

@@ -1,11 +1,9 @@
package mediadevices package mediadevices
import ( import (
"io"
"testing" "testing"
"github.com/pion/mediadevices/pkg/codec" "github.com/pion/webrtc/v2"
"github.com/pion/webrtc/v4"
) )
type mockMediaStreamTrack struct { type mockMediaStreamTrack struct {
@@ -16,56 +14,29 @@ func (track *mockMediaStreamTrack) ID() string {
return "" return ""
} }
func (track *mockMediaStreamTrack) StreamID() string {
return ""
}
func (track *mockMediaStreamTrack) RID() string {
return ""
}
func (track *mockMediaStreamTrack) Close() error { func (track *mockMediaStreamTrack) Close() error {
return nil return nil
} }
func (track *mockMediaStreamTrack) Kind() webrtc.RTPCodecType { func (track *mockMediaStreamTrack) Kind() MediaDeviceType {
switch track.kind { return track.kind
case AudioInput:
return webrtc.RTPCodecTypeAudio
case VideoInput:
return webrtc.RTPCodecTypeVideo
default:
panic("invalid track kind")
}
} }
func (track *mockMediaStreamTrack) OnEnded(handler func(error)) { func (track *mockMediaStreamTrack) OnEnded(handler func(error)) {
} }
func (track *mockMediaStreamTrack) Bind(ctx webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { func (track *mockMediaStreamTrack) Bind(pc *webrtc.PeerConnection) (*webrtc.Track, error) {
return webrtc.RTPCodecParameters{}, nil return nil, nil
} }
func (track *mockMediaStreamTrack) Unbind(ctx webrtc.TrackLocalContext) error { func (track *mockMediaStreamTrack) Unbind(pc *webrtc.PeerConnection) error {
return nil return nil
} }
func (track *mockMediaStreamTrack) NewRTPReader(codecName string, ssrc uint32, mtu int) (RTPReadCloser, error) { func (track *mockMediaStreamTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser, error) {
return nil, nil return nil, nil
} }
func (track *mockMediaStreamTrack) NewEncodedReader(codecName string) (EncodedReadCloser, error) {
return nil, nil
}
func (track *mockMediaStreamTrack) NewEncodedIOReader(codecName string) (io.ReadCloser, error) {
return nil, nil
}
func (track *mockMediaStreamTrack) EncoderController() codec.EncoderController {
return nil
}
func TestMediaStreamFilters(t *testing.T) { func TestMediaStreamFilters(t *testing.T) {
audioTracks := []Track{ audioTracks := []Track{
&mockMediaStreamTrack{AudioInput}, &mockMediaStreamTrack{AudioInput},

View File

@@ -14,7 +14,7 @@ func detectCurrentVideoProp(broadcaster *video.Broadcaster) (prop.Media, error)
// buffered frame or a new frame from the source. This also implies that no frame will be lost // buffered frame or a new frame from the source. This also implies that no frame will be lost
// in any case. // in any case.
metaReader := broadcaster.NewReader(false) metaReader := broadcaster.NewReader(false)
metaReader = video.DetectChanges(0, 0, func(p prop.Media) { currentProp = p })(metaReader) metaReader = video.DetectChanges(0, func(p prop.Media) { currentProp = p })(metaReader)
_, _, err := metaReader.Read() _, _, err := metaReader.Read()
return currentProp, err return currentProp, err

View File

@@ -27,7 +27,6 @@
#define MAX_DEVICES 8 #define MAX_DEVICES 8
#define MAX_PROPERTIES 64 #define MAX_PROPERTIES 64
#define MAX_DEVICE_UID_CHARS 64 #define MAX_DEVICE_UID_CHARS 64
#define MAX_DEVICE_NAME_CHARS 64
typedef const char* STATUS; typedef const char* STATUS;
static STATUS STATUS_OK = (STATUS) NULL; static STATUS STATUS_OK = (STATUS) NULL;
@@ -46,8 +45,7 @@ typedef enum AVBindMediaType {
typedef enum AVBindFrameFormat { typedef enum AVBindFrameFormat {
AVBindFrameFormatI420, AVBindFrameFormatI420,
AVBindFrameFormatNV21, AVBindFrameFormatNV21,
AVBindFrameFormatNV12, AVBindFrameFormatYUY2,
AVBindFrameFormatYUYV,
AVBindFrameFormatUYVY, AVBindFrameFormatUYVY,
} AVBindFrameFormat; } AVBindFrameFormat;
@@ -66,7 +64,6 @@ typedef struct AVBindSession AVBindSession, *PAVBindSession;
typedef struct { typedef struct {
char uid[MAX_DEVICE_UID_CHARS + 1]; char uid[MAX_DEVICE_UID_CHARS + 1];
char name[MAX_DEVICE_NAME_CHARS + 1];
} AVBindDevice, *PAVBindDevice; } AVBindDevice, *PAVBindDevice;
// AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static // AVBindDevices returns a list of AVBindDevices. The result array is pointing to a static

View File

@@ -1,17 +1,17 @@
// MIT License // MIT License
// //
// Copyright (c) 2019-2020 Pion // Copyright (c) 2019-2020 Pion
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal // of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights // in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is // copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions: // furnished to do so, subject to the following conditions:
// //
// The above copyright notice and this permission notice shall be included in all // The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software. // copies or substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -46,8 +46,6 @@
} \ } \
} while(0) } while(0)
static NSString *const UnrecognizedMacOSVersionException = @"UnrecognizedMacOSVersionException";
@interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate> @interface VideoDataDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
@property (readonly) AVBindDataCallback mCallback; @property (readonly) AVBindDataCallback mCallback;
@@ -76,61 +74,32 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 || if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
!CMSampleBufferIsValid(sampleBuffer) || !CMSampleBufferIsValid(sampleBuffer) ||
!CMSampleBufferDataIsReady(sampleBuffer)) { !CMSampleBufferDataIsReady(sampleBuffer)) {
return; return;
} }
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
if (imageBuffer == NULL) { if (imageBuffer == NULL) {
return; return;
} }
CVBufferRetain(imageBuffer); imageBuffer = CVBufferRetain(imageBuffer);
CVReturn ret = CVReturn ret =
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
if (ret != kCVReturnSuccess) { if (ret != kCVReturnSuccess) {
CVBufferRelease(imageBuffer); return;
return;
} }
// Handle NV12 special case size_t heightY = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
OSType pixelFormat = CVPixelBufferGetPixelFormatType(imageBuffer); size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
if (pixelFormat == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) {
// Get actual dimensions of image (without padding) size_t heightUV = CVPixelBufferGetHeightOfPlane(imageBuffer, 1);
size_t width = CVPixelBufferGetWidth(imageBuffer); size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
size_t height = CVPixelBufferGetHeight(imageBuffer);
size_t totalSize = /*Y plane*/ width * height + /*UV plane*/ width * height / 2; int len = (int)((heightY * bytesPerRowY) + (2 * heightUV * bytesPerRowUV));
void *buf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
size_t bytesPerRowY = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0); _mCallback(_mPUserData, buf, len);
size_t bytesPerRowUV = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
void *mergedBuffer = malloc(totalSize);
if (!mergedBuffer) {
NSLog(@"Failed to allocate memory for merged buffer");
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
CVBufferRelease(imageBuffer);
return;
}
// Truncate data where we know it should end to strip padding
void *yPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
for (size_t row = 0; row < height; ++row) {
memcpy(mergedBuffer + row * width, yPlaneBuf + row * bytesPerRowY, width);
}
void *uvPlaneBuf = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
for (size_t row = 0; row < height / 2; ++row) {
memcpy(mergedBuffer + width * height + row * width, uvPlaneBuf + row * bytesPerRowUV, width);
}
_mCallback(_mPUserData, mergedBuffer, (int)totalSize);
free(mergedBuffer);
} else {
void *buf = CVPixelBufferGetBaseAddress(imageBuffer);
size_t dataSize = CVPixelBufferGetDataSize(imageBuffer);
_mCallback(_mPUserData, buf, (int)dataSize);
}
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
CVBufferRelease(imageBuffer); CVBufferRelease(imageBuffer);
} }
@@ -164,21 +133,13 @@ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) { STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
STATUS retStatus = STATUS_OK; STATUS retStatus = STATUS_OK;
// Useful mapping reference from ffmpeg:
// https://github.com/FFmpeg/FFmpeg/blob/c810a9502cebe32e1dd08ee3d0d17053dde44aa9/libavdevice/avfoundation.m#L53-L80
switch (format) { switch (format) {
case AVBindFrameFormatI420: case AVBindFrameFormatNV21:
*pFourCC = kCVPixelFormatType_420YpCbCr8Planar; *pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;
break;
case AVBindFrameFormatNV12:
*pFourCC = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
break; break;
case AVBindFrameFormatUYVY: case AVBindFrameFormatUYVY:
*pFourCC = kCVPixelFormatType_422YpCbCr8; *pFourCC = kCVPixelFormatType_422YpCbCr8;
break; break;
case AVBindFrameFormatYUYV:
*pFourCC = kCVPixelFormatType_422YpCbCr8_yuvs;
break;
// TODO: Add the rest of frame formats // TODO: Add the rest of frame formats
default: default:
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT; retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
@@ -189,22 +150,15 @@ STATUS frameFormatToFourCC(AVBindFrameFormat format, FourCharCode *pFourCC) {
STATUS frameFormatFromFourCC(FourCharCode fourCC, AVBindFrameFormat *pFormat) { STATUS frameFormatFromFourCC(FourCharCode fourCC, AVBindFrameFormat *pFormat) {
STATUS retStatus = STATUS_OK; STATUS retStatus = STATUS_OK;
switch (fourCC) { switch (fourCC) {
case kCVPixelFormatType_420YpCbCr8Planar: case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
*pFormat = AVBindFrameFormatI420; *pFormat = AVBindFrameFormatNV21;
break; break;
case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange: case kCVPixelFormatType_422YpCbCr8:
case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: *pFormat = AVBindFrameFormatUYVY;
*pFormat = AVBindFrameFormatNV12; break;
break;
case kCVPixelFormatType_422YpCbCr8:
*pFormat = AVBindFrameFormatUYVY;
break;
case kCVPixelFormatType_422YpCbCr8_yuvs:
*pFormat = AVBindFrameFormatYUYV;
break;
// TODO: Add the rest of frame formats // TODO: Add the rest of frame formats
default: default:
retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT; retStatus = STATUS_UNSUPPORTED_FRAME_FORMAT;
} }
return retStatus; return retStatus;
} }
@@ -216,53 +170,34 @@ STATUS AVBindDevices(AVBindMediaType mediaType, PAVBindDevice *ppDevices, int *p
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
CHK(mediaType == AVBindMediaTypeVideo || mediaType == AVBindMediaTypeAudio, STATUS_UNSUPPORTED_MEDIA_TYPE); CHK(mediaType == AVBindMediaTypeVideo || mediaType == AVBindMediaTypeAudio, STATUS_UNSUPPORTED_MEDIA_TYPE);
CHK(ppDevices != NULL && pLen != NULL, STATUS_NULL_ARG); CHK(ppDevices != NULL && pLen != NULL, STATUS_NULL_ARG);
PAVBindDevice pDevice; PAVBindDevice pDevice;
AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio; AVMediaType _mediaType = mediaType == AVBindMediaTypeVideo ? AVMediaTypeVideo : AVMediaTypeAudio;
NSArray *refAllTypes = @[
NSArray *refAllTypes; AVCaptureDeviceTypeBuiltInWideAngleCamera,
#if defined(MAC_OS_VERSION_14_0) AVCaptureDeviceTypeBuiltInMicrophone,
if (@available(macOS 14.0, *)) { AVCaptureDeviceTypeExternalUnknown
refAllTypes = @[ ];
AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeMicrophone,
AVCaptureDeviceTypeExternal,
];
} else {
@throw [NSException exceptionWithName:UnrecognizedMacOSVersionException
reason:@"Unrecognized or unsupported macOS version detected."
userInfo:nil];
}
#else
refAllTypes = @[
AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeBuiltInMicrophone,
AVCaptureDeviceTypeExternalUnknown,
];
#endif
AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession
discoverySessionWithDeviceTypes: refAllTypes discoverySessionWithDeviceTypes: refAllTypes
mediaType: _mediaType mediaType: _mediaType
position: AVCaptureDevicePositionUnspecified]; position: AVCaptureDevicePositionUnspecified];
int i = 0; int i = 0;
for (AVCaptureDevice *refDevice in refSession.devices) { for (AVCaptureDevice *refDevice in refSession.devices) {
if (i >= MAX_DEVICES) { if (i >= MAX_DEVICES) {
break; break;
} }
pDevice = devices + i; pDevice = devices + i;
strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS); strncpy(pDevice->uid, refDevice.uniqueID.UTF8String, MAX_DEVICE_UID_CHARS);
pDevice->uid[MAX_DEVICE_UID_CHARS] = '\0'; pDevice->uid[MAX_DEVICE_UID_CHARS] = '\0';
strncpy(pDevice->name, refDevice.localizedName.UTF8String, MAX_DEVICE_NAME_CHARS);
pDevice->name[MAX_DEVICE_NAME_CHARS] = '\0';
i++; i++;
} }
*ppDevices = devices; *ppDevices = devices;
*pLen = i; *pLen = i;
cleanup: cleanup:
[refPool drain]; [refPool drain];
return retStatus; return retStatus;
@@ -282,7 +217,7 @@ STATUS AVBindSessionInit(AVBindDevice device, PAVBindSession *ppSessionResult) {
pSession->device = device; pSession->device = device;
pSession->refCaptureSession = NULL; pSession->refCaptureSession = NULL;
*ppSessionResult = pSession; *ppSessionResult = pSession;
cleanup: cleanup:
return retStatus; return retStatus;
} }
@@ -309,15 +244,15 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
STATUS retStatus = STATUS_OK; STATUS retStatus = STATUS_OK;
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
CHK(pSession != NULL && dataCallback != NULL, STATUS_NULL_ARG); CHK(pSession != NULL && dataCallback != NULL, STATUS_NULL_ARG);
AVCaptureDeviceInput *refInput; AVCaptureDeviceInput *refInput;
NSError *refErr = NULL; NSError *refErr = NULL;
NSString *refUID = [NSString stringWithUTF8String: pSession->device.uid]; NSString *refUID = [NSString stringWithUTF8String: pSession->device.uid];
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refUID]; AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refUID];
refInput = [[AVCaptureDeviceInput alloc] initWithDevice: refDevice error: &refErr]; refInput = [[AVCaptureDeviceInput alloc] initWithDevice: refDevice error: &refErr];
CHK(refErr == NULL, STATUS_DEVICE_INIT_FAILED); CHK(refErr == NULL, STATUS_DEVICE_INIT_FAILED);
AVCaptureSession *refCaptureSession = [[AVCaptureSession alloc] init]; AVCaptureSession *refCaptureSession = [[AVCaptureSession alloc] init];
refCaptureSession.sessionPreset = AVCaptureSessionPresetMedium; refCaptureSession.sessionPreset = AVCaptureSessionPresetMedium;
[refCaptureSession addInput: refInput]; [refCaptureSession addInput: refInput];
@@ -326,7 +261,7 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
VideoDataDelegate *pDelegate = [[VideoDataDelegate alloc] VideoDataDelegate *pDelegate = [[VideoDataDelegate alloc]
init: dataCallback init: dataCallback
withUserData: pUserData]; withUserData: pUserData];
AVCaptureVideoDataOutput *pOutput = [[AVCaptureVideoDataOutput alloc] init]; AVCaptureVideoDataOutput *pOutput = [[AVCaptureVideoDataOutput alloc] init];
FourCharCode fourCC; FourCharCode fourCC;
CHK_STATUS(frameFormatToFourCC(property.frameFormat, &fourCC)); CHK_STATUS(frameFormatToFourCC(property.frameFormat, &fourCC));
@@ -344,10 +279,10 @@ STATUS AVBindSessionOpen(PAVBindSession pSession,
} else { } else {
// TODO: implement audio pipeline // TODO: implement audio pipeline
} }
pSession->refCaptureSession = [refCaptureSession retain]; pSession->refCaptureSession = [refCaptureSession retain];
[refCaptureSession startRunning]; [refCaptureSession startRunning];
cleanup: cleanup:
[refPool drain]; [refPool drain];
return retStatus; return retStatus;
@@ -358,30 +293,20 @@ STATUS AVBindSessionClose(PAVBindSession pSession) {
STATUS retStatus = STATUS_OK; STATUS retStatus = STATUS_OK;
CHK(pSession != NULL, STATUS_NULL_ARG); CHK(pSession != NULL, STATUS_NULL_ARG);
CHK(pSession->refCaptureSession != NULL, STATUS_OK); CHK(pSession->refCaptureSession != NULL, STATUS_OK);
[pSession->refCaptureSession stopRunning]; [pSession->refCaptureSession stopRunning];
[pSession->refCaptureSession release]; [pSession->refCaptureSession release];
pSession->refCaptureSession = NULL; pSession->refCaptureSession = NULL;
cleanup: cleanup:
return retStatus; 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 AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *ppProperties, int *pLen) {
STATUS retStatus = STATUS_OK; STATUS retStatus = STATUS_OK;
NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *refPool = [[NSAutoreleasePool alloc] init];
CHK(pSession != NULL && ppProperties != NULL && pLen != NULL, STATUS_NULL_ARG); CHK(pSession != NULL && ppProperties != NULL && pLen != NULL, STATUS_NULL_ARG);
NSString *refDeviceUID = [NSString stringWithUTF8String: pSession->device.uid]; NSString *refDeviceUID = [NSString stringWithUTF8String: pSession->device.uid];
AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refDeviceUID]; AVCaptureDevice *refDevice = [AVCaptureDevice deviceWithUniqueID: refDeviceUID];
FourCharCode fourCC; FourCharCode fourCC;
@@ -394,17 +319,15 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
for (AVCaptureDeviceFormat *refFormat in refDevice.formats) { for (AVCaptureDeviceFormat *refFormat in refDevice.formats) {
// TODO: Probably gives a warn to the user // TODO: Probably gives a warn to the user
if (len >= MAX_PROPERTIES) { if (len >= MAX_PROPERTIES) {
NSLog(@"[WARNING] skipping the rest of properties due to MAX_PROPERTIES");
break; break;
} }
if ([refFormat.mediaType isEqual:AVMediaTypeVideo]) { if ([refFormat.mediaType isEqual:AVMediaTypeVideo]) {
fourCC = CMFormatDescriptionGetMediaSubType(refFormat.formatDescription); fourCC = CMFormatDescriptionGetMediaSubType(refFormat.formatDescription);
if (frameFormatFromFourCC(fourCC, &pProperty->frameFormat) != STATUS_OK) { if (frameFormatFromFourCC(fourCC, &pProperty->frameFormat) != STATUS_OK) {
NSLog(@"[WARNING] skipping %@ %dx%d since it's not supported", FourCCString(fourCC), videoDimensions.width, videoDimensions.height);
continue; continue;
} }
videoFormat = (CMVideoFormatDescriptionRef) refFormat.formatDescription; videoFormat = (CMVideoFormatDescriptionRef) refFormat.formatDescription;
videoDimensions = CMVideoFormatDescriptionGetDimensions(videoFormat); videoDimensions = CMVideoFormatDescriptionGetDimensions(videoFormat);
pProperty->height = videoDimensions.height; pProperty->height = videoDimensions.height;
@@ -412,16 +335,16 @@ STATUS AVBindSessionProperties(PAVBindSession pSession, PAVBindMediaProperty *pp
} else { } else {
// TODO: Get audio properties // TODO: Get audio properties
} }
pProperty++; pProperty++;
len++; len++;
} }
*ppProperties = pSession->properties; *ppProperties = pSession->properties;
*pLen = len; *pLen = len;
cleanup: cleanup:
[refPool drain]; [refPool drain];
return retStatus; return retStatus;
} }

View File

@@ -1,5 +1,6 @@
package avfoundation package avfoundation
// extern void onData(void*, void*, int);
import "C" import "C"
import ( import (
"sync" "sync"
@@ -17,10 +18,11 @@ type handleID int
//export onData //export onData
func onData(userData unsafe.Pointer, buf unsafe.Pointer, length C.int) { func onData(userData unsafe.Pointer, buf unsafe.Pointer, length C.int) {
data := C.GoBytes(buf, length)
handleNum := (*C.int)(userData) handleNum := (*C.int)(userData)
cb, ok := lookup(handleID(*handleNum)) cb, ok := lookup(handleID(*handleNum))
if ok { if ok {
data := C.GoBytes(buf, length)
cb(data) cb(data)
} }
} }

View File

@@ -11,10 +11,8 @@ package avfoundation
// } // }
import "C" import "C"
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"sync"
"unsafe" "unsafe"
"github.com/pion/mediadevices/pkg/frame" "github.com/pion/mediadevices/pkg/frame"
@@ -34,7 +32,6 @@ type Device struct {
// UID is a unique identifier for a device // UID is a unique identifier for a device
UID string UID string
cDevice C.AVBindDevice cDevice C.AVBindDevice
Name string
} }
func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) { func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
@@ -43,10 +40,8 @@ func frameFormatToAVBind(f frame.Format) (C.AVBindFrameFormat, bool) {
return C.AVBindFrameFormatI420, true return C.AVBindFrameFormatI420, true
case frame.FormatNV21: case frame.FormatNV21:
return C.AVBindFrameFormatNV21, true return C.AVBindFrameFormatNV21, true
case frame.FormatNV12: case frame.FormatYUY2:
return C.AVBindFrameFormatNV12, true return C.AVBindFrameFormatYUY2, true
case frame.FormatYUYV:
return C.AVBindFrameFormatYUYV, true
case frame.FormatUYVY: case frame.FormatUYVY:
return C.AVBindFrameFormatUYVY, true return C.AVBindFrameFormatUYVY, true
default: default:
@@ -60,10 +55,8 @@ func frameFormatFromAVBind(f C.AVBindFrameFormat) (frame.Format, bool) {
return frame.FormatI420, true return frame.FormatI420, true
case C.AVBindFrameFormatNV21: case C.AVBindFrameFormatNV21:
return frame.FormatNV21, true return frame.FormatNV21, true
case C.AVBindFrameFormatNV12: case C.AVBindFrameFormatYUY2:
return frame.FormatNV12, true return frame.FormatYUY2, true
case C.AVBindFrameFormatYUYV:
return frame.FormatYUYV, true
case C.AVBindFrameFormatUYVY: case C.AVBindFrameFormatUYVY:
return frame.FormatUYVY, true return frame.FormatUYVY, true
default: default:
@@ -88,7 +81,6 @@ func Devices(mediaType MediaType) ([]Device, error) {
for i := range devices { for i := range devices {
devices[i].UID = C.GoString(&cDevices[i].uid[0]) devices[i].UID = C.GoString(&cDevices[i].uid[0])
devices[i].cDevice = cDevices[i] devices[i].cDevice = cDevices[i]
devices[i].Name = C.GoString(&cDevices[i].name[0])
} }
return devices, nil return devices, nil
@@ -97,13 +89,9 @@ func Devices(mediaType MediaType) ([]Device, error) {
// ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the // ReadCloser is a wrapper around the data callback from AVFoundation. The data received from the
// the underlying callback can be retrieved by calling Read. // the underlying callback can be retrieved by calling Read.
type ReadCloser struct { type ReadCloser struct {
dataChan chan []byte dataChan chan []byte
id handleID id handleID
onClose func() onClose func()
cancelCtx context.Context
cancelFunc func()
closeWG sync.WaitGroup
lock sync.Mutex
} }
func newReadCloser(onClose func()) *ReadCloser { func newReadCloser(onClose func()) *ReadCloser {
@@ -111,25 +99,12 @@ func newReadCloser(onClose func()) *ReadCloser {
rc.dataChan = make(chan []byte, 1) rc.dataChan = make(chan []byte, 1)
rc.onClose = onClose rc.onClose = onClose
rc.id = register(rc.dataCb) rc.id = register(rc.dataCb)
cancelCtx, cancelFunc := context.WithCancel(context.Background())
rc.cancelCtx = cancelCtx
rc.cancelFunc = cancelFunc
return &rc return &rc
} }
func (rc *ReadCloser) dataCb(data []byte) { func (rc *ReadCloser) dataCb(data []byte) {
rc.closeWG.Add(1)
defer rc.closeWG.Done()
// TODO: add a policy for slow reader // TODO: add a policy for slow reader
if rc.cancelCtx.Err() != nil { rc.dataChan <- data
return
}
select {
// Use the Done channel to avoid waiting for new data from closed camera
case <-rc.cancelCtx.Done():
case rc.dataChan <- data:
}
} }
// Read reads raw data, the format is determined by the media type and property: // Read reads raw data, the format is determined by the media type and property:
@@ -145,28 +120,17 @@ func (rc *ReadCloser) Read() ([]byte, func(), error) {
// Close closes the capturing session, and no data will flow anymore // Close closes the capturing session, and no data will flow anymore
func (rc *ReadCloser) Close() { func (rc *ReadCloser) Close() {
rc.lock.Lock()
defer rc.lock.Unlock()
if rc.cancelCtx.Err() != nil {
return // already closed
}
if rc.onClose != nil { if rc.onClose != nil {
rc.onClose() rc.onClose()
} }
rc.cancelFunc()
unregister(rc.id)
rc.closeWG.Wait()
close(rc.dataChan) close(rc.dataChan)
unregister(rc.id)
} }
// Session represents a capturing session. // Session represents a capturing session.
type Session struct { type Session struct {
device Device device Device
cSession C.PAVBindSession cSession C.PAVBindSession
lock sync.Mutex
closed bool
} }
// NewSession creates a new capturing session // NewSession creates a new capturing session
@@ -184,13 +148,6 @@ func NewSession(device Device) (*Session, error) {
// Close stops capturing session and frees up resources // Close stops capturing session and frees up resources
func (session *Session) Close() error { func (session *Session) Close() error {
session.lock.Lock()
defer session.lock.Unlock()
if session.closed {
return nil
}
session.closed = true
if session.cSession == nil { if session.cSession == nil {
return nil return nil
} }

View File

@@ -1,125 +1,35 @@
package codec package codec
import ( import (
"time"
"github.com/pion/mediadevices/pkg/io/audio" "github.com/pion/mediadevices/pkg/io/audio"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop" "github.com/pion/mediadevices/pkg/prop"
"github.com/pion/rtp" "github.com/pion/webrtc/v2"
"github.com/pion/rtp/codecs"
"github.com/pion/webrtc/v4"
) )
// RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future. // RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future.
type RTPCodec struct { type RTPCodec struct {
webrtc.RTPCodecParameters *webrtc.RTPCodec
rtp.Payloader
// Latency of static frame size codec.
Latency time.Duration
} }
// NewRTPH264Codec is a helper to create an H264 codec // NewRTPH264Codec is a helper to create an H264 codec
func NewRTPH264Codec(clockrate uint32) *RTPCodec { func NewRTPH264Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{ return &RTPCodec{webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, clockrate)}
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: nil,
},
PayloadType: 125,
},
Payloader: &codecs.H264Payloader{},
}
}
// NewRTPH265Codec is a helper to create an H265 codec
func NewRTPH265Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: nil,
},
PayloadType: 116,
},
Payloader: &codecs.H265Payloader{},
}
} }
// NewRTPVP8Codec is a helper to create an VP8 codec // NewRTPVP8Codec is a helper to create an VP8 codec
func NewRTPVP8Codec(clockrate uint32) *RTPCodec { func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{ return &RTPCodec{webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, clockrate)}
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: nil,
},
PayloadType: 96,
},
Payloader: &codecs.VP8Payloader{},
}
} }
// NewRTPVP9Codec is a helper to create an VP9 codec // NewRTPVP9Codec is a helper to create an VP9 codec
func NewRTPVP9Codec(clockrate uint32) *RTPCodec { func NewRTPVP9Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{ return &RTPCodec{webrtc.NewRTPVP9Codec(webrtc.DefaultPayloadTypeVP9, clockrate)}
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: nil,
},
PayloadType: 98,
},
Payloader: &codecs.VP9Payloader{},
}
}
// NewRTPAV1Codec is a helper to create an AV1 codec
func NewRTPAV1Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-idx=5;profile=0;tier=0",
RTCPFeedback: nil,
},
PayloadType: 99,
},
Payloader: &codecs.AV1Payloader{},
}
} }
// NewRTPOpusCodec is a helper to create an Opus codec // NewRTPOpusCodec is a helper to create an Opus codec
func NewRTPOpusCodec(clockrate uint32) *RTPCodec { func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
return &RTPCodec{ return &RTPCodec{webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, clockrate)}
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
RTCPFeedback: nil,
},
PayloadType: 111,
},
Payloader: &codecs.OpusPayloader{},
}
} }
// AudioEncoderBuilder is the interface that wraps basic operations that are // AudioEncoderBuilder is the interface that wraps basic operations that are
@@ -146,37 +56,15 @@ type VideoEncoderBuilder interface {
BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error) BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error)
} }
// ReadCloser is an io.ReadCloser with a controller // ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame
type ReadCloser interface { type ReadCloser interface {
Read() (b []byte, release func(), err error) Read() (b []byte, release func(), err error)
Close() 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 interface{}
// 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 // SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted
// but this also means that the quality will also be lower. // but this also means that the quality will also be lower.
SetBitRate(int) error SetBitRate(int) error
// ForceKeyFrame forces the next frame to be a keyframe, aka intra-frame.
ForceKeyFrame() error
} }
// BaseParams represents an codec's encoding properties // BaseParams represents an codec's encoding properties

View File

@@ -1 +0,0 @@
tmp

View File

@@ -1,41 +0,0 @@
version=n7.0
srcPath=tmp/$(version)/src
installPath=tmp/$(version)
CGO_CFLAGS := -I$(CURDIR)/$(installPath)/include/
CGO_LDFLAGS := -L$(CURDIR)/$(installPath)/lib/
PKG_CONFIG_PATH := $(CURDIR)/$(installPath)/lib/pkgconfig
configure := --enable-libx264 --enable-gpl
# Main test target - depends on FFmpeg being built
test: $(installPath)/lib/libavcodec.a
PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" go test -v .
# Separate target for building FFmpeg (used by CI when cache miss)
build-ffmpeg: $(installPath)/lib/libavcodec.a
@echo "FFmpeg build completed"
# Clean incomplete builds before starting
clean-incomplete:
@if [ -d "$(srcPath)" ] && [ ! -f "$(installPath)/lib/libavcodec.a" ]; then \
echo "Cleaning incomplete build..."; \
rm -rf $(srcPath); \
fi
# FFmpeg build rule
$(installPath)/lib/libavcodec.a: clean-incomplete $(srcPath)/Makefile
cd $(srcPath) && make -j4
cd $(srcPath) && make install
@echo "Installation completed, checking results..."
@ls -la $(installPath)/lib/ || echo "lib directory not found"
@ls -la $(installPath)/include/ || echo "include directory not found"
$(srcPath)/Makefile: $(srcPath)/.git
cd $(srcPath) && ./configure --prefix=$(CURDIR)/$(installPath) $(configure)
$(srcPath)/.git:
rm -rf $(srcPath)
mkdir -p $(srcPath)
cd $(srcPath) && git clone https://github.com/FFmpeg/FFmpeg .
cd $(srcPath) && git checkout $(version)
.PHONY: test build-ffmpeg clean-incomplete

View File

@@ -1,18 +0,0 @@
package ffmpeg
import (
"errors"
)
var (
errFailedToCreateHwDevice = errors.New("ffmpeg: failed to create device")
errCodecNotFound = errors.New("ffmpeg: codec not found")
errFailedToCreateCodecCtx = errors.New("ffmpeg: failed to allocate codec context")
errFailedToCreateHwFramesCtx = errors.New("ffmpeg: failed to create hardware frames context")
errFailedToInitHwFramesCtx = errors.New("ffmpeg: failed to initialize hardware frames context")
errFailedToOpenCodecCtx = errors.New("ffmpeg: failed to open codec context")
errFailedToAllocFrame = errors.New("ffmpeg: failed to allocate frame")
errFailedToAllocSwBuf = errors.New("ffmpeg: failed to allocate software buffer")
errFailedToAllocHwBuf = errors.New("ffmpeg: failed to allocate hardware buffer")
errFailedToAllocPacket = errors.New("ffmpeg: failed to allocate packet")
)

View File

@@ -1,439 +0,0 @@
// Package ffmpeg brings libavcodec's encoding capabilities to mediadevices.
// This package requires ffmpeg headers and libraries to be built.
// For more information, see https://github.com/asticode/go-astiav?tab=readme-ov-file#install-ffmpeg-from-source.
//
// Currently, only nvenc, x264, vaapi are implemented, but extending this to other ffmpeg supported codecs should
// be simple.
package ffmpeg
import (
"errors"
"io"
"sync"
"github.com/asticode/go-astiav"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
type baseEncoder struct {
codecCtx *astiav.CodecContext
frame *astiav.Frame
packet *astiav.Packet
width int
height int
r video.Reader
nextIsKeyFrame bool
mu sync.Mutex
closed bool
}
type hardwareEncoder struct {
baseEncoder
hwFramesCtx *astiav.HardwareFramesContext
hwFrame *astiav.Frame
}
type softwareEncoder struct {
baseEncoder
}
func newHardwareEncoder(r video.Reader, p prop.Media, params Params) (*hardwareEncoder, error) {
if p.FrameRate == 0 {
p.FrameRate = params.FrameRate
}
astiav.SetLogLevel(astiav.LogLevel(astiav.LogLevelWarning))
var hardwareDeviceType astiav.HardwareDeviceType
switch params.codecName {
case "h264_nvenc", "hevc_nvenc", "av1_nvenc":
hardwareDeviceType = astiav.HardwareDeviceType(astiav.HardwareDeviceTypeCUDA)
case "vp8_vaapi", "vp9_vaapi", "h264_vaapi", "hevc_vaapi":
hardwareDeviceType = astiav.HardwareDeviceType(astiav.HardwareDeviceTypeVAAPI)
}
hwDevice, err := astiav.CreateHardwareDeviceContext(
hardwareDeviceType,
params.hardwareDevice,
nil,
0,
)
if err != nil {
return nil, errFailedToCreateHwDevice
}
codec := astiav.FindEncoderByName(params.codecName)
if codec == nil {
return nil, errCodecNotFound
}
codecCtx := astiav.AllocCodecContext(codec)
if codecCtx == nil {
return nil, errFailedToCreateCodecCtx
}
// Configure codec context
codecCtx.SetWidth(p.Width)
codecCtx.SetHeight(p.Height)
codecCtx.SetTimeBase(astiav.NewRational(1, int(p.FrameRate)))
codecCtx.SetFramerate(codecCtx.TimeBase().Invert())
codecCtx.SetBitRate(int64(params.BitRate))
codecCtx.SetRateControlMaxRate(int64(params.BitRate + params.BitRate/10))
codecCtx.SetRateControlMinRate(int64(params.BitRate - params.BitRate/10))
codecCtx.SetGopSize(params.KeyFrameInterval)
codecCtx.SetMaxBFrames(0)
switch params.codecName {
case "h264_nvenc", "hevc_nvenc", "av1_nvenc":
codecCtx.SetPixelFormat(astiav.PixelFormat(astiav.PixelFormatCuda))
case "vp8_vaapi", "vp9_vaapi", "h264_vaapi", "hevc_vaapi":
codecCtx.SetPixelFormat(astiav.PixelFormat(astiav.PixelFormatVaapi))
}
codecOptions := codecCtx.PrivateData().Options()
switch params.codecName {
case "av1_nvenc":
codecCtx.SetProfile(astiav.Profile(astiav.ProfileAv1Main))
codecOptions.Set("tier", "0", 0)
case "h264_vaapi":
codecCtx.SetProfile(astiav.Profile(astiav.ProfileH264Main))
codecOptions.Set("profile", "main", 0)
codecOptions.Set("level", "1", 0)
case "hevc_vaapi":
codecCtx.SetProfile(astiav.Profile(astiav.ProfileHevcMain))
codecOptions.Set("profile", "main", 0)
codecOptions.Set("tier", "main", 0)
codecOptions.Set("level", "1", 0)
}
switch params.codecName {
case "h264_nvenc", "hevc_nvenc", "av1_nvenc":
codecOptions.Set("forced-idr", "1", 0)
codecOptions.Set("zerolatency", "1", 0)
codecOptions.Set("intra-refresh", "1", 0)
codecOptions.Set("delay", "0", 0)
codecOptions.Set("tune", "ll", 0)
codecOptions.Set("preset", "p1", 0)
codecOptions.Set("rc", "vbr", 0)
case "vp8_vaapi", "vp9_vaapi", "h264_vaapi", "hevc_vaapi":
codecOptions.Set("rc_mode", "CBR", 0)
}
// Create hardware frames context
hwFramesCtx := astiav.AllocHardwareFramesContext(hwDevice)
hwDevice.Free()
if hwFramesCtx == nil {
codecCtx.Free()
return nil, errFailedToCreateHwFramesCtx
}
// Set hardware frames context parameters
hwFramesCtx.SetWidth(p.Width)
hwFramesCtx.SetHeight(p.Height)
switch params.codecName {
case "h264_nvenc", "hevc_nvenc", "av1_nvenc":
hwFramesCtx.SetHardwarePixelFormat(astiav.PixelFormat(astiav.PixelFormatCuda))
case "vp8_vaapi", "vp9_vaapi", "h264_vaapi", "hevc_vaapi":
hwFramesCtx.SetHardwarePixelFormat(astiav.PixelFormat(astiav.PixelFormatVaapi))
}
hwFramesCtx.SetSoftwarePixelFormat(params.pixelFormat)
if err = hwFramesCtx.Initialize(); err != nil {
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToInitHwFramesCtx
}
codecCtx.SetHardwareFramesContext(hwFramesCtx)
// Open codec context
if err = codecCtx.Open(codec, nil); err != nil {
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToOpenCodecCtx
}
softwareFrame := astiav.AllocFrame()
if softwareFrame == nil {
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToAllocFrame
}
softwareFrame.SetWidth(p.Width)
softwareFrame.SetHeight(p.Height)
softwareFrame.SetPixelFormat(params.pixelFormat)
if err = softwareFrame.AllocBuffer(0); err != nil {
softwareFrame.Free()
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToAllocSwBuf
}
hardwareFrame := astiav.AllocFrame()
if err = hardwareFrame.AllocHardwareBuffer(hwFramesCtx); err != nil {
softwareFrame.Free()
hardwareFrame.Free()
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToAllocHwBuf
}
packet := astiav.AllocPacket()
if packet == nil {
softwareFrame.Free()
hardwareFrame.Free()
codecCtx.Free()
hwFramesCtx.Free()
return nil, errFailedToAllocPacket
}
return &hardwareEncoder{
baseEncoder: baseEncoder{
codecCtx: codecCtx,
frame: softwareFrame,
packet: packet,
width: p.Width,
height: p.Height,
r: r,
nextIsKeyFrame: false,
},
hwFramesCtx: hwFramesCtx,
hwFrame: hardwareFrame,
}, nil
}
func (e *hardwareEncoder) Controller() codec.EncoderController {
return e
}
func (e *hardwareEncoder) Read() ([]byte, func(), error) {
e.mu.Lock()
defer e.mu.Unlock()
if e.closed {
return nil, func() {}, io.EOF
}
img, release, err := e.r.Read()
if err != nil {
return nil, func() {}, err
}
defer release()
if e.nextIsKeyFrame {
e.frame.SetPictureType(astiav.PictureType(astiav.PictureTypeI))
e.hwFrame.SetPictureType(astiav.PictureType(astiav.PictureTypeI))
e.nextIsKeyFrame = false
} else {
e.frame.SetPictureType(astiav.PictureType(astiav.PictureTypeNone))
e.hwFrame.SetPictureType(astiav.PictureType(astiav.PictureTypeNone))
}
if err = e.frame.Data().FromImage(img); err != nil {
return nil, func() {}, err
}
if err = e.frame.TransferHardwareData(e.hwFrame); err != nil {
return nil, func() {}, err
}
if err = e.codecCtx.SendFrame(e.hwFrame); err != nil {
return nil, func() {}, err
}
if err = e.codecCtx.ReceivePacket(e.packet); err != nil {
return nil, func() {}, err
}
data := make([]byte, e.packet.Size())
copy(data, e.packet.Data())
e.packet.Unref()
return data, func() {}, nil
}
// ForceKeyFrame forces the next frame to be encoded as a keyframe
func (e *hardwareEncoder) ForceKeyFrame() error {
e.mu.Lock()
defer e.mu.Unlock()
e.nextIsKeyFrame = true
return nil
}
func (e *hardwareEncoder) SetBitRate(bitrate int) error {
e.mu.Lock()
defer e.mu.Unlock()
e.codecCtx.SetBitRate(int64(bitrate))
e.codecCtx.SetRateControlMaxRate(int64(bitrate + bitrate/10))
e.codecCtx.SetRateControlMinRate(int64(bitrate - bitrate/10))
return nil
}
func (e *hardwareEncoder) Close() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.packet != nil {
e.packet.Free()
}
if e.frame != nil {
e.frame.Free()
}
if e.hwFrame != nil {
e.hwFrame.Free()
}
if e.codecCtx != nil {
e.codecCtx.Free()
}
if e.hwFramesCtx != nil {
e.hwFramesCtx.Free()
}
e.closed = true
return nil
}
func newSoftwareEncoder(r video.Reader, p prop.Media, params Params) (*softwareEncoder, error) {
if p.FrameRate == 0 {
p.FrameRate = params.FrameRate
}
astiav.SetLogLevel(astiav.LogLevel(astiav.LogLevelWarning))
codec := astiav.FindEncoderByName(params.codecName)
if codec == nil {
return nil, errCodecNotFound
}
codecCtx := astiav.AllocCodecContext(codec)
if codecCtx == nil {
return nil, errFailedToCreateCodecCtx
}
// Configure codec context
codecCtx.SetWidth(p.Width)
codecCtx.SetHeight(p.Height)
codecCtx.SetTimeBase(astiav.NewRational(1, int(p.FrameRate)))
codecCtx.SetFramerate(codecCtx.TimeBase().Invert())
codecCtx.SetPixelFormat(astiav.PixelFormat(astiav.PixelFormatYuv420P))
codecCtx.SetBitRate(int64(params.BitRate))
codecCtx.SetGopSize(params.KeyFrameInterval)
codecCtx.SetMaxBFrames(0)
codecOptions := codecCtx.PrivateData().Options()
codecOptions.Set("preset", "ultrafast", 0)
codecOptions.Set("tune", "zerolatency", 0)
codecCtx.SetFlags(astiav.CodecContextFlags(astiav.CodecContextFlagLowDelay))
// Open codec context
if err := codecCtx.Open(codec, nil); err != nil {
codecCtx.Free()
return nil, errFailedToOpenCodecCtx
}
softwareFrame := astiav.AllocFrame()
if softwareFrame == nil {
codecCtx.Free()
return nil, errFailedToAllocFrame
}
softwareFrame.SetWidth(p.Width)
softwareFrame.SetHeight(p.Height)
softwareFrame.SetPixelFormat(astiav.PixelFormat(astiav.PixelFormatYuv420P))
if err := softwareFrame.AllocBuffer(0); err != nil {
softwareFrame.Free()
codecCtx.Free()
return nil, errFailedToAllocSwBuf
}
packet := astiav.AllocPacket()
if packet == nil {
softwareFrame.Free()
codecCtx.Free()
return nil, errFailedToAllocPacket
}
return &softwareEncoder{
baseEncoder: baseEncoder{
codecCtx: codecCtx,
frame: softwareFrame,
packet: packet,
width: p.Width,
height: p.Height,
r: video.ToI420(r),
nextIsKeyFrame: false,
},
}, nil
}
func (e *softwareEncoder) Read() ([]byte, func(), error) {
e.mu.Lock()
defer e.mu.Unlock()
if e.closed {
return nil, func() {}, io.EOF
}
img, release, err := e.r.Read()
if err != nil {
return nil, func() {}, err
}
defer release()
if e.nextIsKeyFrame {
e.frame.SetPictureType(astiav.PictureType(astiav.PictureTypeI))
e.nextIsKeyFrame = false
} else {
e.frame.SetPictureType(astiav.PictureType(astiav.PictureTypeNone))
}
if err = e.frame.Data().FromImage(img); err != nil {
return nil, func() {}, err
}
if err = e.codecCtx.SendFrame(e.frame); err != nil {
return nil, func() {}, err
}
for {
if err = e.codecCtx.ReceivePacket(e.packet); err != nil {
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
continue
}
return nil, func() {}, err
}
break
}
data := make([]byte, e.packet.Size())
copy(data, e.packet.Data())
e.packet.Unref()
return data, func() {}, nil
}
func (e *softwareEncoder) Controller() codec.EncoderController {
return e
}
func (e *softwareEncoder) ForceKeyFrame() error {
e.mu.Lock()
defer e.mu.Unlock()
e.nextIsKeyFrame = true
return nil
}
func (e *softwareEncoder) SetBitRate(bitrate int) error {
e.mu.Lock()
defer e.mu.Unlock()
e.codecCtx.SetBitRate(int64(bitrate))
return nil
}
func (e *softwareEncoder) Close() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.packet != nil {
e.packet.Free()
}
if e.frame != nil {
e.frame.Free()
}
if e.codecCtx != nil {
e.codecCtx.Free()
}
e.closed = true
return nil
}

View File

@@ -1,357 +0,0 @@
package ffmpeg
import (
"context"
"image"
"io"
"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"
)
func TestEncoder(t *testing.T) {
for name, factory := range map[string]func() (codec.VideoEncoderBuilder, error){
"x264": func() (codec.VideoEncoderBuilder, error) {
p, err := NewH264X264Params()
p.FrameRate = 30
p.BitRate = 1000000
p.KeyFrameInterval = 60
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) {
t.Skip("Changing image size on the fly is currently not supported")
for name, factory := range map[string]func() (codec.VideoEncoderBuilder, error){
"x264": func() (codec.VideoEncoderBuilder, error) {
p, err := NewH264X264Params()
p.FrameRate = 30
p.BitRate = 1000000
p.KeyFrameInterval = 60
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){
"x264": func() (codec.VideoEncoderBuilder, error) {
p, err := NewH264X264Params()
p.FrameRate = 30
p.BitRate = 1000000
p.KeyFrameInterval = 60
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)
}
// TODO: check if this is a key frame
// 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){
"x264": func() (codec.VideoEncoderBuilder, error) {
p, err := NewH264X264Params()
p.FrameRate = 30
p.BitRate = 1000000
p.KeyFrameInterval = 60
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 := &softwareEncoder{}
if _, ok := e.Controller().(codec.BitRateController); !ok {
t.Error()
}
}
func TestShouldImplementKeyFrameControl(t *testing.T) {
e := &softwareEncoder{}
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 := NewH264X264Params()
params.FrameRate = 30
params.BitRate = 1000000
params.KeyFrameInterval = 60
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()
}
}
}

View File

@@ -1,36 +0,0 @@
module github.com/pion/mediadevices/pkg/codec/ffmpeg
go 1.21
replace github.com/pion/mediadevices => ../../../
require (
github.com/asticode/go-astiav v0.35.1
github.com/pion/mediadevices v0.7.1
)
require (
github.com/asticode/go-astikit v0.42.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.15 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.11 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.1.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)

View File

@@ -1,56 +0,0 @@
github.com/asticode/go-astiav v0.35.1 h1:jq27Ihf+GXtOTnhzNTcpKrW1iLNRAuPSoarh7/SapYc=
github.com/asticode/go-astiav v0.35.1/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s=
github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w=
github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.15 h1:MuhuGn1cxpVCPLNY1lI7F1tQ8Spntpgf12ob+pOYT8s=
github.com/pion/rtp v1.8.15/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.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.1.0 h1:yq/p0G5nKGbHISf0YKNA8Yk+kmijbblBvuSLwaJ4QYg=
github.com/pion/webrtc/v4 v4.1.0/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,191 +0,0 @@
package ffmpeg
import (
"github.com/asticode/go-astiav"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/video"
"github.com/pion/mediadevices/pkg/prop"
)
type Params struct {
codec.BaseParams
codecName string
hardwareDevice string
pixelFormat astiav.PixelFormat
FrameRate float32
}
type VP8Params struct {
Params
}
func NewVP8VAAPIParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (VP8Params, error) {
return VP8Params{
Params: Params{
codecName: "vp8_vaapi",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func (p *VP8Params) RTPCodec() *codec.RTPCodec {
return codec.NewRTPVP8Codec(90000)
}
func (p *VP8Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newHardwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}
type VP9Params struct {
Params
}
func NewVP9VAAPIParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (VP8Params, error) {
return VP8Params{
Params: Params{
codecName: "vp9_vaapi",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func (p *VP9Params) RTPCodec() *codec.RTPCodec {
return codec.NewRTPVP9Codec(90000)
}
func (p *VP9Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newHardwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}
type H264Params struct {
Params
}
func NewH264NVENCParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (H264Params, error) {
return H264Params{
Params: Params{
codecName: "h264_nvenc",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func NewH264VAAPIParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (H264Params, error) {
return H264Params{
Params: Params{
codecName: "h264_vaapi",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
// RTPCodec represents the codec metadata
func (p *H264Params) RTPCodec() *codec.RTPCodec {
return codec.NewRTPH264Codec(90000)
}
func (p *H264Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newHardwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}
type H264SoftwareParams struct {
Params
}
func NewH264X264Params() (H264SoftwareParams, error) {
return H264SoftwareParams{
Params: Params{
codecName: "libx264",
},
}, nil
}
func (p *H264SoftwareParams) RTPCodec() *codec.RTPCodec {
return codec.NewRTPH264Codec(90000)
}
func (p *H264SoftwareParams) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newSoftwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}
type H265Params struct {
Params
}
func NewH265NVENCParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (H265Params, error) {
return H265Params{
Params: Params{
codecName: "hevc_nvenc",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func NewH265VAAPIParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (H265Params, error) {
return H265Params{
Params: Params{
codecName: "hevc_vaapi",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func (p *H265Params) RTPCodec() *codec.RTPCodec {
return codec.NewRTPH265Codec(90000)
}
func (p *H265Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newHardwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}
type AV1Params struct {
Params
}
func NewAV1NVENCParams(hardwareDevice string, pixelFormat astiav.PixelFormat) (AV1Params, error) {
return AV1Params{
Params: Params{
codecName: "av1_nvenc",
hardwareDevice: hardwareDevice,
pixelFormat: pixelFormat,
},
}, nil
}
func (p *AV1Params) RTPCodec() *codec.RTPCodec {
return codec.NewRTPAV1Codec(90000)
}
func (p *AV1Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
readCloser, err := newHardwareEncoder(r, property, p.Params)
if err != nil {
return nil, err
}
return readCloser, nil
}

View File

@@ -1,159 +0,0 @@
// 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)
}
}

View File

@@ -3,8 +3,7 @@
// Reference: https://github.com/raspberrypi/userland/tree/master/interface/mmal // Reference: https://github.com/raspberrypi/userland/tree/master/interface/mmal
package mmal package mmal
// #cgo CFLAGS: -I/opt/vc/include // #cgo pkg-config: mmal
// #cgo LDFLAGS: -L/opt/vc/lib -lmmal -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host -lvcsm -lvcos
// #include "bridge.h" // #include "bridge.h"
import "C" import "C"
import ( import (
@@ -64,11 +63,10 @@ func (e *encoder) Read() ([]byte, func(), error) {
return nil, func() {}, io.EOF return nil, func() {}, io.EOF
} }
img, release, err := e.r.Read() img, _, err := e.r.Read()
if err != nil { if err != nil {
return nil, func() {}, err return nil, func() {}, err
} }
defer release()
imgReal := img.(*image.YCbCr) imgReal := img.(*image.YCbCr)
var y, cb, cr C.Slice var y, cb, cr C.Slice
y.data = (*C.uchar)(&imgReal.Y[0]) y.data = (*C.uchar)(&imgReal.Y[0])
@@ -92,8 +90,12 @@ func (e *encoder) Read() ([]byte, func(), error) {
return encoded, func() {}, err return encoded, func() {}, err
} }
func (e *encoder) Controller() codec.EncoderController { func (e *encoder) SetBitRate(b int) error {
return e panic("SetBitRate is not implemented")
}
func (e *encoder) ForceKeyFrame() error {
panic("ForceKeyFrame is not implemented")
} }
func (e *encoder) Close() error { func (e *encoder) Close() error {

View File

@@ -1,84 +0,0 @@
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()
}
}

View File

@@ -1 +0,0 @@
src

View File

@@ -1,23 +0,0 @@
Copyright (c) 2013, Cisco Systems
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,47 +0,0 @@
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

View File

@@ -21,26 +21,29 @@ Encoder *enc_new(const EncoderOptions opts, int *eresult) {
return NULL; return NULL;
} }
params.iUsageType = opts.usage_type; // TODO: Remove hardcoded values
params.iUsageType = CAMERA_VIDEO_REAL_TIME;
params.iPicWidth = opts.width; params.iPicWidth = opts.width;
params.iPicHeight = opts.height; params.iPicHeight = opts.height;
params.iTargetBitrate = opts.target_bitrate; params.iTargetBitrate = opts.target_bitrate;
params.iMaxBitrate = opts.target_bitrate; params.iMaxBitrate = opts.target_bitrate;
params.iRCMode = opts.rc_mode; params.iRCMode = RC_BITRATE_MODE;
params.fMaxFrameRate = opts.max_fps; params.fMaxFrameRate = opts.max_fps;
params.bEnableFrameSkip = opts.enable_frame_skip; params.bEnableFrameSkip = true;
params.uiMaxNalSize = opts.max_nal_size; params.uiMaxNalSize = 0;
params.uiIntraPeriod = opts.intra_period; params.uiIntraPeriod = 30;
params.iMultipleThreadIdc = opts.multiple_thread_idc; // set to 0, so that it'll automatically use multi threads when needed
params.iMultipleThreadIdc = 0;
// The base spatial layer 0 is the only one we use. // The base spatial layer 0 is the only one we use.
params.sSpatialLayers[0].iVideoWidth = params.iPicWidth; params.sSpatialLayers[0].iVideoWidth = params.iPicWidth;
params.sSpatialLayers[0].iVideoHeight = params.iPicHeight; params.sSpatialLayers[0].iVideoHeight = params.iPicHeight;
params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate; params.sSpatialLayers[0].fFrameRate = params.fMaxFrameRate;
params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate; params.sSpatialLayers[0].iSpatialBitrate = params.iTargetBitrate;
params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate; params.sSpatialLayers[0].iMaxSpatialBitrate = params.iTargetBitrate;
params.sSpatialLayers[0].sSliceArgument.uiSliceNum = opts.slice_num; // Single NAL unit mode
params.sSpatialLayers[0].sSliceArgument.uiSliceMode = opts.slice_mode; params.sSpatialLayers[0].sSliceArgument.uiSliceNum = 1;
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = opts.slice_size_constraint; params.sSpatialLayers[0].sSliceArgument.uiSliceMode = SM_SIZELIMITED_SLICE;
params.sSpatialLayers[0].sSliceArgument.uiSliceSizeConstraint = 12800;
rv = engine->InitializeExt(&params); rv = engine->InitializeExt(&params);
if (rv != 0) { if (rv != 0) {
@@ -69,16 +72,6 @@ void enc_free(Encoder *e, int *eresult) {
free(e); free(e);
} }
void enc_set_bitrate(Encoder *e, int bitrate) {
SEncParamExt encParamExt;
e->engine->GetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
encParamExt.iTargetBitrate=bitrate;
encParamExt.iMaxBitrate=bitrate;
encParamExt.sSpatialLayers[0].iSpatialBitrate = bitrate;
encParamExt.sSpatialLayers[0].iMaxSpatialBitrate = bitrate;
e->engine->SetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
}
// There's a good reference from ffmpeg in using the encode_frame // There's a good reference from ffmpeg in using the encode_frame
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html // Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
Slice enc_encode(Encoder *e, Frame f, int *eresult) { Slice enc_encode(Encoder *e, Frame f, int *eresult) {
@@ -87,16 +80,12 @@ Slice enc_encode(Encoder *e, Frame f, int *eresult) {
SFrameBSInfo info = {0}; SFrameBSInfo info = {0};
Slice payload = {0}; Slice payload = {0};
if(e->force_key_frame == 1) {
e->engine->ForceIntraFrame(true);
e->force_key_frame = 0;
}
pic.iPicWidth = f.width; pic.iPicWidth = f.width;
pic.iPicHeight = f.height; pic.iPicHeight = f.height;
pic.iColorFormat = videoFormatI420; pic.iColorFormat = videoFormatI420;
pic.iStride[0] = f.ystride; // We always received I420 format
pic.iStride[1] = pic.iStride[2] = f.cstride; pic.iStride[0] = pic.iPicWidth;
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth / 2;
pic.pData[0] = (unsigned char *)f.y; pic.pData[0] = (unsigned char *)f.y;
pic.pData[1] = (unsigned char *)f.u; pic.pData[1] = (unsigned char *)f.u;
pic.pData[2] = (unsigned char *)f.v; pic.pData[2] = (unsigned char *)f.v;

View File

@@ -12,8 +12,6 @@ typedef struct Slice {
typedef struct Frame { typedef struct Frame {
void *y, *u, *v; void *y, *u, *v;
int ystride;
int cstride;
int height; int height;
int width; int width;
} Frame; } Frame;
@@ -22,15 +20,6 @@ typedef struct EncoderOptions {
int width, height; int width, height;
int target_bitrate; int target_bitrate;
float max_fps; float max_fps;
EUsageType usage_type;
RC_MODES rc_mode;
bool enable_frame_skip;
unsigned int max_nal_size;
unsigned int intra_period;
int multiple_thread_idc;
unsigned int slice_num;
SliceModeEnum slice_mode;
unsigned int slice_size_constraint;
} EncoderOptions; } EncoderOptions;
typedef struct Encoder { typedef struct Encoder {
@@ -38,13 +27,11 @@ typedef struct Encoder {
ISVCEncoder *engine; ISVCEncoder *engine;
unsigned char *buff; unsigned char *buff;
int buff_size; int buff_size;
int force_key_frame;
} Encoder; } Encoder;
Encoder *enc_new(const EncoderOptions params, int *eresult); Encoder *enc_new(const EncoderOptions params, int *eresult);
void enc_free(Encoder *e, int *eresult); void enc_free(Encoder *e, int *eresult);
Slice enc_encode(Encoder *e, Frame f, int *eresult); Slice enc_encode(Encoder *e, Frame f, int *eresult);
void enc_set_bitrate(Encoder *e, int bitrate);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -1,15 +0,0 @@
//The current file is auto-generated by script: generate_codec_ver.sh
#ifndef CODEC_VER_H
#define CODEC_VER_H
#include "codec_app_def.h"
static const OpenH264Version g_stCodecVersion = {2, 1, 1, 2005};
static const char* const g_strCodecVer = "OpenH264 version:2.1.1.2005";
#define OPENH264_MAJOR (2)
#define OPENH264_MINOR (1)
#define OPENH264_REVISION (1)
#define OPENH264_RESERVED (2005)
#endif // CODEC_VER_H

View File

@@ -1,5 +1,7 @@
package openh264 package openh264
// #cgo CFLAGS: -I${SRCDIR}/../../../cvendor/include
// #cgo CXXFLAGS: -I${SRCDIR}/../../../cvendor/include
// #include <string.h> // #include <string.h>
// #include <openh264/codec_api.h> // #include <openh264/codec_api.h>
// #include <errno.h> // #include <errno.h>
@@ -33,19 +35,10 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser,
var rv C.int var rv C.int
cEncoder := C.enc_new(C.EncoderOptions{ cEncoder := C.enc_new(C.EncoderOptions{
width: C.int(p.Width), width: C.int(p.Width),
height: C.int(p.Height), height: C.int(p.Height),
target_bitrate: C.int(params.BitRate), target_bitrate: C.int(params.BitRate),
max_fps: C.float(p.FrameRate), max_fps: C.float(p.FrameRate),
usage_type: C.EUsageType(params.UsageType),
rc_mode: C.RC_MODES(params.RCMode),
enable_frame_skip: C.bool(params.EnableFrameSkip),
max_nal_size: C.uint(params.MaxNalSize),
intra_period: C.uint(params.IntraPeriod),
multiple_thread_idc: C.int(params.MultipleThreadIdc),
slice_num: C.uint(params.SliceNum),
slice_mode: C.SliceModeEnum(params.SliceMode),
slice_size_constraint: C.uint(params.SliceSizeConstraint),
}, &rv) }, &rv)
if err := errResult(rv); err != nil { if err := errResult(rv); err != nil {
return nil, fmt.Errorf("failed in creating encoder: %v", err) return nil, fmt.Errorf("failed in creating encoder: %v", err)
@@ -65,23 +58,20 @@ func (e *encoder) Read() ([]byte, func(), error) {
return nil, func() {}, io.EOF return nil, func() {}, io.EOF
} }
img, release, err := e.r.Read() img, _, err := e.r.Read()
if err != nil { if err != nil {
return nil, func() {}, err return nil, func() {}, err
} }
defer release()
yuvImg := img.(*image.YCbCr) yuvImg := img.(*image.YCbCr)
bounds := yuvImg.Bounds() bounds := yuvImg.Bounds()
var rv C.int var rv C.int
s := C.enc_encode(e.engine, C.Frame{ s := C.enc_encode(e.engine, C.Frame{
y: unsafe.Pointer(&yuvImg.Y[0]), y: unsafe.Pointer(&yuvImg.Y[0]),
u: unsafe.Pointer(&yuvImg.Cb[0]), u: unsafe.Pointer(&yuvImg.Cb[0]),
v: unsafe.Pointer(&yuvImg.Cr[0]), v: unsafe.Pointer(&yuvImg.Cr[0]),
ystride: C.int(yuvImg.YStride), height: C.int(bounds.Max.Y - bounds.Min.Y),
cstride: C.int(yuvImg.CStride), width: C.int(bounds.Max.X - bounds.Min.X),
height: C.int(bounds.Max.Y - bounds.Min.Y),
width: C.int(bounds.Max.X - bounds.Min.X),
}, &rv) }, &rv)
if err := errResult(rv); err != nil { if err := errResult(rv); err != nil {
return nil, func() {}, fmt.Errorf("failed in encoding: %v", err) return nil, func() {}, fmt.Errorf("failed in encoding: %v", err)
@@ -91,28 +81,18 @@ func (e *encoder) Read() ([]byte, func(), error) {
return encoded, func() {}, nil return encoded, func() {}, nil
} }
func (e *encoder) SetBitRate(b int) error {
panic("SetBitRate is not implemented")
}
func (e *encoder) ForceKeyFrame() error { func (e *encoder) ForceKeyFrame() error {
e.engine.force_key_frame = C.int(1) panic("ForceKeyFrame is not implemented")
return nil
}
func (e *encoder) SetBitRate(bitrate int) error {
C.enc_set_bitrate(e.engine, C.int(bitrate))
return nil
}
func (e *encoder) Controller() codec.EncoderController {
return e
} }
func (e *encoder) Close() error { func (e *encoder) Close() error {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
if e.closed {
return nil
}
e.closed = true e.closed = true
var rv C.int var rv C.int

View File

@@ -0,0 +1,6 @@
// +build !dynamic
package openh264
// #cgo LDFLAGS: ${SRCDIR}/../../../cvendor/lib/openh264/libopenh264.x86_64-darwin.a
import "C"

View File

@@ -0,0 +1,6 @@
// +build !dynamic
package openh264
// #cgo LDFLAGS: ${SRCDIR}/../../../cvendor/lib/openh264/libopenh264.x86_64-linux.a
import "C"

View File

@@ -1,13 +0,0 @@
// +build !dynamic
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 darwin,arm64 LDFLAGS: ${SRCDIR}/lib/libopenh264-darwin-arm64.a
//#cgo windows,amd64 LDFLAGS: ${SRCDIR}/lib/libopenh264-windows-x64.a -lssp
import "C"

View File

@@ -1,82 +0,0 @@
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,
),
)
})
}

View File

@@ -0,0 +1,14 @@
// +build !dynamic
package openh264
// #cgo LDFLAGS: ${SRCDIR}/../../../cvendor/lib/openh264/libopenh264.x86_64-windows.a -lssp
// #include <stdio.h>
// #include <crtdefs.h>
// #if __MINGW64_VERSION_MAJOR < 6
// // Workaround for the breaking ABI change between MinGW 5 and 6
// FILE *__cdecl __acrt_iob_func(unsigned index) { return &(__iob_func()[index]); }
// typedef FILE *__cdecl (*_f__acrt_iob_func)(unsigned index);
// _f__acrt_iob_func __MINGW_IMP_SYMBOL(__acrt_iob_func) = __acrt_iob_func;
// #endif
import "C"

View File

@@ -1,8 +1,5 @@
package openh264 package openh264
// #include <openh264/codec_api.h>
import "C"
import ( import (
"github.com/pion/mediadevices/pkg/codec" "github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/io/video" "github.com/pion/mediadevices/pkg/io/video"
@@ -12,62 +9,14 @@ import (
// Params stores libopenh264 specific encoding parameters. // Params stores libopenh264 specific encoding parameters.
type Params struct { type Params struct {
codec.BaseParams codec.BaseParams
UsageType UsageTypeEnum
RCMode RCModeEnum
EnableFrameSkip bool
MaxNalSize uint
IntraPeriod uint
MultipleThreadIdc int
SliceNum uint
SliceMode SliceModeEnum
SliceSizeConstraint uint
} }
type UsageTypeEnum int
const (
CameraVideoRealTime UsageTypeEnum = C.CAMERA_VIDEO_REAL_TIME ///< camera video for real-time communication
ScreenContentRealTime UsageTypeEnum = C.SCREEN_CONTENT_REAL_TIME ///< screen content signal
CameraVideoNonRealTime UsageTypeEnum = C.CAMERA_VIDEO_NON_REAL_TIME
ScreenContentNonRealTime UsageTypeEnum = C.SCREEN_CONTENT_NON_REAL_TIME
InputContentTypeAll UsageTypeEnum = C.INPUT_CONTENT_TYPE_ALL
)
type RCModeEnum int
const (
RCQualityMode RCModeEnum = C.RC_QUALITY_MODE ///< quality mode
RCBitrateMode RCModeEnum = C.RC_BITRATE_MODE ///< bitrate mode
RCBufferbaseedMode RCModeEnum = C.RC_BUFFERBASED_MODE ///< no bitrate control,only using buffer status,adjust the video quality
RCTimestampMode RCModeEnum = C.RC_TIMESTAMP_MODE //rate control based timestamp
RCBitrateModePostSkip RCModeEnum = C.RC_BITRATE_MODE_POST_SKIP ///< this is in-building RC MODE, WILL BE DELETED after algorithm tuning!
RCOffMode RCModeEnum = C.RC_OFF_MODE ///< rate control off mode
)
type SliceModeEnum uint
const (
SMSingleSlice SliceModeEnum = C.SM_SINGLE_SLICE ///< | SliceNum==1
SMFixedslcnumSlice SliceModeEnum = C.SM_FIXEDSLCNUM_SLICE ///< | according to SliceNum | enabled dynamic slicing for multi-thread
SMRasterSlice SliceModeEnum = C.SM_RASTER_SLICE ///< | according to SlicesAssign | need input of MB numbers each slice. In addition, if other constraint in SSliceArgument is presented, need to follow the constraints. Typically if MB num and slice size are both constrained, re-encoding may be involved.
SMSizelimitedSlice SliceModeEnum = C.SM_SIZELIMITED_SLICE ///< | according to SliceSize | slicing according to size, the slicing will be dynamic(have no idea about slice_nums until encoding current frame)
)
// NewParams returns default openh264 codec specific parameters. // NewParams returns default openh264 codec specific parameters.
func NewParams() (Params, error) { func NewParams() (Params, error) {
return Params{ return Params{
BaseParams: codec.BaseParams{ BaseParams: codec.BaseParams{
BitRate: 100000, BitRate: 100000,
}, },
UsageType: CameraVideoRealTime,
RCMode: RCBitrateMode,
EnableFrameSkip: true,
MaxNalSize: 0,
IntraPeriod: 30,
MultipleThreadIdc: 0, // Defaults to 0, so that it'll automatically use multi threads when needed
SliceNum: 1, // Defaults to single NAL unit mode
SliceMode: SMSizelimitedSlice,
SliceSizeConstraint: 12800,
}, nil }, nil
} }

View File

@@ -1 +0,0 @@
src

View File

@@ -1,44 +0,0 @@
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
Jean-Marc Valin, Timothy B. Terriberry,
CSIRO, Gregory Maxwell, Mark Borgerding,
Erik de Castro Lopo
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of Internet Society, IETF or IETF Trust, nor the
names of specific contributors, may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Opus is subject to the royalty-free patent licenses which are
specified at:
Xiph.Org Foundation:
https://datatracker.ietf.org/ipr/1524/
Microsoft Corporation:
https://datatracker.ietf.org/ipr/1914/
Broadcom Corporation:
https://datatracker.ietf.org/ipr/1526/

View File

@@ -1,31 +0,0 @@
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

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