mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-26 20:41:46 +08:00
Compare commits
5 Commits
21d2f4618c
...
add_codec_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d8741c02e0 | ||
![]() |
29b9eaf317 | ||
![]() |
104cc5a5ab | ||
![]() |
32bfdbb52a | ||
![]() |
00e120c79f |
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 ./...
|
18
README.md
18
README.md
@@ -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
|
||||
|
||||
|
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
|
||||
}
|
Reference in New Issue
Block a user