mirror of
https://github.com/pion/mediadevices.git
synced 2025-10-26 18:10:23 +08:00
Compare commits
5 Commits
add-svt-av
...
add_codec_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8741c02e0 | ||
|
|
29b9eaf317 | ||
|
|
104cc5a5ab | ||
|
|
32bfdbb52a | ||
|
|
00e120c79f |
27
.github/workflows/ci.yaml
vendored
27
.github/workflows/ci.yaml
vendored
@@ -13,13 +13,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.25", "1.24"] # auto-update/supported-go-version-list
|
go:
|
||||||
|
- '1.21' # oldest version this package supports
|
||||||
|
- '1.22' # oldstable Go version
|
||||||
|
- '1.23' # stable Go version
|
||||||
name: Linux Go ${{ matrix.go }}
|
name: Linux Go ${{ matrix.go }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -27,7 +30,6 @@ jobs:
|
|||||||
sudo apt-get update -qq \
|
sudo apt-get update -qq \
|
||||||
&& sudo apt-get install --no-install-recommends -y \
|
&& sudo apt-get install --no-install-recommends -y \
|
||||||
libopus-dev \
|
libopus-dev \
|
||||||
libsvtav1enc-dev \
|
|
||||||
libva-dev \
|
libva-dev \
|
||||||
libvpx-dev \
|
libvpx-dev \
|
||||||
libx11-dev \
|
libx11-dev \
|
||||||
@@ -42,24 +44,25 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.25", "1.24"] # auto-update/supported-go-version-list
|
go:
|
||||||
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
name: Darwin Go ${{ matrix.go }}
|
name: Darwin Go ${{ matrix.go }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
which brew
|
which brew
|
||||||
brew install \
|
brew install \
|
||||||
libvpx \
|
|
||||||
opus \
|
|
||||||
pkg-config \
|
pkg-config \
|
||||||
svt-av1 \
|
opus \
|
||||||
|
libvpx \
|
||||||
x264
|
x264
|
||||||
- name: Run Test Suite
|
- name: Run Test Suite
|
||||||
run: make test
|
run: make test
|
||||||
@@ -71,9 +74,9 @@ jobs:
|
|||||||
name: Check Licenses
|
name: Check Licenses
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
- name: Installing go-licenses
|
- name: Installing go-licenses
|
||||||
|
|||||||
157
.github/workflows/pkg-codec-ffmpeg-ci.yaml
vendored
Normal file
157
.github/workflows/pkg-codec-ffmpeg-ci.yaml
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
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 ./...
|
||||||
6
.github/workflows/renovate-go-mod-fix.yaml
vendored
6
.github/workflows/renovate-go-mod-fix.yaml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: fix
|
- name: fix
|
||||||
@@ -20,6 +20,4 @@ jobs:
|
|||||||
github_token: ${{ secrets.PIONBOT_GITHUB_TOKEN }}
|
github_token: ${{ secrets.PIONBOT_GITHUB_TOKEN }}
|
||||||
commit_style: squash
|
commit_style: squash
|
||||||
push: force
|
push: force
|
||||||
go_mod_paths: |
|
go_mod_paths: ./
|
||||||
./
|
|
||||||
./examples/
|
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -149,14 +149,6 @@ A codec library which supports H.264 encoding and decoding. It is suitable for u
|
|||||||
* Package: [github.com/pion/mediadevices/pkg/codec/openh264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/openh264)
|
* Package: [github.com/pion/mediadevices/pkg/codec/openh264](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/openh264)
|
||||||
* Installation: no installation needed, included as a static binary
|
* Installation: no installation needed, included as a static binary
|
||||||
|
|
||||||
##### svtav1
|
|
||||||
A free software video codec library from the Alliance for Open Media that implements AV1 video coding formats.
|
|
||||||
|
|
||||||
* Package: [github.com/pion/mediadevices/pkg/codec/svtav1](https://pkg.go.dev/github.com/pion/mediadevices/pkg/codec/svtav1)
|
|
||||||
* Installation:
|
|
||||||
* Mac: `brew install svt-av1`
|
|
||||||
* Ubuntu: `apt install libsvtav1enc-dev`
|
|
||||||
|
|
||||||
##### vpx
|
##### vpx
|
||||||
A free software video codec library from Google and the Alliance for Open Media that implements VP8/VP9 video coding formats.
|
A free software video codec library from Google and the Alliance for Open Media that implements VP8/VP9 video coding formats.
|
||||||
|
|
||||||
@@ -172,6 +164,24 @@ An open source API that allows applications such as VLC media player or GStreame
|
|||||||
* Installation:
|
* Installation:
|
||||||
* Ubuntu: `apt install libva-dev`
|
* 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
|
#### Audio Codecs
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ go 1.21
|
|||||||
require (
|
require (
|
||||||
github.com/esimov/pigo v1.4.6
|
github.com/esimov/pigo v1.4.6
|
||||||
github.com/pion/mediadevices v0.0.0
|
github.com/pion/mediadevices v0.0.0
|
||||||
github.com/pion/webrtc/v4 v4.1.5
|
github.com/pion/webrtc/v4 v4.1.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blackjack/webcam v0.6.1 // indirect
|
github.com/blackjack/webcam v0.6.1 // indirect
|
||||||
github.com/gen2brain/malgo v0.11.24 // indirect
|
github.com/gen2brain/malgo v0.11.23 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.7 // indirect
|
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/interceptor v0.1.41 // indirect
|
github.com/pion/interceptor v0.1.40 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.3 // indirect
|
||||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.16 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/rtp v1.8.24 // indirect
|
github.com/pion/rtp v1.8.18 // indirect
|
||||||
github.com/pion/sctp v1.8.39 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.16 // indirect
|
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.8 // indirect
|
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.8 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/image v0.23.0 // indirect
|
golang.org/x/image v0.23.0 // indirect
|
||||||
|
|||||||
@@ -6,47 +6,47 @@ github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44am
|
|||||||
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
|
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/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/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/gen2brain/malgo v0.11.24 h1:hHcIJVfzWcEDHFdPl5Dl/CUSOjzOleY0zzAV8Kx+imE=
|
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||||
github.com/gen2brain/malgo v0.11.24/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
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 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
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 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
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 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.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
|
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||||
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
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 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
|
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 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/webrtc/v4 v4.1.5 h1:hJqfKPdRAVcXV9rsg2xcCiuXuMJ38BLW/87GsYJUtUU=
|
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||||
github.com/pion/webrtc/v4 v4.1.5/go.mod h1:vzHh7egVnZRgkK83lYzciWVszdDs759y3/eyu6AvZRA=
|
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 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
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 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
|||||||
22
go.mod
22
go.mod
@@ -4,15 +4,15 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blackjack/webcam v0.6.1
|
github.com/blackjack/webcam v0.6.1
|
||||||
github.com/gen2brain/malgo v0.11.24
|
github.com/gen2brain/malgo v0.11.23
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||||
github.com/pion/interceptor v0.1.41
|
github.com/pion/interceptor v0.1.40
|
||||||
github.com/pion/logging v0.2.4
|
github.com/pion/logging v0.2.4
|
||||||
github.com/pion/rtcp v1.2.16
|
github.com/pion/rtcp v1.2.15
|
||||||
github.com/pion/rtp v1.8.24
|
github.com/pion/rtp v1.8.19
|
||||||
github.com/pion/webrtc/v4 v4.1.5
|
github.com/pion/webrtc/v4 v4.1.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/image v0.23.0
|
golang.org/x/image v0.23.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,16 +23,16 @@ require (
|
|||||||
github.com/jezek/xgb v1.1.1 // indirect
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.7 // indirect
|
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.39 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.16 // indirect
|
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.8 // indirect
|
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.8 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
|||||||
44
go.sum
44
go.sum
@@ -2,8 +2,8 @@ github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVo
|
|||||||
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
|
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 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.24 h1:hHcIJVfzWcEDHFdPl5Dl/CUSOjzOleY0zzAV8Kx+imE=
|
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
|
||||||
github.com/gen2brain/malgo v0.11.24/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
|
||||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
@@ -24,40 +24,40 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+
|
|||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
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/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
|
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 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.41 h1:NpvX3HgWIukTf2yTBVjVGFXtpSpWgXjqz7IIpu7NsOw=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/pion/interceptor v0.1.41/go.mod h1:nEt4187unvRXJFyjiw00GKo+kIuXMWQI9K89fsosDLY=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
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 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.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.24 h1:+ICyZXUQDv95EsHN70RrA4XKJf5MGWyC6QQc1u6/ynI=
|
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c=
|
||||||
github.com/pion/rtp v1.8.24/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v3 v3.0.8 h1:RjRrjcIeQsilPzxvdaElN0CpuQZdMvcl9VZ5UY9suUM=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/srtp/v3 v3.0.8/go.mod h1:2Sq6YnDH7/UDCvkSoHSDNDeyBcFgWL0sAVycVbAsXFg=
|
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 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/webrtc/v4 v4.1.5 h1:hJqfKPdRAVcXV9rsg2xcCiuXuMJ38BLW/87GsYJUtUU=
|
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
|
||||||
github.com/pion/webrtc/v4 v4.1.5/go.mod h1:vzHh7egVnZRgkK83lYzciWVszdDs759y3/eyu6AvZRA=
|
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 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
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 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package mediadevices
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
"github.com/pion/mediadevices/pkg/codec"
|
||||||
@@ -94,7 +93,13 @@ func TestMediaStreamFilters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range actual {
|
for _, a := range actual {
|
||||||
found := slices.Contains(expected, a)
|
found := false
|
||||||
|
for _, e := range expected {
|
||||||
|
if e == a {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a)
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package codec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BitrateTracker struct {
|
|
||||||
windowSize time.Duration
|
|
||||||
buffer []int
|
|
||||||
times []time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBitrateTracker(windowSize time.Duration) *BitrateTracker {
|
|
||||||
return &BitrateTracker{
|
|
||||||
windowSize: windowSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bt *BitrateTracker) AddFrame(sizeBytes int, timestamp time.Time) {
|
|
||||||
bt.buffer = append(bt.buffer, sizeBytes)
|
|
||||||
bt.times = append(bt.times, timestamp)
|
|
||||||
|
|
||||||
// Remove old entries outside the window
|
|
||||||
cutoff := timestamp.Add(-bt.windowSize)
|
|
||||||
i := 0
|
|
||||||
for ; i < len(bt.times); i++ {
|
|
||||||
if bt.times[i].After(cutoff) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bt.buffer = bt.buffer[i:]
|
|
||||||
bt.times = bt.times[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bt *BitrateTracker) GetBitrate() float64 {
|
|
||||||
if len(bt.times) < 2 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
totalBytes := 0
|
|
||||||
for _, b := range bt.buffer {
|
|
||||||
totalBytes += b
|
|
||||||
}
|
|
||||||
duration := bt.times[len(bt.times)-1].Sub(bt.times[0]).Seconds()
|
|
||||||
if duration <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return float64(totalBytes*8) / duration // bits per second
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package codec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBitrateTracker(t *testing.T) {
|
|
||||||
packetSize := 1000
|
|
||||||
bt := NewBitrateTracker(time.Second)
|
|
||||||
bt.AddFrame(packetSize, time.Now())
|
|
||||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*100))
|
|
||||||
bt.AddFrame(packetSize, time.Now().Add(time.Millisecond*999))
|
|
||||||
eps := float64(packetSize*8) / 10
|
|
||||||
if got, want := bt.GetBitrate(), float64(packetSize*8)*3; math.Abs(got-want) > eps {
|
|
||||||
t.Fatalf("GetBitrate() = %v, want %v (|diff| <= %v)", got, want, eps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package codec
|
package codec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/io/audio"
|
"github.com/pion/mediadevices/pkg/io/audio"
|
||||||
@@ -155,19 +153,10 @@ type ReadCloser interface {
|
|||||||
Controllable
|
Controllable
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoDecoderBuilder interface {
|
|
||||||
BuildVideoDecoder(r io.Reader, p prop.Media) (VideoDecoder, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type VideoDecoder interface {
|
|
||||||
Read() (image.Image, func(), error)
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncoderController is the interface allowing to control the encoder behaviour after it's initialisation.
|
// EncoderController is the interface allowing to control the encoder behaviour after it's initialisation.
|
||||||
// It will possibly have common control method in the future.
|
// It will possibly have common control method in the future.
|
||||||
// A controller can have optional methods represented by *Controller interfaces
|
// A controller can have optional methods represented by *Controller interfaces
|
||||||
type EncoderController any
|
type EncoderController interface{}
|
||||||
|
|
||||||
// Controllable is a interface representing a encoder which can be controlled
|
// Controllable is a interface representing a encoder which can be controlled
|
||||||
// after it's initialisation with an EncoderController
|
// after it's initialisation with an EncoderController
|
||||||
@@ -190,12 +179,6 @@ type BitRateController interface {
|
|||||||
SetBitRate(int) error
|
SetBitRate(int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type QPController interface {
|
|
||||||
EncoderController
|
|
||||||
// DynamicQPControl adjusts the QP of the encoder based on the current and target bitrate
|
|
||||||
DynamicQPControl(currentBitrate int, targetBitrate int) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaseParams represents an codec's encoding properties
|
// BaseParams represents an codec's encoding properties
|
||||||
type BaseParams struct {
|
type BaseParams struct {
|
||||||
// Target bitrate in bps.
|
// Target bitrate in bps.
|
||||||
|
|||||||
1
pkg/codec/ffmpeg/.gitignore
vendored
Normal file
1
pkg/codec/ffmpeg/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tmp
|
||||||
41
pkg/codec/ffmpeg/Makefile
Normal file
41
pkg/codec/ffmpeg/Makefile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
18
pkg/codec/ffmpeg/errors.go
Normal file
18
pkg/codec/ffmpeg/errors.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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")
|
||||||
|
)
|
||||||
439
pkg/codec/ffmpeg/ffmpeg.go
Normal file
439
pkg/codec/ffmpeg/ffmpeg.go
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
357
pkg/codec/ffmpeg/ffmpeg_test.go
Normal file
357
pkg/codec/ffmpeg/ffmpeg_test.go
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
pkg/codec/ffmpeg/go.mod
Normal file
36
pkg/codec/ffmpeg/go.mod
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
56
pkg/codec/ffmpeg/go.sum
Normal file
56
pkg/codec/ffmpeg/go.sum
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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=
|
||||||
191
pkg/codec/ffmpeg/params.go
Normal file
191
pkg/codec/ffmpeg/params.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#include <EbSvtAv1.h>
|
|
||||||
#include <EbSvtAv1Enc.h>
|
|
||||||
#include <EbSvtAv1ErrorCodes.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define ERR_INIT_ENC_HANDLER 1
|
|
||||||
#define ERR_SET_ENC_PARAM 2
|
|
||||||
#define ERR_ENC_INIT 3
|
|
||||||
#define ERR_SEND_PICTURE 4
|
|
||||||
#define ERR_GET_PACKET 5
|
|
||||||
|
|
||||||
typedef struct Encoder {
|
|
||||||
EbSvtAv1EncConfiguration *param;
|
|
||||||
EbComponentType *handle;
|
|
||||||
EbBufferHeaderType *in_buf;
|
|
||||||
|
|
||||||
bool force_keyframe;
|
|
||||||
} Encoder;
|
|
||||||
|
|
||||||
int enc_free(Encoder *e) {
|
|
||||||
free(e->in_buf->p_buffer);
|
|
||||||
free(e->in_buf);
|
|
||||||
free(e->param);
|
|
||||||
free(e);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_new(Encoder **e) {
|
|
||||||
*e = malloc(sizeof(Encoder));
|
|
||||||
(*e)->param = malloc(sizeof(EbSvtAv1EncConfiguration));
|
|
||||||
(*e)->in_buf = malloc(sizeof(EbBufferHeaderType));
|
|
||||||
|
|
||||||
memset((*e)->in_buf, 0, sizeof(EbBufferHeaderType));
|
|
||||||
(*e)->in_buf->p_buffer = malloc(sizeof(EbSvtIOFormat));
|
|
||||||
(*e)->in_buf->size = sizeof(EbBufferHeaderType);
|
|
||||||
|
|
||||||
#if SVT_AV1_CHECK_VERSION(3, 0, 0)
|
|
||||||
const EbErrorType sret = svt_av1_enc_init_handle(&(*e)->handle, (*e)->param);
|
|
||||||
#else
|
|
||||||
const EbErrorType sret = svt_av1_enc_init_handle(&(*e)->handle, NULL, (*e)->param);
|
|
||||||
#endif
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
enc_free(*e);
|
|
||||||
return ERR_INIT_ENC_HANDLER;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_init(Encoder *e) {
|
|
||||||
EbErrorType sret;
|
|
||||||
|
|
||||||
e->param->encoder_bit_depth = 8;
|
|
||||||
e->param->encoder_color_format = EB_YUV420;
|
|
||||||
|
|
||||||
sret = svt_av1_enc_set_parameter(e->handle, e->param);
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
return ERR_SET_ENC_PARAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
sret = svt_av1_enc_init(e->handle);
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
return ERR_ENC_INIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_apply_param(Encoder *e) {
|
|
||||||
const EbErrorType sret = svt_av1_enc_set_parameter(e->handle, e->param);
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
return ERR_SET_ENC_PARAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_force_keyframe(Encoder *e) {
|
|
||||||
e->force_keyframe = true;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_send_frame(Encoder *e, uint8_t *y, uint8_t *cb, uint8_t *cr, int ystride, int cstride) {
|
|
||||||
EbSvtIOFormat *in_data = (EbSvtIOFormat *)e->in_buf->p_buffer;
|
|
||||||
in_data->luma = y;
|
|
||||||
in_data->cb = cb;
|
|
||||||
in_data->cr = cr;
|
|
||||||
in_data->y_stride = ystride;
|
|
||||||
in_data->cb_stride = cstride;
|
|
||||||
in_data->cr_stride = cstride;
|
|
||||||
|
|
||||||
e->in_buf->pic_type = EB_AV1_INVALID_PICTURE; // auto
|
|
||||||
if (e->force_keyframe) {
|
|
||||||
e->in_buf->pic_type = EB_AV1_KEY_PICTURE;
|
|
||||||
e->force_keyframe = false;
|
|
||||||
}
|
|
||||||
e->in_buf->flags = 0;
|
|
||||||
e->in_buf->pts++;
|
|
||||||
e->in_buf->n_filled_len = ystride * e->param->source_height;
|
|
||||||
e->in_buf->n_filled_len += 2 * cstride * e->param->source_height / 2;
|
|
||||||
|
|
||||||
const EbErrorType sret = svt_av1_enc_send_picture(e->handle, e->in_buf);
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
return ERR_SEND_PICTURE;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int enc_get_packet(Encoder *e, EbBufferHeaderType **out) {
|
|
||||||
const EbErrorType sret = svt_av1_enc_get_packet(e->handle, out, 0);
|
|
||||||
if (sret == EB_NoErrorEmptyQueue) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (sret != EB_ErrorNone) {
|
|
||||||
return ERR_GET_PACKET;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void memcpy_uint8(uint8_t *dst, const uint8_t *src, size_t n) {
|
|
||||||
// Just make CGO types compatible
|
|
||||||
memcpy(dst, src, n);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package svtav1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUnknownErrorCode = errors.New("unknown error code")
|
|
||||||
ErrInitEncHandler = errors.New("failed to initialize encoder handler")
|
|
||||||
ErrSetEncParam = errors.New("failed to set encoder parameters")
|
|
||||||
ErrEncInit = errors.New("failed to initialize encoder")
|
|
||||||
ErrSendPicture = errors.New("failed to send picture")
|
|
||||||
ErrGetPacket = errors.New("failed to get packet")
|
|
||||||
)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package svtav1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Params stores libx264 specific encoding parameters.
|
|
||||||
type Params struct {
|
|
||||||
codec.BaseParams
|
|
||||||
|
|
||||||
// Preset configuration number of SVT-AV1
|
|
||||||
// 1-3: extremely high efficiency but heavy
|
|
||||||
// 4-6: a balance of efficiency and reasonable compute time
|
|
||||||
// 7-13: real-time encoding
|
|
||||||
Preset int
|
|
||||||
|
|
||||||
StartingBufferLevel time.Duration
|
|
||||||
OptimalBufferLevel time.Duration
|
|
||||||
MaximumBufferSize time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParams returns default x264 codec specific parameters.
|
|
||||||
func NewParams() (Params, error) {
|
|
||||||
return Params{
|
|
||||||
BaseParams: codec.BaseParams{
|
|
||||||
KeyFrameInterval: 60,
|
|
||||||
},
|
|
||||||
Preset: 9,
|
|
||||||
StartingBufferLevel: 400 * time.Millisecond,
|
|
||||||
OptimalBufferLevel: 200 * time.Millisecond,
|
|
||||||
MaximumBufferSize: 500 * time.Millisecond,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RTPCodec represents the codec metadata
|
|
||||||
func (p *Params) RTPCodec() *codec.RTPCodec {
|
|
||||||
return codec.NewRTPAV1Codec(90000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildVideoEncoder builds x264 encoder with given params
|
|
||||||
func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) {
|
|
||||||
return newEncoder(r, property, *p)
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
// Package svtav1 implements AV1 encoder.
|
|
||||||
// This package requires libSvtAv1Enc headers and libraries to be built.
|
|
||||||
package svtav1
|
|
||||||
|
|
||||||
// #cgo pkg-config: SvtAv1Enc
|
|
||||||
// #include "bridge.h"
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
)
|
|
||||||
|
|
||||||
type encoder struct {
|
|
||||||
engine *C.Encoder
|
|
||||||
r video.Reader
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
|
|
||||||
outPool sync.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
|
|
||||||
var enc *C.Encoder
|
|
||||||
|
|
||||||
if p.FrameRate == 0 {
|
|
||||||
p.FrameRate = 30
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_new(&enc)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
enc.param.source_width = C.uint32_t(p.Width)
|
|
||||||
enc.param.source_height = C.uint32_t(p.Height)
|
|
||||||
enc.param.profile = C.MAIN_PROFILE
|
|
||||||
enc.param.enc_mode = C.int8_t(params.Preset)
|
|
||||||
enc.param.rate_control_mode = C.SVT_AV1_RC_MODE_CBR
|
|
||||||
enc.param.pred_structure = C.SVT_AV1_PRED_LOW_DELAY_B
|
|
||||||
enc.param.target_bit_rate = C.uint32_t(params.BitRate)
|
|
||||||
enc.param.frame_rate_numerator = C.uint32_t(p.FrameRate * 1000)
|
|
||||||
enc.param.frame_rate_denominator = 1000
|
|
||||||
enc.param.intra_refresh_type = C.SVT_AV1_KF_REFRESH
|
|
||||||
enc.param.intra_period_length = C.int32_t(params.KeyFrameInterval)
|
|
||||||
enc.param.starting_buffer_level_ms = C.int64_t(params.StartingBufferLevel.Milliseconds())
|
|
||||||
enc.param.optimal_buffer_level_ms = C.int64_t(params.OptimalBufferLevel.Milliseconds())
|
|
||||||
enc.param.maximum_buffer_size_ms = C.int64_t(params.MaximumBufferSize.Milliseconds())
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_init(enc)); err != nil {
|
|
||||||
_ = C.enc_free(enc)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e := encoder{
|
|
||||||
engine: enc,
|
|
||||||
r: video.ToI420(r),
|
|
||||||
outPool: sync.Pool{
|
|
||||||
New: func() any {
|
|
||||||
return []byte(nil)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return &e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func errFromC(ret C.int) error {
|
|
||||||
switch ret {
|
|
||||||
case 0:
|
|
||||||
return nil
|
|
||||||
case C.ERR_INIT_ENC_HANDLER:
|
|
||||||
return ErrInitEncHandler
|
|
||||||
case C.ERR_SET_ENC_PARAM:
|
|
||||||
return ErrSetEncParam
|
|
||||||
case C.ERR_ENC_INIT:
|
|
||||||
return ErrEncInit
|
|
||||||
case C.ERR_SEND_PICTURE:
|
|
||||||
return ErrSendPicture
|
|
||||||
case C.ERR_GET_PACKET:
|
|
||||||
return ErrGetPacket
|
|
||||||
default:
|
|
||||||
return ErrUnknownErrorCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) Read() ([]byte, func(), error) {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
if e.closed {
|
|
||||||
return nil, func() {}, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
img, release, err := e.r.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
defer release()
|
|
||||||
yuvImg := img.(*image.YCbCr)
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_send_frame(
|
|
||||||
e.engine,
|
|
||||||
(*C.uchar)(&yuvImg.Y[0]),
|
|
||||||
(*C.uchar)(&yuvImg.Cb[0]),
|
|
||||||
(*C.uchar)(&yuvImg.Cr[0]),
|
|
||||||
C.int(yuvImg.YStride),
|
|
||||||
C.int(yuvImg.CStride),
|
|
||||||
)); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf *C.EbBufferHeaderType
|
|
||||||
if err := errFromC(C.enc_get_packet(e.engine, &buf)); err != nil {
|
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
|
||||||
if buf == nil {
|
|
||||||
// Feed frames until receiving a packet
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
n := int(buf.n_filled_len)
|
|
||||||
outBuf := e.outPool.Get().([]byte)
|
|
||||||
if cap(outBuf) < n {
|
|
||||||
outBuf = make([]byte, n)
|
|
||||||
} else {
|
|
||||||
outBuf = outBuf[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
C.memcpy_uint8((*C.uchar)(&outBuf[0]), buf.p_buffer, C.size_t(n))
|
|
||||||
C.svt_av1_enc_release_out_buffer(&buf)
|
|
||||||
|
|
||||||
return outBuf, func() {
|
|
||||||
e.outPool.Put(outBuf)
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) ForceKeyFrame() error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_force_keyframe(e.engine)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) SetBitRate(bitrate int) error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
e.engine.param.target_bit_rate = C.uint32_t(bitrate)
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_apply_param(e.engine)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) Close() error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
if e.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := errFromC(C.enc_free(e.engine)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.closed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package svtav1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
|
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getTestVideoEncoder() (codec.ReadCloser, error) {
|
|
||||||
p, err := NewParams()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.BitRate = 200000
|
|
||||||
enc, err := p.BuildVideoEncoder(video.ReaderFunc(func() (image.Image, func(), error) {
|
|
||||||
return image.NewYCbCr(
|
|
||||||
image.Rect(0, 0, 256, 144),
|
|
||||||
image.YCbCrSubsampleRatio420,
|
|
||||||
), nil, nil
|
|
||||||
}), prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: 256,
|
|
||||||
Height: 144,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return enc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncoder(t *testing.T) {
|
|
||||||
t.Run("SimpleRead", func(t *testing.T) {
|
|
||||||
p, err := NewParams()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.BitRate = 200000
|
|
||||||
codectest.VideoEncoderSimpleReadTest(t, &p,
|
|
||||||
prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: 256,
|
|
||||||
Height: 144,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
image.NewYCbCr(
|
|
||||||
image.Rect(0, 0, 256, 144),
|
|
||||||
image.YCbCrSubsampleRatio420,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
t.Run("CloseTwice", func(t *testing.T) {
|
|
||||||
p, err := NewParams()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.BitRate = 200000
|
|
||||||
codectest.VideoEncoderCloseTwiceTest(t, &p, prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: 640,
|
|
||||||
Height: 480,
|
|
||||||
FrameRate: 30,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("ReadAfterClose", func(t *testing.T) {
|
|
||||||
p, err := NewParams()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.BitRate = 200000
|
|
||||||
codectest.VideoEncoderReadAfterCloseTest(t, &p,
|
|
||||||
prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: 256,
|
|
||||||
Height: 144,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
image.NewYCbCr(
|
|
||||||
image.Rect(0, 0, 256, 144),
|
|
||||||
image.YCbCrSubsampleRatio420,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldImplementKeyFrameControl(t *testing.T) {
|
|
||||||
e := &encoder{}
|
|
||||||
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
|
|
||||||
t.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoErrorOnForceKeyFrame(t *testing.T) {
|
|
||||||
enc, err := getTestVideoEncoder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
kfc, ok := enc.Controller().(codec.KeyFrameController)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Failed to get KeyFrameController")
|
|
||||||
}
|
|
||||||
if err := kfc.ForceKeyFrame(); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
_, rel, err := enc.Read() // try to read the encoded frame
|
|
||||||
rel()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldImplementBitRateControl(t *testing.T) {
|
|
||||||
e := &encoder{}
|
|
||||||
if _, ok := e.Controller().(codec.BitRateController); !ok {
|
|
||||||
t.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoErrorOnSetBitRate(t *testing.T) {
|
|
||||||
enc, err := getTestVideoEncoder()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
brc, ok := enc.Controller().(codec.BitRateController)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("Failed to get BitRateController")
|
|
||||||
}
|
|
||||||
if err := brc.SetBitRate(1000); err != nil { // 1000 bit/second is ridiculously low, but this is a testcase.
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
_, rel, err := enc.Read() // try to read the encoded frame
|
|
||||||
rel()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,7 +54,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -82,12 +81,6 @@ type encoder struct {
|
|||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
kRateControlThreshold = 0.15
|
|
||||||
kMinQuantizer = 20
|
|
||||||
kMaxQuantizer = 63
|
|
||||||
)
|
|
||||||
|
|
||||||
// VP8Params is codec specific paramaters
|
// VP8Params is codec specific paramaters
|
||||||
type VP8Params struct {
|
type VP8Params struct {
|
||||||
Params
|
Params
|
||||||
@@ -261,10 +254,6 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ec := C.vpx_codec_enc_config_set(e.codec, e.cfg); ec != 0 {
|
|
||||||
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec)
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := t.Sub(e.tLastFrame).Microseconds()
|
duration := t.Sub(e.tLastFrame).Microseconds()
|
||||||
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
||||||
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
||||||
@@ -333,24 +322,6 @@ func (e *encoder) SetBitRate(bitrate int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *encoder) DynamicQPControl(currentBitrate int, targetBitrate int) error {
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
bitrateDiff := math.Abs(float64(currentBitrate - targetBitrate))
|
|
||||||
if bitrateDiff <= float64(currentBitrate)*kRateControlThreshold {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
currentMax := e.cfg.rc_max_quantizer
|
|
||||||
|
|
||||||
if targetBitrate < currentBitrate {
|
|
||||||
e.cfg.rc_max_quantizer = min(currentMax+1, kMaxQuantizer)
|
|
||||||
} else {
|
|
||||||
e.cfg.rc_max_quantizer = max(currentMax-1, kMinQuantizer)
|
|
||||||
}
|
|
||||||
e.cfg.rc_min_quantizer = e.cfg.rc_max_quantizer
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *encoder) Controller() codec.EncoderController {
|
func (e *encoder) Controller() codec.EncoderController {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
package vpx
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo pkg-config: vpx
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <vpx/vpx_decoder.h>
|
|
||||||
#include <vpx/vpx_codec.h>
|
|
||||||
#include <vpx/vpx_image.h>
|
|
||||||
#include <vpx/vp8dx.h>
|
|
||||||
|
|
||||||
vpx_codec_iface_t *ifaceVP8Decoder() {
|
|
||||||
return vpx_codec_vp8_dx();
|
|
||||||
}
|
|
||||||
vpx_codec_iface_t *ifaceVP9Decoder() {
|
|
||||||
return vpx_codec_vp9_dx();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocates a new decoder context
|
|
||||||
vpx_codec_ctx_t* newDecoderCtx() {
|
|
||||||
return (vpx_codec_ctx_t*)malloc(sizeof(vpx_codec_ctx_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializes the decoder
|
|
||||||
vpx_codec_err_t decoderInit(vpx_codec_ctx_t* ctx, vpx_codec_iface_t* iface) {
|
|
||||||
return vpx_codec_dec_init_ver(ctx, iface, NULL, 0, VPX_DECODER_ABI_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decodes an encoded frame
|
|
||||||
vpx_codec_err_t decodeFrame(vpx_codec_ctx_t* ctx, const uint8_t* data, unsigned int data_sz) {
|
|
||||||
return vpx_codec_decode(ctx, data, data_sz, NULL, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates an iterator
|
|
||||||
vpx_codec_iter_t* newIter() {
|
|
||||||
return (vpx_codec_iter_t*)malloc(sizeof(vpx_codec_iter_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the next decoded frame
|
|
||||||
vpx_image_t* getFrame(vpx_codec_ctx_t* ctx, vpx_codec_iter_t* iter) {
|
|
||||||
return vpx_codec_get_frame(ctx, iter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frees a decoded frane
|
|
||||||
void freeFrame(vpx_image_t* f) {
|
|
||||||
vpx_img_free(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frees a decoder context
|
|
||||||
void freeDecoderCtx(vpx_codec_ctx_t* ctx) {
|
|
||||||
vpx_codec_destroy(ctx);
|
|
||||||
free(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/codec"
|
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
|
||||||
)
|
|
||||||
|
|
||||||
type decoder struct {
|
|
||||||
codec *C.vpx_codec_ctx_t
|
|
||||||
raw *C.vpx_image_t
|
|
||||||
cfg *C.vpx_codec_dec_cfg_t
|
|
||||||
iter C.vpx_codec_iter_t
|
|
||||||
frameIndex int
|
|
||||||
tStart time.Time
|
|
||||||
tLastFrame time.Time
|
|
||||||
reader io.Reader
|
|
||||||
buf []byte
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildVideoDecoder(r io.Reader, property prop.Media) (codec.VideoDecoder, error) {
|
|
||||||
return NewDecoder(r, property)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDecoder(r io.Reader, p prop.Media) (codec.VideoDecoder, error) {
|
|
||||||
cfg := &C.vpx_codec_dec_cfg_t{}
|
|
||||||
cfg.threads = 1
|
|
||||||
cfg.w = C.uint(p.Width)
|
|
||||||
cfg.h = C.uint(p.Height)
|
|
||||||
|
|
||||||
codec := C.newDecoderCtx()
|
|
||||||
if C.decoderInit(codec, C.ifaceVP8Decoder()) != C.VPX_CODEC_OK {
|
|
||||||
return nil, fmt.Errorf("vpx_codec_dec_init failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &decoder{
|
|
||||||
codec: codec,
|
|
||||||
cfg: cfg,
|
|
||||||
iter: nil, // initialize to NULL to start iteration
|
|
||||||
reader: r,
|
|
||||||
buf: make([]byte, 1024*1024),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *decoder) Read() (image.Image, func(), error) {
|
|
||||||
var input *C.vpx_image_t
|
|
||||||
for {
|
|
||||||
input = C.getFrame(d.codec, &d.iter)
|
|
||||||
if input != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
d.iter = nil
|
|
||||||
// Read if there are no remained frames in the decoder
|
|
||||||
n, err := d.reader.Read(d.buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
status := C.decodeFrame(d.codec, (*C.uint8_t)(&d.buf[0]), C.uint(n))
|
|
||||||
if status != C.VPX_CODEC_OK {
|
|
||||||
return nil, nil, fmt.Errorf("decode failed: %v", status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w := int(input.d_w)
|
|
||||||
h := int(input.d_h)
|
|
||||||
yStride := int(input.stride[0])
|
|
||||||
uStride := int(input.stride[1])
|
|
||||||
vStride := int(input.stride[2])
|
|
||||||
|
|
||||||
ySrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[0])), yStride*h)
|
|
||||||
uSrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[1])), uStride*h/2)
|
|
||||||
vSrc := unsafe.Slice((*byte)(unsafe.Pointer(input.planes[2])), vStride*h/2)
|
|
||||||
|
|
||||||
dst := image.NewYCbCr(image.Rect(0, 0, w, h), image.YCbCrSubsampleRatio420)
|
|
||||||
|
|
||||||
// copy luma
|
|
||||||
for r := 0; r < h; r++ {
|
|
||||||
copy(dst.Y[r*dst.YStride:r*dst.YStride+w], ySrc[r*yStride:r*yStride+w])
|
|
||||||
}
|
|
||||||
// copy chroma
|
|
||||||
for r := 0; r < h/2; r++ {
|
|
||||||
copy(dst.Cb[r*dst.CStride:r*dst.CStride+w/2], uSrc[r*uStride:r*uStride+w/2])
|
|
||||||
copy(dst.Cr[r*dst.CStride:r*dst.CStride+w/2], vSrc[r*vStride:r*vStride+w/2])
|
|
||||||
}
|
|
||||||
C.freeFrame(input)
|
|
||||||
return dst, func() {}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *decoder) Close() error {
|
|
||||||
C.freeDecoderCtx(d.codec)
|
|
||||||
d.closed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -4,9 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,7 +13,6 @@ import (
|
|||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncoder(t *testing.T) {
|
func TestEncoder(t *testing.T) {
|
||||||
@@ -364,155 +360,3 @@ func TestEncoderFrameMonotonic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVP8DynamicQPControl(t *testing.T) {
|
|
||||||
t.Run("VP8", func(t *testing.T) {
|
|
||||||
p, err := NewVP8Params()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.LagInFrames = 0 // Disable frame lag buffering for real-time encoding
|
|
||||||
p.RateControlEndUsage = RateControlCBR
|
|
||||||
totalFrames := 100
|
|
||||||
frameRate := 10
|
|
||||||
initialWidth, initialHeight := 800, 600
|
|
||||||
var cnt uint32
|
|
||||||
|
|
||||||
r, err := p.BuildVideoEncoder(
|
|
||||||
video.ReaderFunc(func() (image.Image, func(), error) {
|
|
||||||
i := atomic.AddUint32(&cnt, 1)
|
|
||||||
if i == uint32(totalFrames+1) {
|
|
||||||
return nil, nil, io.EOF
|
|
||||||
}
|
|
||||||
img := image.NewYCbCr(image.Rect(0, 0, initialWidth, initialHeight), image.YCbCrSubsampleRatio420)
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
for i := range img.Y {
|
|
||||||
img.Y[i] = uint8(r.Intn(256))
|
|
||||||
}
|
|
||||||
for i := range img.Cb {
|
|
||||||
img.Cb[i] = uint8(r.Intn(256))
|
|
||||||
}
|
|
||||||
for i := range img.Cr {
|
|
||||||
img.Cr[i] = uint8(r.Intn(256))
|
|
||||||
}
|
|
||||||
return img, func() {}, nil
|
|
||||||
}),
|
|
||||||
prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: initialWidth,
|
|
||||||
Height: initialHeight,
|
|
||||||
FrameRate: float32(frameRate),
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
initialBitrate := 100
|
|
||||||
currentBitrate := initialBitrate
|
|
||||||
targetBitrate := 300
|
|
||||||
for i := 0; i < totalFrames; i++ {
|
|
||||||
r.Controller().(codec.KeyFrameController).ForceKeyFrame()
|
|
||||||
r.Controller().(codec.QPController).DynamicQPControl(currentBitrate, targetBitrate)
|
|
||||||
data, rel, err := r.Read()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
rel()
|
|
||||||
encodedSize := len(data)
|
|
||||||
currentBitrate = encodedSize * 8 / 1000 / frameRate
|
|
||||||
}
|
|
||||||
assert.Less(t, math.Abs(float64(targetBitrate-currentBitrate)), math.Abs(float64(initialBitrate-currentBitrate)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVP8EncodeDecode(t *testing.T) {
|
|
||||||
t.Run("VP8", func(t *testing.T) {
|
|
||||||
initialWidth, initialHeight := 800, 600
|
|
||||||
reader, writer := io.Pipe()
|
|
||||||
decoder, err := BuildVideoDecoder(reader, prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: initialWidth,
|
|
||||||
Height: initialHeight,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error creating VP8 decoder: %v", err)
|
|
||||||
}
|
|
||||||
defer decoder.Close()
|
|
||||||
|
|
||||||
// [... encoder setup code ...]
|
|
||||||
p, err := NewVP8Params()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
p.LagInFrames = 0 // Disable frame lag buffering for real-time encoding
|
|
||||||
p.RateControlEndUsage = RateControlCBR
|
|
||||||
totalFrames := 10
|
|
||||||
var cnt uint32
|
|
||||||
r, err := p.BuildVideoEncoder(
|
|
||||||
video.ReaderFunc(func() (image.Image, func(), error) {
|
|
||||||
i := atomic.AddUint32(&cnt, 1)
|
|
||||||
if i == uint32(totalFrames+1) {
|
|
||||||
return nil, nil, io.EOF
|
|
||||||
}
|
|
||||||
img := image.NewYCbCr(image.Rect(0, 0, initialWidth, initialHeight), image.YCbCrSubsampleRatio420)
|
|
||||||
return img, func() {}, nil
|
|
||||||
}),
|
|
||||||
prop.Media{
|
|
||||||
Video: prop.Video{
|
|
||||||
Width: initialWidth,
|
|
||||||
Height: initialHeight,
|
|
||||||
FrameRate: 30,
|
|
||||||
FrameFormat: frame.FormatI420,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
counter := 0
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
img, rel, err := decoder.Read()
|
|
||||||
if err == io.EOF {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("decoder read error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.Equal(t, initialWidth, img.Bounds().Dx())
|
|
||||||
assert.Equal(t, initialHeight, img.Bounds().Dy())
|
|
||||||
rel()
|
|
||||||
counter++
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// --- feed encoded frames to writer
|
|
||||||
for {
|
|
||||||
data, rel, err := r.Read()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("encoder error: %v", err)
|
|
||||||
}
|
|
||||||
_, werr := writer.Write(data)
|
|
||||||
rel()
|
|
||||||
if werr != nil {
|
|
||||||
t.Fatalf("writer error: %v", werr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.Close()
|
|
||||||
wg.Wait()
|
|
||||||
assert.Equal(t, totalFrames, counter)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// Package vnc implements a VNC client.
|
// Package vnc implements a VNC client.
|
||||||
//
|
//
|
||||||
// References:
|
// References:
|
||||||
//
|
// [PROTOCOL]: http://tools.ietf.org/html/rfc6143
|
||||||
// [PROTOCOL]: http://tools.ietf.org/html/rfc6143
|
|
||||||
package vnc
|
package vnc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -97,7 +96,7 @@ func (c *ClientConn) CutText(text string) error {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
// This is the fixed size data we'll send
|
// This is the fixed size data we'll send
|
||||||
fixedData := []any{
|
fixedData := []interface{}{
|
||||||
uint8(6),
|
uint8(6),
|
||||||
uint8(0),
|
uint8(0),
|
||||||
uint8(0),
|
uint8(0),
|
||||||
@@ -142,7 +141,7 @@ func (c *ClientConn) FramebufferUpdateRequest(incremental bool, x, y, width, hei
|
|||||||
incrementalByte = 1
|
incrementalByte = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
data := []any{
|
data := []interface{}{
|
||||||
uint8(3),
|
uint8(3),
|
||||||
incrementalByte,
|
incrementalByte,
|
||||||
x, y, width, height,
|
x, y, width, height,
|
||||||
@@ -173,7 +172,7 @@ func (c *ClientConn) KeyEvent(keysym uint32, down bool) error {
|
|||||||
downFlag = 1
|
downFlag = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
data := []any{
|
data := []interface{}{
|
||||||
uint8(4),
|
uint8(4),
|
||||||
downFlag,
|
downFlag,
|
||||||
uint8(0),
|
uint8(0),
|
||||||
@@ -200,7 +199,7 @@ func (c *ClientConn) KeyEvent(keysym uint32, down bool) error {
|
|||||||
func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
data := []any{
|
data := []interface{}{
|
||||||
uint8(5),
|
uint8(5),
|
||||||
uint8(mask),
|
uint8(mask),
|
||||||
x,
|
x,
|
||||||
@@ -226,7 +225,7 @@ func (c *ClientConn) PointerEvent(mask ButtonMask, x, y uint16) error {
|
|||||||
//
|
//
|
||||||
// See RFC 6143 Section 7.5.2
|
// See RFC 6143 Section 7.5.2
|
||||||
func (c *ClientConn) SetEncodings(encs []Encoding) error {
|
func (c *ClientConn) SetEncodings(encs []Encoding) error {
|
||||||
data := make([]any, 3+len(encs))
|
data := make([]interface{}, 3+len(encs))
|
||||||
data[0] = uint8(2)
|
data[0] = uint8(2)
|
||||||
data[1] = uint8(0)
|
data[1] = uint8(0)
|
||||||
data[2] = uint16(len(encs))
|
data[2] = uint16(len(encs))
|
||||||
@@ -320,7 +319,7 @@ func (c *ClientConn) handshake() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Respond with the version we will support
|
// Respond with the version we will support
|
||||||
if maxMinor < 8 {
|
if maxMinor<8 {
|
||||||
if _, err = c.c.Write([]byte("RFB 003.003\n")); err != nil {
|
if _, err = c.c.Write([]byte("RFB 003.003\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -332,7 +331,7 @@ func (c *ClientConn) handshake() error {
|
|||||||
if numSecurityTypes == 0 {
|
if numSecurityTypes == 0 {
|
||||||
return fmt.Errorf("no security types: %s", c.readErrorReason())
|
return fmt.Errorf("no security types: %s", c.readErrorReason())
|
||||||
}
|
}
|
||||||
} else {
|
}else{
|
||||||
if _, err = c.c.Write([]byte("RFB 003.008\n")); err != nil {
|
if _, err = c.c.Write([]byte("RFB 003.008\n")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (*FramebufferUpdateMessage) Read(c *ClientConn, r io.Reader) (ServerMessage
|
|||||||
var encodingType int32
|
var encodingType int32
|
||||||
|
|
||||||
rect := &rects[i]
|
rect := &rects[i]
|
||||||
data := []any{
|
data := []interface{}{
|
||||||
&rect.X,
|
&rect.X,
|
||||||
&rect.Y,
|
&rect.Y,
|
||||||
&rect.Width,
|
&rect.Width,
|
||||||
@@ -128,7 +128,7 @@ func (*SetColorMapEntriesMessage) Read(c *ClientConn, r io.Reader) (ServerMessag
|
|||||||
for i := uint16(0); i < numColors; i++ {
|
for i := uint16(0); i < numColors; i++ {
|
||||||
|
|
||||||
color := &result.Colors[i]
|
color := &result.Colors[i]
|
||||||
data := []any{
|
data := []interface{}{
|
||||||
&color.R,
|
&color.R,
|
||||||
&color.G,
|
&color.G,
|
||||||
&color.B,
|
&color.B,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
coreConfig = config.Core
|
coreConfig = config.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (any, func(), error) {
|
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}), coreConfig)
|
}), coreConfig)
|
||||||
|
|
||||||
@@ -39,11 +39,11 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
||||||
copyFn := func(src any) any { return src }
|
copyFn := func(src interface{}) interface{} { return src }
|
||||||
|
|
||||||
if copyChunk {
|
if copyChunk {
|
||||||
buffer := wave.NewBuffer()
|
buffer := wave.NewBuffer()
|
||||||
copyFn = func(src any) any {
|
copyFn = func(src interface{}) interface{} {
|
||||||
realSrc, _ := src.(wave.Audio)
|
realSrc, _ := src.(wave.Audio)
|
||||||
buffer.StoreCopy(realSrc)
|
buffer.StoreCopy(realSrc)
|
||||||
return buffer.Load()
|
return buffer.Load()
|
||||||
@@ -60,7 +60,7 @@ func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader {
|
|||||||
|
|
||||||
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (any, func(), error) {
|
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const (
|
|||||||
var errEmptySource = fmt.Errorf("Source can't be nil")
|
var errEmptySource = fmt.Errorf("Source can't be nil")
|
||||||
|
|
||||||
type broadcasterData struct {
|
type broadcasterData struct {
|
||||||
data any
|
data interface{}
|
||||||
count uint32
|
count uint32
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -124,10 +124,10 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
// copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring
|
||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyFn func(any) any) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{}) Reader {
|
||||||
currentCount := broadcaster.buffer.lastCount()
|
currentCount := broadcaster.buffer.lastCount()
|
||||||
|
|
||||||
return ReaderFunc(func() (data any, release func(), err error) {
|
return ReaderFunc(func() (data interface{}, release func(), err error) {
|
||||||
currentCount++
|
currentCount++
|
||||||
if push := broadcaster.buffer.acquire(currentCount); push != nil {
|
if push := broadcaster.buffer.acquire(currentCount); push != nil {
|
||||||
data, _, err = broadcaster.source.Load().(Reader).Read()
|
data, _, err = broadcaster.source.Load().(Reader).Read()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func TestBroadcast(t *testing.T) {
|
|||||||
frameCount := 0
|
frameCount := 0
|
||||||
frameSent := 0
|
frameSent := 0
|
||||||
lastSend := time.Now()
|
lastSend := time.Now()
|
||||||
src = ReaderFunc(func() (any, func(), error) {
|
src = ReaderFunc(func() (interface{}, func(), error) {
|
||||||
if pauseCond.src && frameSent == 30 {
|
if pauseCond.src && frameSent == 30 {
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func TestBroadcast(t *testing.T) {
|
|||||||
wg.Add(n)
|
wg.Add(n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
reader := broadcaster.NewReader(func(src any) any { return src })
|
reader := broadcaster.NewReader(func(src interface{}) interface{} { return src })
|
||||||
count := 0
|
count := 0
|
||||||
lastFrameCount := -1
|
lastFrameCount := -1
|
||||||
droppedFrames := 0
|
droppedFrames := 0
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ type Reader interface {
|
|||||||
// there will be new allocations during streaming, and old unused memory will become garbage. As a consequence,
|
// there will be new allocations during streaming, and old unused memory will become garbage. As a consequence,
|
||||||
// these garbage will put a lot of pressure to the garbage collector and makes it to run more often and finish
|
// these garbage will put a lot of pressure to the garbage collector and makes it to run more often and finish
|
||||||
// slower as the heap memory usage increases and more garbage to collect.
|
// slower as the heap memory usage increases and more garbage to collect.
|
||||||
Read() (data any, release func(), err error)
|
Read() (data interface{}, release func(), err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReaderFunc is a proxy type for Reader
|
// ReaderFunc is a proxy type for Reader
|
||||||
type ReaderFunc func() (data any, release func(), err error)
|
type ReaderFunc func() (data interface{}, release func(), err error)
|
||||||
|
|
||||||
func (f ReaderFunc) Read() (data any, release func(), err error) {
|
func (f ReaderFunc) Read() (data interface{}, release func(), err error) {
|
||||||
data, release, err = f()
|
data, release, err = f()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
coreConfig = config.Core
|
coreConfig = config.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (any, func(), error) {
|
broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}), coreConfig)
|
}), coreConfig)
|
||||||
|
|
||||||
@@ -39,11 +39,11 @@ func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster {
|
|||||||
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
// buffer, this means that slow readers might miss some data if they're really late and the data is no longer
|
||||||
// in the ring buffer.
|
// in the ring buffer.
|
||||||
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
||||||
copyFn := func(src any) any { return src }
|
copyFn := func(src interface{}) interface{} { return src }
|
||||||
|
|
||||||
if copyFrame {
|
if copyFrame {
|
||||||
buffer := NewFrameBuffer(0)
|
buffer := NewFrameBuffer(0)
|
||||||
copyFn = func(src any) any {
|
copyFn = func(src interface{}) interface{} {
|
||||||
realSrc, _ := src.(image.Image)
|
realSrc, _ := src.(image.Image)
|
||||||
buffer.StoreCopy(realSrc)
|
buffer.StoreCopy(realSrc)
|
||||||
return buffer.Load()
|
return buffer.Load()
|
||||||
@@ -60,7 +60,7 @@ func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader {
|
|||||||
|
|
||||||
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
// ReplaceSource replaces the underlying source. This operation is thread safe.
|
||||||
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
func (broadcaster *Broadcaster) ReplaceSource(source Reader) error {
|
||||||
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (any, func(), error) {
|
return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) {
|
||||||
return source.Read()
|
return source.Read()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package prop
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -55,8 +54,10 @@ type DurationOneOf []time.Duration
|
|||||||
|
|
||||||
// Compare implements DurationConstraint.
|
// Compare implements DurationConstraint.
|
||||||
func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
|
func (d DurationOneOf) Compare(a time.Duration) (float64, bool) {
|
||||||
if slices.Contains(d, a) {
|
for _, ii := range d {
|
||||||
return 0.0, true
|
if ii == a {
|
||||||
|
return 0.0, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package prop
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,8 +53,10 @@ type FloatOneOf []float32
|
|||||||
|
|
||||||
// Compare implements FloatConstraint.
|
// Compare implements FloatConstraint.
|
||||||
func (f FloatOneOf) Compare(a float32) (float64, bool) {
|
func (f FloatOneOf) Compare(a float32) (float64, bool) {
|
||||||
if slices.Contains(f, a) {
|
for _, ff := range f {
|
||||||
return 0.0, true
|
if ff == a {
|
||||||
|
return 0.0, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package prop
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,8 +56,10 @@ type FrameFormatOneOf []frame.Format
|
|||||||
|
|
||||||
// Compare implements FrameFormatConstraint.
|
// Compare implements FrameFormatConstraint.
|
||||||
func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
|
func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) {
|
||||||
if slices.Contains(f, a) {
|
for _, ff := range f {
|
||||||
return 0.0, true
|
if ff == a {
|
||||||
|
return 0.0, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package prop
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,8 +53,10 @@ type IntOneOf []int
|
|||||||
|
|
||||||
// Compare implements IntConstraint.
|
// Compare implements IntConstraint.
|
||||||
func (i IntOneOf) Compare(a int) (float64, bool) {
|
func (i IntOneOf) Compare(a int) (float64, bool) {
|
||||||
if slices.Contains(i, a) {
|
for _, ii := range i {
|
||||||
return 0.0, true
|
if ii == a {
|
||||||
|
return 0.0, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (m *Media) String() string {
|
|||||||
return prettifyStruct(m)
|
return prettifyStruct(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prettifyStruct(i any) string {
|
func prettifyStruct(i interface{}) string {
|
||||||
var rows []string
|
var rows []string
|
||||||
var addRows func(int, reflect.Value)
|
var addRows func(int, reflect.Value)
|
||||||
addRows = func(level int, obj reflect.Value) {
|
addRows = func(level int, obj reflect.Value) {
|
||||||
@@ -67,7 +67,7 @@ type setterFn func(fieldA, fieldB reflect.Value)
|
|||||||
|
|
||||||
// merge merges all the field values from o to p, except zero values. It's guaranteed that setterFn will be called
|
// merge merges all the field values from o to p, except zero values. It's guaranteed that setterFn will be called
|
||||||
// when fieldA and fieldB are not struct.
|
// when fieldA and fieldB are not struct.
|
||||||
func (p *Media) merge(o any, set setterFn) {
|
func (p *Media) merge(o interface{}, set setterFn) {
|
||||||
rp := reflect.ValueOf(p).Elem()
|
rp := reflect.ValueOf(p).Elem()
|
||||||
ro := reflect.ValueOf(o)
|
ro := reflect.ValueOf(o)
|
||||||
|
|
||||||
@@ -86,8 +86,10 @@ func (p *Media) merge(o any, set setterFn) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this with fieldB.IsZero() when we move to go1.13
|
||||||
// If non-boolean or non-discrete values are zeroes we skip them
|
// If non-boolean or non-discrete values are zeroes we skip them
|
||||||
if fieldB.IsZero() && fieldB.Kind() != reflect.Bool {
|
if fieldB.Interface() == reflect.Zero(fieldB.Type()).Interface() &&
|
||||||
|
fieldB.Kind() != reflect.Bool {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,13 +159,13 @@ func (p *MediaConstraints) FitnessDistance(o Media) (float64, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type comparisons []struct {
|
type comparisons []struct {
|
||||||
desired, actual any
|
desired, actual interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *comparisons) add(desired, actual any) {
|
func (c *comparisons) add(desired, actual interface{}) {
|
||||||
if desired != nil {
|
if desired != nil {
|
||||||
*c = append(*c,
|
*c = append(*c,
|
||||||
struct{ desired, actual any }{
|
struct{ desired, actual interface{} }{
|
||||||
desired, actual,
|
desired, actual,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package prop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,8 +55,10 @@ type StringOneOf []string
|
|||||||
|
|
||||||
// Compare implements StringConstraint.
|
// Compare implements StringConstraint.
|
||||||
func (f StringOneOf) Compare(a string) (float64, bool) {
|
func (f StringOneOf) Compare(a string) (float64, bool) {
|
||||||
if slices.Contains(f, a) {
|
for _, ff := range f {
|
||||||
return 0.0, true
|
if ff == a {
|
||||||
|
return 0.0, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1.0, false
|
return 1.0, false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user