From 00e120c79ffa06ea83c0fca1b8e229defc9f60a9 Mon Sep 17 00:00:00 2001 From: Jingyang Kang <3drxkjy@gmail.com> Date: Mon, 7 Apr 2025 01:07:14 +0800 Subject: [PATCH] Add codec ffmpeg --- .github/workflows/ci.yaml | 147 ++++++++++- README.md | 18 ++ pkg/codec/ffmpeg/.gitignore | 1 + pkg/codec/ffmpeg/Makefile | 41 +++ pkg/codec/ffmpeg/errors.go | 18 ++ pkg/codec/ffmpeg/ffmpeg.go | 452 ++++++++++++++++++++++++++++++++ pkg/codec/ffmpeg/ffmpeg_test.go | 357 +++++++++++++++++++++++++ pkg/codec/ffmpeg/go.mod | 36 +++ pkg/codec/ffmpeg/go.sum | 56 ++++ pkg/codec/ffmpeg/params.go | 191 ++++++++++++++ 10 files changed, 1307 insertions(+), 10 deletions(-) create mode 100644 pkg/codec/ffmpeg/.gitignore create mode 100644 pkg/codec/ffmpeg/Makefile create mode 100644 pkg/codec/ffmpeg/errors.go create mode 100644 pkg/codec/ffmpeg/ffmpeg.go create mode 100644 pkg/codec/ffmpeg/ffmpeg_test.go create mode 100644 pkg/codec/ffmpeg/go.mod create mode 100644 pkg/codec/ffmpeg/go.sum create mode 100644 pkg/codec/ffmpeg/params.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 086d2d0..415eeb9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,11 +21,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - - name: Install dependencies + + - name: Install system dependencies run: | sudo apt-get update -qq \ && sudo apt-get install --no-install-recommends -y \ @@ -34,12 +36,72 @@ jobs: libvpx-dev \ libx11-dev \ libx264-dev \ - libxext-dev + libxext-dev \ + nasm \ + yasm + + # Cache FFmpeg build - include both source and install directories + - 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: | + echo "=== Starting FFmpeg build ===" + 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" + make build-ffmpeg + echo "=== FFmpeg build finished ===" + ls -la tmp/n7.0/ + ls -la tmp/n7.0/lib/ 2>/dev/null || echo "lib directory not found after build" + ls -la tmp/n7.0/include/ 2>/dev/null || echo "include directory not found after build" + + - 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 Test Suite - run: make test + run: | + make test \ + && cd pkg/codec/ffmpeg \ + && 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 @@ -52,34 +114,99 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - - name: Install dependencies + + - name: Install system dependencies run: | - which brew brew install \ pkg-config \ opus \ libvpx \ x264 + + # Cache FFmpeg build for macOS + - 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: | + echo "=== Starting FFmpeg build ===" + 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" + make build-ffmpeg + echo "=== FFmpeg build finished ===" + ls -la tmp/n7.0/ + ls -la tmp/n7.0/lib/ 2>/dev/null || echo "lib directory not found after build" + ls -la tmp/n7.0/include/ 2>/dev/null || echo "include directory not found after build" + + - 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 Test Suite - run: make test + run: | + make test \ + && cd pkg/codec/ffmpeg \ + && 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 ./... + + - name: Cache go-licenses binary + uses: actions/cache@v4 + with: + path: ~/go/bin/go-licenses + key: go-licenses-${{ runner.os }} + + - name: Install go-licenses + run: | + if [ ! -f ~/go/bin/go-licenses ]; then + go install github.com/google/go-licenses@latest + fi + + - name: Check licenses + run: ~/go/bin/go-licenses check ./... diff --git a/README.md b/README.md index 99f92ce..99848ea 100644 --- a/README.md +++ b/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 diff --git a/pkg/codec/ffmpeg/.gitignore b/pkg/codec/ffmpeg/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/pkg/codec/ffmpeg/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/pkg/codec/ffmpeg/Makefile b/pkg/codec/ffmpeg/Makefile new file mode 100644 index 0000000..0c47461 --- /dev/null +++ b/pkg/codec/ffmpeg/Makefile @@ -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 diff --git a/pkg/codec/ffmpeg/errors.go b/pkg/codec/ffmpeg/errors.go new file mode 100644 index 0000000..c3b1bf6 --- /dev/null +++ b/pkg/codec/ffmpeg/errors.go @@ -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") +) diff --git a/pkg/codec/ffmpeg/ffmpeg.go b/pkg/codec/ffmpeg/ffmpeg.go new file mode 100644 index 0000000..52893f0 --- /dev/null +++ b/pkg/codec/ffmpeg/ffmpeg.go @@ -0,0 +1,452 @@ +// 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 hardwareEncoder struct { + codecCtx *astiav.CodecContext + hwFramesCtx *astiav.HardwareFramesContext + frame *astiav.Frame + hwFrame *astiav.Frame + packet *astiav.Packet + width int + height int + r video.Reader + nextIsKeyFrame bool + + mu sync.Mutex + closed bool +} + +type softwareEncoder struct { + codec *astiav.Codec + codecCtx *astiav.CodecContext + frame *astiav.Frame + packet *astiav.Packet + width int + height int + r video.Reader + nextIsKeyFrame bool + + mu sync.Mutex + closed bool +} + +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.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("delay", "0", 0) + codecOptions.Set("tune", "ll", 0) + codecOptions.Set("preset", "p1", 0) + codecOptions.Set("rc", "cbr", 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) + + err = hwFramesCtx.Initialize() + if 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) + + err = softwareFrame.AllocBuffer(0) + if err != nil { + softwareFrame.Free() + codecCtx.Free() + hwFramesCtx.Free() + return nil, errFailedToAllocSwBuf + } + + hardwareFrame := astiav.AllocFrame() + + err = hardwareFrame.AllocHardwareBuffer(hwFramesCtx) + if 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{ + codecCtx: codecCtx, + hwFramesCtx: hwFramesCtx, + frame: softwareFrame, + hwFrame: hardwareFrame, + packet: packet, + width: p.Width, + height: p.Height, + r: r, + nextIsKeyFrame: false, + }, 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)) + } + + err = e.frame.Data().FromImage(img) + if err != nil { + return nil, func() {}, err + } + + err = e.frame.TransferHardwareData(e.hwFrame) + if err != nil { + return nil, func() {}, err + } + + // Send frame to encoder + if err := e.codecCtx.SendFrame(e.hwFrame); 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 +} + +// 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)) + 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)) + + err := softwareFrame.AllocBuffer(0) + if err != nil { + softwareFrame.Free() + codecCtx.Free() + return nil, errFailedToAllocSwBuf + } + + packet := astiav.AllocPacket() + if packet == nil { + softwareFrame.Free() + codecCtx.Free() + return nil, errFailedToAllocPacket + } + + return &softwareEncoder{ + 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)) + } + err = e.frame.Data().FromImage(img) + if 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 +} diff --git a/pkg/codec/ffmpeg/ffmpeg_test.go b/pkg/codec/ffmpeg/ffmpeg_test.go new file mode 100644 index 0000000..2a2f8bb --- /dev/null +++ b/pkg/codec/ffmpeg/ffmpeg_test.go @@ -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() + } + } +} diff --git a/pkg/codec/ffmpeg/go.mod b/pkg/codec/ffmpeg/go.mod new file mode 100644 index 0000000..92c5186 --- /dev/null +++ b/pkg/codec/ffmpeg/go.mod @@ -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 +) diff --git a/pkg/codec/ffmpeg/go.sum b/pkg/codec/ffmpeg/go.sum new file mode 100644 index 0000000..51a3e4c --- /dev/null +++ b/pkg/codec/ffmpeg/go.sum @@ -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= diff --git a/pkg/codec/ffmpeg/params.go b/pkg/codec/ffmpeg/params.go new file mode 100644 index 0000000..3a51253 --- /dev/null +++ b/pkg/codec/ffmpeg/params.go @@ -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 +}