Compare commits

...

27 Commits

Author SHA1 Message Date
3DRX
d8741c02e0 correct error handling and codec config 2025-07-03 21:44:07 +08:00
3DRX
29b9eaf317 resolve cr issues: tidy pkg-codec-ffmpeg-ci.yaml 2025-07-03 21:06:49 +08:00
3DRX
104cc5a5ab resolve cr issues: tidy ci 2025-07-03 21:06:49 +08:00
3DRX
32bfdbb52a resolve cr issues: tidy code 2025-07-03 21:06:49 +08:00
Jingyang Kang
00e120c79f Add codec ffmpeg 2025-07-03 21:06:49 +08:00
renovate[bot]
c4fd28c7df fix(deps): update github.com/kbinani/screenshot digest to 089614a (#640)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:33:19 +09:00
renovate[bot]
c79e16706b fix(deps): update module github.com/pion/logging to v0.2.4 (#639)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:31:48 +09:00
renovate[bot]
89420ae84d fix(deps): update module github.com/pion/rtp to v1.8.19 (#632)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:31:16 +09:00
renovate[bot]
4db71e5b52 fix(deps): update module github.com/pion/webrtc/v4 to v4.1.2 (#637)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 10:38:39 +08:00
Jackie Li
a45a5e50cd fix track.unbind panic (#634)
Fix #633

Here the signalCh could have been closed by another goroutine, we should
use returned signalCh from `track.removeActivePeerConnection()` to close
the channel.

Actually, I don't know why we need to close the signalCh, we're using it
to send over the doneCh, why ever close it?
2025-06-13 13:31:00 +08:00
renovate[bot]
d90220699e fix(deps): update module github.com/pion/webrtc/v4 to v4.1.1 (#629)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 15:20:59 +08:00
renovate[bot]
71deb52047 fix(deps): update module github.com/pion/interceptor to v0.1.39 (#628)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 15:12:30 +08:00
Jingyang Kang
84ccb15157 Align H265 payloadType in NewRTPH265Codec with RegisterDefaultCodecs (#631) 2025-06-01 23:37:30 +08:00
renovate[bot]
ec6a4b6925 fix(deps): update module github.com/pion/webrtc/v4 to v4.1.0 (#627)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 16:08:17 +08:00
renovate[bot]
551fb6afd8 fix(deps): update module github.com/pion/webrtc/v4 to v4.0.15 (#625)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 23:29:49 +08:00
Kyle
20e8c50735 Implement bitrate controller in vpx and h264 codecs (#467)
Add bitrate control to vpx and h264 encoders

Co-authored-by: Jingyang Kang <3drxkjy@gmail.com>
2025-04-11 12:05:01 +08:00
Jingyang Kang
7211d077ee Update social links (#620) 2025-04-07 01:07:14 +08:00
代码人生
cd5f8eb43a set bitrate for openh264 (#566)
* Added the set bitrate function for openh264

* add examples

* Format

---------

Co-authored-by: Jingyang Kang <3drxkjy@gmail.com>
2025-04-06 20:47:23 +08:00
renovate[bot]
2d7bdd4e24 fix(deps): update module github.com/pion/webrtc/v4 to v4.0.14 (#616)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 11:49:54 +08:00
Jingyang Kang
5cad3f1b41 Feat/add helper function for av1 and h265 codec (#611)
* bump deps version

* Add NewRTPAV1Codec

* fix comment

* Update SDPFmtpLine for RTPAV1Codec

* Add helper function for h265 as well
2025-03-10 07:25:25 -07:00
Jingyang Kang
9d5e9cb3ea Add EncoderController() codec.EncoderController in Track interface. (#614)
* Add EncoderController() codec.EncoderController in Track interface.

* Update test, add EncoderController in mockMediaStreamTrack.
2025-03-10 07:25:14 -07:00
Jingyang Kang
9a47a07eba Add EncoderController method to VideoTrack and AudioTrack. (#612)
* Add GetEncoderController method for VideoTrack and AudioTrack.

* Change the naming, remove unecessary private getter func.
2025-03-09 22:35:55 -07:00
renovate[bot]
4c70a5f686 fix(deps): update module github.com/pion/webrtc/v4 to v4.0.13 (#608)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 10:00:02 +09:00
renovate[bot]
36a03e823e fix(deps): update github.com/kbinani/screenshot digest to a3924b7 (#601)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 13:46:01 +09:00
renovate[bot]
24e3a722cf fix(deps): update module github.com/pion/webrtc/v4 to v4.0.9 (#598)
Generated by Renovate Bot

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 13:44:26 +09:00
Yohan Totting
ce9b412d0e Fix the VPX encoder is not properly handling the frame timestamp (#606)
* Fix the VPX encoder is not properly handling the frame timestamp

* Apply suggestions from code review

Co-authored-by: Atsushi Watanabe <atsushi.w@ieee.org>

---------

Co-authored-by: Atsushi Watanabe <atsushi.w@ieee.org>
2025-02-14 12:37:53 +07:00
Atsushi Watanabe
8f916c5c67 Fix dependency installation on the latest actions runner (#607)
Explicitly install libx11-dev and libxext-dev
2025-02-13 11:26:11 +09:00
29 changed files with 1848 additions and 141 deletions

View File

@@ -32,7 +32,9 @@ jobs:
libopus-dev \
libva-dev \
libvpx-dev \
libx264-dev
libx11-dev \
libx264-dev \
libxext-dev
- name: Run Test Suite
run: make test
- uses: codecov/codecov-action@v5

View 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 ./...

View File

@@ -5,7 +5,7 @@
</h1>
<h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4>
<p align="center">
<a href="https://pion.ly/slack"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a> <a href="https://twitter.com/_pion?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40_pion&style=social&url=https%3A%2F%2Ftwitter.com%2F_pion" alt="Twitter Widget"></a>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/mediadevices/test.yaml">
<a href="https://pkg.go.dev/github.com/pion/mediadevices"><img src="https://pkg.go.dev/badge/github.com/pion/mediadevices.svg" alt="Go Reference"></a>
<a href="https://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a>
@@ -164,6 +164,24 @@ An open source API that allows applications such as VLC media player or GStreame
* Installation:
* 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
@@ -218,9 +236,9 @@ There are 2 common problems:
The library can be used with our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
### Community
Pion has an active community on the [Slack](https://pion.ly/slack).
Pion has an active community on [Discord](https://discord.gg/PngbdqpFbt).
Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news.
Follow the [Pion Twitter](https://twitter.com/_pion) and the [Pion Bluesky](https://bsky.app/profile/pion.ly) for project updates and important WebRTC news.
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)

View File

@@ -5,33 +5,33 @@ go 1.21
require (
github.com/esimov/pigo v1.4.6
github.com/pion/mediadevices v0.0.0
github.com/pion/webrtc/v4 v4.0.5
github.com/pion/webrtc/v4 v4.1.2
)
require (
github.com/blackjack/webcam v0.6.1 // indirect
github.com/gen2brain/malgo v0.11.22 // indirect
github.com/gen2brain/malgo v0.11.23 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.3 // indirect
github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/interceptor v0.1.40 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.14 // indirect
github.com/pion/rtp v1.8.9 // indirect
github.com/pion/sctp v1.8.34 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/rtp v1.8.18 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)
replace github.com/pion/mediadevices v0.0.0 => ../

View File

@@ -1,78 +1,67 @@
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/esimov/pigo v1.4.6 h1:wpB9FstbqeGP/CZP+nTR52tUJe7XErq8buG+k4xCXlw=
github.com/esimov/pigo v1.4.6/go.mod h1:uqj9Y3+3IRYhFK071rxz1QYq0ePhA6+R9jrUZavi46M=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gen2brain/malgo v0.11.22 h1:fRtTbzVI9CDWnfEJGo/GxKxN7pXtCb0NsAeUVUjZk9U=
github.com/gen2brain/malgo v0.11.22/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
github.com/gen2brain/malgo v0.11.23/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice/v4 v4.0.3 h1:9s5rI1WKzF5DRqhJ+Id8bls/8PzM7mau0mj1WZb4IXE=
github.com/pion/ice/v4 v4.0.3/go.mod h1:VfHy0beAZ5loDT7BmJ2LtMtC4dbawIkkkejHPRZNB3Y=
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.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.34 h1:rCuD3m53i0oGxCSp7FLQKvqVx0Nf5AUAHhMRXTTQjBc=
github.com/pion/sctp v1.8.34/go.mod h1:yWkCClkXlzVW7BXfI2PjrUGBwUI0CjXJBkhLt+sdo4U=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
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/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.5 h1:8cVPojcv3cQTwVga2vF1rzCNvkiEimnYdCCG7yF317I=
github.com/pion/webrtc/v4 v4.0.5/go.mod h1:LvP8Np5b/sM0uyJIcUPvJcCvhtjHxJwzh2H2PYzE6cQ=
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

1
examples/openh264/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
openh264

View File

@@ -0,0 +1,22 @@
## Instructions
### Install required codecs
In this example, we'll be using openh264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system.
Installation steps:
* [openh264](https://github.com/pion/mediadevices#openh264)
### Download archive examplee
```
git clone https://github.com/pion/mediadevices.git
```
### Run openh264 example
Run `cd mediadevices/examples/openh264 && go build && ./openh264 recorded.h264`
set bitrate ,first press `Ctrl+c` or send a SIGINT signal.
To stop recording,second press `Ctrl+c` or send a SIGINT signal.

96
examples/openh264/main.go Normal file
View File

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

30
go.mod
View File

@@ -6,12 +6,12 @@ require (
github.com/blackjack/webcam v0.6.1
github.com/gen2brain/malgo v0.11.23
github.com/google/uuid v1.6.0
github.com/kbinani/screenshot v0.0.0-20240820160931-a8a2c5d0e191
github.com/pion/interceptor v0.1.37
github.com/pion/logging v0.2.2
github.com/pion/rtcp v1.2.14
github.com/pion/rtp v1.8.9
github.com/pion/webrtc/v4 v4.0.5
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/pion/interceptor v0.1.40
github.com/pion/logging v0.2.4
github.com/pion/rtcp v1.2.15
github.com/pion/rtp v1.8.19
github.com/pion/webrtc/v4 v4.1.2
github.com/stretchr/testify v1.10.0
golang.org/x/image v0.23.0
)
@@ -22,22 +22,22 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.3 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.34 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

70
go.sum
View File

@@ -1,6 +1,5 @@
github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE=
github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gen2brain/malgo v0.11.23 h1:3/VAI8DP9/Wyx1CUDNlUQJVdWUvGErhjHDqYcHVk9ME=
@@ -13,8 +12,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/kbinani/screenshot v0.0.0-20240820160931-a8a2c5d0e191 h1:5UHVWNX1qrIbNw7OpKbxe5bHkhHRk3xRKztMjERuCsU=
github.com/kbinani/screenshot v0.0.0-20240820160931-a8a2c5d0e191/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -23,64 +22,55 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/ice/v4 v4.0.3 h1:9s5rI1WKzF5DRqhJ+Id8bls/8PzM7mau0mj1WZb4IXE=
github.com/pion/ice/v4 v4.0.3/go.mod h1:VfHy0beAZ5loDT7BmJ2LtMtC4dbawIkkkejHPRZNB3Y=
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.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
github.com/pion/sctp v1.8.34 h1:rCuD3m53i0oGxCSp7FLQKvqVx0Nf5AUAHhMRXTTQjBc=
github.com/pion/sctp v1.8.34/go.mod h1:yWkCClkXlzVW7BXfI2PjrUGBwUI0CjXJBkhLt+sdo4U=
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
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/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c=
github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pion/webrtc/v4 v4.0.5 h1:8cVPojcv3cQTwVga2vF1rzCNvkiEimnYdCCG7yF317I=
github.com/pion/webrtc/v4 v4.0.5/go.mod h1:LvP8Np5b/sM0uyJIcUPvJcCvhtjHxJwzh2H2PYzE6cQ=
github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54=
github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -4,6 +4,7 @@ import (
"io"
"testing"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/webrtc/v4"
)
@@ -61,6 +62,10 @@ func (track *mockMediaStreamTrack) NewEncodedIOReader(codecName string) (io.Read
return nil, nil
}
func (track *mockMediaStreamTrack) EncoderController() codec.EncoderController {
return nil
}
func TestMediaStreamFilters(t *testing.T) {
audioTracks := []Track{
&mockMediaStreamTrack{AudioInput},

View File

@@ -37,6 +37,23 @@ func NewRTPH264Codec(clockrate uint32) *RTPCodec {
}
}
// NewRTPH265Codec is a helper to create an H265 codec
func NewRTPH265Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "",
RTCPFeedback: nil,
},
PayloadType: 116,
},
Payloader: &codecs.H265Payloader{},
}
}
// NewRTPVP8Codec is a helper to create an VP8 codec
func NewRTPVP8Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{
@@ -71,6 +88,23 @@ func NewRTPVP9Codec(clockrate uint32) *RTPCodec {
}
}
// NewRTPAV1Codec is a helper to create an AV1 codec
func NewRTPAV1Codec(clockrate uint32) *RTPCodec {
return &RTPCodec{
RTPCodecParameters: webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "level-idx=5;profile=0;tier=0",
RTCPFeedback: nil,
},
PayloadType: 99,
},
Payloader: &codecs.AV1Payloader{},
}
}
// NewRTPOpusCodec is a helper to create an Opus codec
func NewRTPOpusCodec(clockrate uint32) *RTPCodec {
return &RTPCodec{

1
pkg/codec/ffmpeg/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tmp

41
pkg/codec/ffmpeg/Makefile Normal file
View 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

View 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
View 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
}

View 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
View 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
View 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
View 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
}

View File

@@ -69,6 +69,16 @@ void enc_free(Encoder *e, int *eresult) {
free(e);
}
void enc_set_bitrate(Encoder *e, int bitrate) {
SEncParamExt encParamExt;
e->engine->GetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
encParamExt.iTargetBitrate=bitrate;
encParamExt.iMaxBitrate=bitrate;
encParamExt.sSpatialLayers[0].iSpatialBitrate = bitrate;
encParamExt.sSpatialLayers[0].iMaxSpatialBitrate = bitrate;
e->engine->SetOption(ENCODER_OPTION_SVC_ENCODE_PARAM_EXT, &encParamExt);
}
// There's a good reference from ffmpeg in using the encode_frame
// Reference: https://ffmpeg.org/doxygen/2.6/libopenh264enc_8c_source.html
Slice enc_encode(Encoder *e, Frame f, int *eresult) {

View File

@@ -44,6 +44,7 @@ typedef struct Encoder {
Encoder *enc_new(const EncoderOptions params, int *eresult);
void enc_free(Encoder *e, int *eresult);
Slice enc_encode(Encoder *e, Frame f, int *eresult);
void enc_set_bitrate(Encoder *e, int bitrate);
#ifdef __cplusplus
}
#endif

View File

@@ -96,6 +96,11 @@ func (e *encoder) ForceKeyFrame() error {
return nil
}
func (e *encoder) SetBitRate(bitrate int) error {
C.enc_set_bitrate(e.engine, C.int(bitrate))
return nil
}
func (e *encoder) Controller() codec.EncoderController {
return e
}

View File

@@ -69,11 +69,12 @@ type encoder struct {
cfg *C.vpx_codec_enc_cfg_t
r video.Reader
frameIndex int
tStart int
tLastFrame int
tStart time.Time
tLastFrame time.Time
frame []byte
deadline int
requireKeyFrame bool
targetBitrate int
isKeyFrame bool
mu sync.Mutex
@@ -198,7 +199,7 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
); ec != 0 {
return nil, fmt.Errorf("vpx_codec_enc_init failed (%d)", ec)
}
t0 := time.Now().Nanosecond() / 1000000
t0 := time.Now()
return &encoder{
r: video.ToI420(r),
codec: codec,
@@ -208,6 +209,7 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c
tLastFrame: t0,
deadline: int(params.Deadline / time.Microsecond),
frame: make([]byte, 1024),
targetBitrate: params.BitRate,
}, nil
}
@@ -233,7 +235,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
e.raw.stride[1] = C.int(yuvImg.CStride)
e.raw.stride[2] = C.int(yuvImg.CStride)
t := time.Now().Nanosecond() / 1000000
t := time.Now()
if e.cfg.g_w != C.uint(width) || e.cfg.g_h != C.uint(height) {
e.cfg.g_w, e.cfg.g_h = C.uint(width), C.uint(height)
@@ -252,7 +254,7 @@ func (e *encoder) Read() ([]byte, func(), error) {
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
}
duration := t - e.tLastFrame
duration := t.Sub(e.tLastFrame).Microseconds()
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
// and consequently the codec will read the first frame from the buffer. This means the first frame won't
@@ -261,13 +263,23 @@ func (e *encoder) Read() ([]byte, func(), error) {
if duration == 0 {
duration = 1
}
targetVpxBitrate := C.uint(float32(e.targetBitrate / 1000)) // convert to kilobits / second
if e.cfg.rc_target_bitrate != targetVpxBitrate && targetVpxBitrate >= 1 {
e.cfg.rc_target_bitrate = targetVpxBitrate
rc := C.vpx_codec_enc_config_set(e.codec, e.cfg)
if rc != C.VPX_CODEC_OK {
return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", rc)
}
}
var flags int
if e.requireKeyFrame {
flags = flags | C.VPX_EFLAG_FORCE_KF
}
if ec := C.encode_wrapper(
e.codec, e.raw,
C.long(t-e.tStart), C.ulong(duration), C.long(flags), C.ulong(e.deadline),
C.long(t.Sub(e.tStart).Microseconds()), C.ulong(duration), C.long(flags), C.ulong(e.deadline),
(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]),
); ec != C.VPX_CODEC_OK {
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
@@ -303,6 +315,13 @@ func (e *encoder) ForceKeyFrame() error {
return nil
}
func (e *encoder) SetBitRate(bitrate int) error {
e.mu.Lock()
defer e.mu.Unlock()
e.targetBitrate = bitrate
return nil
}
func (e *encoder) Controller() codec.EncoderController {
return e
}

View File

@@ -1,10 +1,12 @@
package vpx
import (
"context"
"image"
"io"
"sync/atomic"
"testing"
"time"
"github.com/pion/mediadevices/pkg/codec"
"github.com/pion/mediadevices/pkg/codec/internal/codectest"
@@ -230,9 +232,76 @@ func TestRequestKeyFrame(t *testing.T) {
}
}
func TestShouldImplementBitRateControl(t *testing.T) {
t.SkipNow() // TODO: Implement bit rate control
func TestSetBitrate(t *testing.T) {
for name, factory := range map[string]func() (codec.VideoEncoderBuilder, error){
"VP8": func() (codec.VideoEncoderBuilder, error) {
p, err := NewVP8Params()
return &p, err
},
"VP9": func() (codec.VideoEncoderBuilder, error) {
p, err := NewVP9Params()
// Disable latency to ease test and begin to receive packets for each input frame
p.LagInFrames = 0
return &p, err
},
} {
factory := factory
t.Run(name, func(t *testing.T) {
param, err := factory()
if err != nil {
t.Fatal(err)
}
var initialWidth, initialHeight, width, height int = 320, 240, 320, 240
var cnt uint32
r, err := param.BuildVideoEncoder(
video.ReaderFunc(func() (image.Image, func(), error) {
i := atomic.AddUint32(&cnt, 1)
if i == 3 {
return nil, nil, io.EOF
}
return image.NewYCbCr(
image.Rect(0, 0, width, height),
image.YCbCrSubsampleRatio420,
), func() {}, nil
}),
prop.Media{
Video: prop.Video{
Width: initialWidth,
Height: initialHeight,
FrameRate: 1,
FrameFormat: frame.FormatI420,
},
},
)
if err != nil {
t.Fatal(err)
}
_, rel, err := r.Read()
if err != nil {
t.Fatal(err)
}
rel()
err = r.Controller().(codec.BitRateController).SetBitRate(1000) // 1000 bit/second is ridiculously low, but this is a testcase.
if err != nil {
t.Fatal(err)
}
_, rel, err = r.Read()
if err != nil {
t.Fatal(err)
}
rel()
_, _, err = r.Read()
if err != io.EOF {
t.Fatal(err)
}
})
}
}
func TestShouldImplementBitRateControl(t *testing.T) {
e := &encoder{}
if _, ok := e.Controller().(codec.BitRateController); !ok {
t.Error()
@@ -245,3 +314,49 @@ func TestShouldImplementKeyFrameControl(t *testing.T) {
t.Error()
}
}
func TestEncoderFrameMonotonic(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
params, err := NewVP8Params()
if err != nil {
t.Fatal(err)
}
encoder, err := params.BuildVideoEncoder(
video.ReaderFunc(func() (image.Image, func(), error) {
return image.NewYCbCr(
image.Rect(0, 0, 320, 240),
image.YCbCrSubsampleRatio420,
), func() {}, nil
},
), prop.Media{
Video: prop.Video{
Width: 320,
Height: 240,
FrameRate: 30,
FrameFormat: frame.FormatI420,
},
})
if err != nil {
t.Fatal(err)
}
ticker := time.NewTicker(33 * time.Millisecond)
defer ticker.Stop()
ctxx, cancell := context.WithCancel(ctx)
defer cancell()
for {
select {
case <-ctxx.Done():
return
case <-ticker.C:
_, rel, err := encoder.Read()
if err != nil {
t.Fatal(err)
}
rel()
}
}
}

View File

@@ -8,6 +8,7 @@
#define ERR_ALLOC_PICTURE -3
#define ERR_OPEN_ENGINE -4
#define ERR_ENCODE -5
#define ERR_BITRATE_RECONFIG -6
typedef struct Slice {
unsigned char *data;
@@ -78,6 +79,22 @@ fail:
return NULL;
}
#define RC_MARGIN 10000 /* 1kilobits / second*/
static int apply_target_bitrate(Encoder *e, int target_bitrate) {
int target_encoder_bitrate = (int)target_bitrate / 1000;
if (e->param.rc.i_bitrate == target_encoder_bitrate || target_encoder_bitrate <= 1) {
return 0; // if no change to bitrate or target bitrate is too small, we return no error (0)
}
e->param.rc.i_bitrate = target_encoder_bitrate;
e->param.rc.f_rate_tolerance = 0.1;
e->param.rc.i_vbv_max_bitrate = target_encoder_bitrate + RC_MARGIN / 2;
e->param.rc.i_vbv_buffer_size = e->param.rc.i_vbv_max_bitrate;
e->param.rc.f_vbv_buffer_init = 0.6;
int success = x264_encoder_reconfig(e->h, &e->param);
return success; // 0 on success or negative on error
}
Slice enc_encode(Encoder *e, uint8_t *y, uint8_t *cb, uint8_t *cr, int *rc) {
x264_nal_t *nal;
int i_nal;

View File

@@ -39,6 +39,8 @@ func (e cerror) Error() string {
return errOpenEngine.Error()
case C.ERR_ENCODE:
return errEncode.Error()
case C.ERR_BITRATE_RECONFIG:
return errSetBitrate.Error()
default:
return "unknown error"
}
@@ -58,6 +60,7 @@ var (
errAllocPicture = fmt.Errorf("failed to alloc picture")
errOpenEngine = fmt.Errorf("failed to open x264")
errEncode = fmt.Errorf("failed to encode")
errSetBitrate = fmt.Errorf("failed to change x264 encoder bitrate")
)
func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
@@ -133,6 +136,16 @@ func (e *encoder) ForceKeyFrame() error {
return nil
}
func (e *encoder) SetBitRate(bitrate int) error {
e.mu.Lock()
defer e.mu.Unlock()
errNum := C.apply_target_bitrate(e.engine, C.int(bitrate))
if err := errFromC(errNum); err != nil {
return err
}
return nil
}
func (e *encoder) Controller() codec.EncoderController {
return e
}

View File

@@ -7,9 +7,34 @@ import (
"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()
@@ -69,19 +94,53 @@ func TestEncoder(t *testing.T) {
}
func TestShouldImplementKeyFrameControl(t *testing.T) {
t.SkipNow() // TODO: Implement key frame control
e := &encoder{}
if _, ok := e.Controller().(codec.KeyFrameController); !ok {
t.Error()
}
}
func TestShouldImplementBitRateControl(t *testing.T) {
t.SkipNow() // TODO: Implement bit rate control
func TestNoErrorOnForceKeyFrame(t *testing.T) {
enc, err := getTestVideoEncoder()
if err != nil {
t.Error(err)
}
kfc, ok := enc.Controller().(codec.KeyFrameController)
if !ok {
t.Error()
}
if err := kfc.ForceKeyFrame(); err != nil {
t.Error(err)
}
_, rel, err := enc.Read() // try to read the encoded frame
rel()
if err != nil {
t.Fatal(err)
}
}
func TestShouldImplementBitRateControl(t *testing.T) {
e := &encoder{}
if _, ok := e.Controller().(codec.BitRateController); !ok {
t.Error()
}
}
func TestNoErrorOnSetBitRate(t *testing.T) {
enc, err := getTestVideoEncoder()
if err != nil {
t.Error(err)
}
brc, ok := enc.Controller().(codec.BitRateController)
if !ok {
t.Error()
}
if err := brc.SetBitRate(1000); err != nil { // 1000 bit/second is ridiculously low, but this is a testcase.
t.Error(err)
}
_, rel, err := enc.Read() // try to read the encoded frame
rel()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -77,6 +77,8 @@ type Track interface {
NewEncodedReader(codecName string) (EncodedReadCloser, error)
// NewEncodedReader creates a new Go standard io.ReadCloser that reads the encoded data in codecName format
NewEncodedIOReader(codecName string) (io.ReadCloser, error)
// EncoderController returns the encoder controller if the track has one, else returns nil
EncoderController() codec.EncoderController
}
type baseTrack struct {
@@ -89,6 +91,7 @@ type baseTrack struct {
kind MediaDeviceType
selector *CodecSelector
activePeerConnections map[string]chan<- chan<- struct{}
encoderController codec.EncoderController
}
func newBaseTrack(source Source, kind MediaDeviceType, selector *CodecSelector) *baseTrack {
@@ -200,8 +203,10 @@ func (track *baseTrack) bind(ctx webrtc.TrackLocalContext, specializedTrack Trac
encodedReader.Close()
// When there's another call to unbind, it won't block since we remove the current ctx from active connections
track.removeActivePeerConnection(ctx.ID())
signalCh := track.removeActivePeerConnection(ctx.ID())
if signalCh != nil {
close(signalCh)
}
if doneCh != nil {
close(doneCh)
}
@@ -230,7 +235,8 @@ func (track *baseTrack) bind(ctx webrtc.TrackLocalContext, specializedTrack Trac
}
}()
keyFrameController, ok := encodedReader.Controller().(codec.KeyFrameController)
track.encoderController = encodedReader.Controller()
keyFrameController, ok := track.encoderController.(codec.KeyFrameController)
if ok {
go track.rtcpReadLoop(ctx.RTCPReader(), keyFrameController, stopRead)
}
@@ -449,6 +455,11 @@ func (track *VideoTrack) NewRTPReader(codecName string, ssrc uint32, mtu int) (R
}, nil
}
// returned encoderController might be nil
func (track *VideoTrack) EncoderController() codec.EncoderController {
return track.encoderController
}
// AudioTrack is a specific track type that contains audio source which allows multiple readers to access, and
// manipulate.
type AudioTrack struct {
@@ -570,3 +581,7 @@ func (track *AudioTrack) NewRTPReader(codecName string, ssrc uint32, mtu int) (R
controllerFn: encodedReader.Controller,
}, nil
}
func (track *AudioTrack) EncoderController() codec.EncoderController {
return track.encoderController
}