Merge branch 'github-master'
Some checks failed
docker / Docker (push) Has been cancelled
test / Docker Test (push) Has been cancelled

This commit is contained in:
Failure 2025-12-06 17:14:52 -08:00
commit f1dd9fc2ed
167 changed files with 2002 additions and 513 deletions

74
.github/workflows/builder.yml vendored Normal file
View file

@ -0,0 +1,74 @@
name: Build and Push Builder Image
on:
pull_request:
paths:
- 'Dockerfile.builder'
- '.github/workflows/builder.yml'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}-builder
GOLANG_VERSION: '1.25.1'
FFMPEG_VERSION: '7.1.1'
VIPS_VERSION: '8.17.2'
jobs:
build-builder:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set build arguments
id: args
run: |
echo "ffmpeg_version=${{ env.FFMPEG_VERSION }}" >> $GITHUB_OUTPUT
echo "vips_version=${{ env.VIPS_VERSION }}" >> $GITHUB_OUTPUT
echo "golang_version=${{ env.GOLANG_VERSION }}" >> $GITHUB_OUTPUT
# Create version tag
VERSION_TAG="ffmpeg-${{ env.FFMPEG_VERSION }}-vips-${{ env.VIPS_VERSION }}-go-${{ env.GOLANG_VERSION }}"
echo "version_tag=${VERSION_TAG}" >> $GITHUB_OUTPUT
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=raw,value=${{ steps.args.outputs.version_tag }}
- name: Build and push builder image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.builder
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GOLANG_VERSION=${{ steps.args.outputs.golang_version }}
FFMPEG_VERSION=${{ steps.args.outputs.ffmpeg_version }}
VIPS_VERSION=${{ steps.args.outputs.vips_version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image digest
run: echo ${{ steps.build.outputs.digest }}

View file

@ -1,13 +1,9 @@
name: docker name: docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on: on:
push: push:
branches: [ master ] branches:
- master
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]
jobs: jobs:
@ -17,32 +13,21 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Setup qemu for Docker buildx
- name: Set up QEMU - name: Set up QEMU
id: qemu uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
with: with:
image: tonistiigi/binfmt:latest
platforms: arm64 platforms: arm64
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
with:
version: latest
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -50,44 +35,28 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.repository == 'cshum/imagorvideo' if: github.repository == 'cshum/imagorvideo'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: | images: |
name=${{ format('ghcr.io/{0}', github.repository) }},enable=true name=ghcr.io/${{ github.repository }},enable=true
name=shumc/imagorvideo,enable=${{ github.repository == 'cshum/imagorvideo' }} name=shumc/imagorvideo,enable=${{ github.repository == 'cshum/imagorvideo' }}
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# Build and push Docker image with Buildx (don't push on PR) - name: Build and push Docker image
# https://github.com/docker/build-push-action uses: docker/build-push-action@v5
- name: Build and push Docker image tag
if: startsWith(github.ref, 'refs/tags/v')
id: build-and-push-tag
uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image branch
if: startsWith(github.ref, 'refs/heads')
id: build-and-push-branch
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View file

@ -1,126 +1,57 @@
name: test name: test
on: [push, pull_request] on:
push:
branches:
- master
pull_request:
branches:
- master
jobs: jobs:
build: test:
name: Test name: Docker Test
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
env:
CGO_CFLAGS_ALLOW: -Xpreprocessor
VIPS_VERSION: 8.15.2
FFMPEG_VERSION: 5.1.2
V: 4
steps: steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.22
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Install linux dependencies - name: Set up Docker Buildx
run: | uses: docker/setup-buildx-action@v3
sudo apt-get update
sudo apt-get install -y \
meson ninja-build \
libglib2.0-dev libexpat-dev librsvg2-dev libpng-dev \
libjpeg-turbo8-dev libimagequant-dev libfftw3-dev \
libpoppler-glib-dev libxml2-dev \
libopenslide-dev libcfitsio-dev liborc-0.4-dev libpango1.0-dev \
libtiff5-dev libgsf-1-dev giflib-tools libwebp-dev libheif-dev \
yasm libx264-dev libx265-dev libnuma-dev libvpx-dev libtheora-dev \
librtmp-dev libvorbis-dev \
libopenjp2-7-dev libcgif-dev
- name: Cache libvips - name: Build test image
uses: actions/cache@v3 uses: docker/build-push-action@v5
with: with:
path: vips-${{ env.VIPS_VERSION }} context: .
key: ${{ runner.os }}-vips-${{ env.V }}-${{ env.VIPS_VERSION }} target: builder
restore-keys: | tags: imagorvideo-test
${{ runner.os }}-vips-${{ env.V }}- load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build libvips from source - name: Run tests in Docker
run: | run: |
if [ ! -d "vips-${{ env.VIPS_VERSION }}" ] docker run --rm \
then -v ${{ github.workspace }}:/workspace \
wget https://github.com/libvips/libvips/releases/download/v${{ env.VIPS_VERSION }}/vips-${{ env.VIPS_VERSION }}.tar.xz -w /workspace \
tar xf vips-${{ env.VIPS_VERSION }}.tar.xz imagorvideo-test \
fi go test ./... -v
cd vips-${{ env.VIPS_VERSION }}
meson setup _build \
--buildtype=release \
--strip \
--prefix=/usr/local \
--libdir=lib \
-Dgtk_doc=false \
-Dmagick=disabled \
-Dintrospection=disabled
ninja -C _build
sudo ninja -C _build install
sudo ldconfig
- name: Cache ffmpeg
uses: actions/cache@v3
with:
path: ffmpeg-${{ env.FFMPEG_VERSION }}
key: ${{ runner.os }}-ffmpeg-${{ env.V }}-${{ env.FFMPEG_VERSION }}
restore-keys: |
${{ runner.os }}-ffmpeg-${{ env.V }}-
- name: Build ffmpeg from source
run: |
if [ ! -d "ffmpeg-${{ env.FFMPEG_VERSION }}" ]
then
wget https://ffmpeg.org/releases/ffmpeg-${{ env.FFMPEG_VERSION }}.tar.bz2
tar jvxf ffmpeg-${{ env.FFMPEG_VERSION }}.tar.bz2
fi
cd ffmpeg-${{ env.FFMPEG_VERSION }}
./configure --prefix=/usr/local \
--disable-debug \
--disable-doc \
--disable-ffplay \
--disable-static \
--enable-shared \
--enable-version3 \
--enable-gpl \
--enable-libtheora \
--enable-libvorbis \
--enable-librtmp \
--enable-libwebp \
--enable-libvpx \
--enable-libx265 \
--enable-libx264
make && sudo make install
sudo ldconfig
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Get dependencies
run: make get
- name: Test
run: make test
- name: Commit golden files - name: Commit golden files
if: github.event_name != 'pull_request' if: github.event_name == 'pull_request'
uses: stefanzweifel/git-auto-commit-action@v4 uses: stefanzweifel/git-auto-commit-action@v5
with: with:
commit_message: "test: update golden files" commit_message: "test: update golden files"
file_pattern: "testdata/golden" file_pattern: "testdata/golden"
- name: Run coverage in Docker
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
imagorvideo-test \
sh -c "rm -rf examples cmd && go test -coverprofile=profile.cov ./..."
- name: Coveralls - name: Coveralls
uses: shogo82148/actions-goveralls@v1 uses: shogo82148/actions-goveralls@v1
with: with:

View file

@ -1,66 +1,9 @@
ARG GOLANG_VERSION=1.22.5 ARG BUILDER_IMAGE_TAG=ffmpeg-7.1.1-vips-8.17.2-go-1.25.1
FROM golang:${GOLANG_VERSION}-bookworm as builder
ARG FFMPEG_VERSION=5.1.2 # Stage 1: Build application using builder image with go + libvips + FFmpeg
ARG VIPS_VERSION=8.15.2 FROM ghcr.io/cshum/imagorvideo-builder:${BUILDER_IMAGE_TAG} AS builder
ARG TARGETARCH
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
ENV MAKEFLAGS="-j8"
# Installs libvips + required libraries
RUN DEBIAN_FRONTEND=noninteractive \
apt-get update && \
apt-get install --no-install-recommends -y \
ca-certificates \
automake build-essential curl \
meson ninja-build pkg-config \
gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg62-turbo-dev libpng-dev \
libwebp-dev libtiff-dev libexif-dev libxml2-dev libpoppler-glib-dev \
swig libpango1.0-dev libmatio-dev libopenslide-dev libcfitsio-dev libopenjp2-7-dev \
libgsf-1-dev libfftw3-dev liborc-0.4-dev librsvg2-dev libimagequant-dev libaom-dev libheif-dev \
yasm libx264-dev libx265-dev libnuma-dev libvpx-dev libtheora-dev \
libspng-dev libcgif-dev librtmp-dev libvorbis-dev && \
cd /tmp && \
curl -fsSLO https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz && \
tar xf vips-${VIPS_VERSION}.tar.xz && \
cd vips-${VIPS_VERSION} && \
meson setup _build \
--buildtype=release \
--strip \
--prefix=/usr/local \
--libdir=lib \
-Dgtk_doc=false \
-Dmagick=disabled \
-Dintrospection=disabled && \
ninja -C _build && \
ninja -C _build install && \
cd /tmp && \
curl -fsSLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar jvxf ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
cd /tmp/ffmpeg-${FFMPEG_VERSION} && \
./configure --prefix=/usr/local \
--disable-debug \
--disable-doc \
--disable-ffplay \
--disable-static \
--enable-shared \
--enable-version3 \
--enable-gpl \
--enable-libtheora \
--enable-libvorbis \
--enable-librtmp \
--enable-libwebp \
--enable-libvpx \
--enable-libx265 \
--enable-libx264 && \
make && make install && \
ldconfig && \
rm -rf /usr/local/lib/python* && \
rm -rf /usr/local/lib/libvips-cpp.* && \
rm -rf /usr/local/lib/*.a && \
rm -rf /usr/local/lib/*.la
WORKDIR ${GOPATH}/src/github.com/cshum/imagorvideo WORKDIR ${GOPATH}/src/github.com/cshum/imagorvideo
@ -73,22 +16,23 @@ COPY . .
RUN go build -o ${GOPATH}/bin/imagorvideo ./cmd/imagorvideo/main.go RUN go build -o ${GOPATH}/bin/imagorvideo ./cmd/imagorvideo/main.go
FROM debian:bookworm-slim # Stage 2: Runtime image
FROM debian:trixie-slim as runtime
LABEL maintainer="adrian@cshum.com" LABEL maintainer="adrian@cshum.com"
COPY --from=builder /usr/local/lib /usr/local/lib COPY --from=builder /usr/local/lib /usr/local/lib
COPY --from=builder /etc/ssl/certs /etc/ssl/certs COPY --from=builder /etc/ssl/certs /etc/ssl/certs
# Install runtime dependencies
RUN DEBIAN_FRONTEND=noninteractive \ RUN DEBIAN_FRONTEND=noninteractive \
apt-get update && \ apt-get update && \
apt-get install --no-install-recommends -y \ apt-get install --no-install-recommends -y \
procps libglib2.0-0 libjpeg62-turbo libpng16-16 libopenexr-3-1-30 \ procps curl libglib2.0-0 libjpeg62-turbo libpng16-16 libopenexr-3-1-30 \
libwebp7 libwebpmux3 libwebpdemux2 libtiff6 libexif12 libxml2 libpoppler-glib8 \ libwebp7 libwebpmux3 libwebpdemux2 libtiff6 libexif12 libxml2 libpoppler-glib8t64 \
libpango1.0-0 libmatio11 libopenslide0 libopenjp2-7 libjemalloc2 \ libpango-1.0-0 libmatio13 libopenslide0 libopenjp2-7 libjemalloc2 \
libgsf-1-114 libfftw3-bin liborc-0.4-0 librsvg2-2 libcfitsio10 libimagequant0 libaom3 libheif1 \ libgsf-1-114 libfftw3-bin liborc-0.4-0 librsvg2-2 libcfitsio10t64 libimagequant0 libaom3 \
libx264-dev libx265-dev libnuma-dev libvpx7 libtheora0 libvorbis-dev \ libspng0 libcgif0 libheif1 libheif-plugin-x265 libheif-plugin-aomenc libjxl0.11 libavif-dev \
libspng0 libcgif0 && \ libmagickwand-7.q16-10 \
libdav1d7 libx264-dev libx265-dev libnuma-dev libvpx9 libtheora0 libvorbis-dev && \
ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \ ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so && \
apt-get autoremove -y && \ apt-get autoremove -y && \
apt-get autoclean && \ apt-get autoclean && \

73
Dockerfile.builder Normal file
View file

@ -0,0 +1,73 @@
ARG GOLANG_VERSION=1.25.1
ARG FFMPEG_VERSION=7.1.1
ARG VIPS_VERSION=8.17.2
FROM golang:${GOLANG_VERSION}-trixie AS cache-builder
ARG FFMPEG_VERSION
ARG VIPS_VERSION
ARG TARGETARCH
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
ENV MAKEFLAGS="-j8"
# Install libvips + FFmpeg + required libraries
RUN DEBIAN_FRONTEND=noninteractive \
apt-get update && \
apt-get install --no-install-recommends -y \
ca-certificates \
automake build-essential curl \
meson ninja-build pkg-config \
gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg62-turbo-dev libpng-dev \
libwebp-dev libtiff-dev libexif-dev libxml2-dev libpoppler-glib-dev \
swig libpango1.0-dev libmatio-dev libopenslide-dev libcfitsio-dev libopenjp2-7-dev liblcms2-dev \
libgsf-1-dev libfftw3-dev liborc-0.4-dev librsvg2-dev libimagequant-dev libaom-dev \
libspng-dev libcgif-dev libheif-dev libheif-plugin-x265 libheif-plugin-aomenc libjxl-dev libavif-dev \
libmagickwand-dev \
yasm libx264-dev libx265-dev libnuma-dev libvpx-dev libtheora-dev \
librtmp-dev libvorbis-dev libdav1d-dev && \
cd /tmp && \
curl -fsSLO https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz && \
tar xf vips-${VIPS_VERSION}.tar.xz && \
cd vips-${VIPS_VERSION} && \
meson setup _build \
--buildtype=release \
--strip \
--prefix=/usr/local \
--libdir=lib \
-Dmagick=enabled \
-Djpeg-xl=enabled \
-Dintrospection=disabled && \
ninja -C _build && \
ninja -C _build install && \
cd /tmp && \
curl -fsSLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz && \
tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz && \
cd /tmp/ffmpeg-${FFMPEG_VERSION} && \
./configure --prefix=/usr/local \
--disable-debug \
--disable-doc \
--disable-ffplay \
--disable-static \
--enable-shared \
--enable-version3 \
--enable-gpl \
--enable-libtheora \
--enable-libvorbis \
--enable-librtmp \
--enable-libwebp \
--enable-libvpx \
--enable-libx265 \
--enable-libx264 \
--enable-libdav1d \
--enable-libaom && \
make && make install && \
ldconfig && \
rm -rf /usr/local/lib/python* && \
rm -rf /usr/local/lib/libvips-cpp.* && \
rm -rf /usr/local/lib/*.a && \
rm -rf /usr/local/lib/*.la && \
rm -rf /tmp/*
# This cache image provides Go + compiled libvips + FFmpeg libraries
LABEL maintainer="imagorvideo"

View file

@ -3,7 +3,6 @@
[![Test Status](https://github.com/cshum/imagorvideo/workflows/test/badge.svg)](https://github.com/cshum/imagorvideo/actions/workflows/test.yml) [![Test Status](https://github.com/cshum/imagorvideo/workflows/test/badge.svg)](https://github.com/cshum/imagorvideo/actions/workflows/test.yml)
[![Coverage Status](https://img.shields.io/coverallsCoverage/github/cshum/imagorvideo)](https://coveralls.io/github/cshum/imagorvideo?branch=master) [![Coverage Status](https://img.shields.io/coverallsCoverage/github/cshum/imagorvideo)](https://coveralls.io/github/cshum/imagorvideo?branch=master)
[![Docker Hub](https://img.shields.io/badge/docker-shumc/imagorvideo-blue.svg)](https://hub.docker.com/r/shumc/imagorvideo/) [![Docker Hub](https://img.shields.io/badge/docker-shumc/imagorvideo-blue.svg)](https://hub.docker.com/r/shumc/imagorvideo/)
[![GitHub Container Registry](https://ghcr-badge.deta.dev/cshum/imagorvideo/latest_tag?trim=major&label=ghcr.io&ignore=next,master&color=%23007ec6)](https://github.com/cshum/imagorvideo/pkgs/container/imagorvideo)
imagorvideo is a new initiative that brings video thumbnail capability through ffmpeg, built on the foundations of [imagor](https://github.com/cshum/imagor) - a fast, secure image processing server and Go library, using libvips. imagorvideo is a new initiative that brings video thumbnail capability through ffmpeg, built on the foundations of [imagor](https://github.com/cshum/imagor) - a fast, secure image processing server and Go library, using libvips.

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
imagorvideoextended "git.cef.icu/CEF/imagorextended" // imagorvideoextended "git.cef.icu/CEF/imagorextended"
"github.com/cshum/imagor/config" "github.com/cshum/imagor/config"
"github.com/cshum/imagor/config/awsconfig" "github.com/cshum/imagor/config/awsconfig"
"github.com/cshum/imagor/config/gcloudconfig" "github.com/cshum/imagor/config/gcloudconfig"
@ -13,8 +13,7 @@ import (
func main() { func main() {
var server = config.CreateServer( var server = config.CreateServer(
os.Args[1:], os.Args[1:],
//imagorvideoextended.LoaderConfig, // imagorvideoextended.Config,
imagorvideoextended.Config,
imagorvideo.Config, imagorvideo.Config,
vipsconfig.WithVips, vipsconfig.WithVips,
awsconfig.WithAWS, awsconfig.WithAWS,

56
ffmpeg/callback.go Normal file
View file

@ -0,0 +1,56 @@
package ffmpeg
// #include "ffmpeg.h"
import "C"
import (
"io"
"reflect"
"unsafe"
"github.com/cshum/vipsgen/pointer"
)
//export goPacketRead
func goPacketRead(opaque unsafe.Pointer, buffer *C.uint8_t, bufSize C.int) C.int {
ctx, ok := pointer.Restore(opaque).(*AVContext)
if !ok || ctx.reader == nil {
return C.int(ErrUnknown)
}
size := int(bufSize)
sh := &reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(buffer)),
Len: size,
Cap: size,
}
buf := *(*[]byte)(unsafe.Pointer(sh))
n, err := ctx.reader.Read(buf)
if err == io.EOF {
if n == 0 {
return C.int(ErrEOF)
}
} else if err != nil {
return C.int(ErrUnknown)
}
return C.int(n)
}
//export goPacketSeek
func goPacketSeek(opaque unsafe.Pointer, offset C.int64_t, whence C.int) C.int64_t {
ctx, ok := pointer.Restore(opaque).(*AVContext)
if !ok || ctx.seeker == nil {
return C.int64_t(ErrUnknown)
}
if whence == C.AVSEEK_SIZE {
return C.int64_t(ctx.size)
}
n, err := ctx.seeker.Seek(int64(offset), int(whence))
if err != nil {
return C.int64_t(ErrUnknown)
}
return C.int64_t(n)
}
//export goAVLoggingHandler
func goAVLoggingHandler(level C.int, cstr *C.char) {
log(AVLogLevel(level), C.GoString(cstr))
}

38
ffmpeg/errors.go Normal file
View file

@ -0,0 +1,38 @@
package ffmpeg
// #include "ffmpeg.h"
import "C"
type avError int
// AV Error enum
const (
ErrNoMem = avError(-C.ENOMEM)
ErrEOF = avError(C.AVERROR_EOF)
ErrUnknown = avError(C.AVERROR_UNKNOWN)
ErrDecoderNotFound = avError(C.AVERROR_DECODER_NOT_FOUND)
ErrInvalidData = avError(C.AVERROR_INVALIDDATA)
ErrTooBig = avError(C.ERR_TOO_BIG)
)
func (e avError) errorString() string {
switch e {
case ErrNoMem:
return "cannot allocate memory"
case ErrTooBig:
return "video or cover art size exceeds maximum allowed dimensions"
case ErrEOF:
return "end of file"
case ErrDecoderNotFound:
return "decoder not found"
case ErrInvalidData:
return "invalid data found when processing input"
default:
return "unknown error occurred"
}
}
// Error implements error interface
func (e avError) Error() string {
return "ffmpeg: " + e.errorString()
}

15
ffmpeg/errors_test.go Normal file
View file

@ -0,0 +1,15 @@
package ffmpeg
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestErrors(t *testing.T) {
assert.Equal(t, "ffmpeg: cannot allocate memory", ErrNoMem.Error())
assert.Equal(t, "ffmpeg: end of file", ErrEOF.Error())
assert.Equal(t, "ffmpeg: unknown error occurred", ErrUnknown.Error())
assert.Equal(t, "ffmpeg: decoder not found", ErrDecoderNotFound.Error())
assert.Equal(t, "ffmpeg: invalid data found when processing input", ErrInvalidData.Error())
assert.Equal(t, "ffmpeg: video or cover art size exceeds maximum allowed dimensions", ErrTooBig.Error())
}

425
ffmpeg/ffmpeg.c Normal file
View file

@ -0,0 +1,425 @@
#include "ffmpeg.h"
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void free_format_context(AVFormatContext *fmt_ctx) {
if (!fmt_ctx) {
return;
}
av_free(fmt_ctx->pb->buffer);
avio_context_free(&fmt_ctx->pb);
avformat_close_input(&fmt_ctx);
}
int allocate_format_context(AVFormatContext **fmt_ctx) {
AVFormatContext *ctx = NULL;
if (!(ctx = avformat_alloc_context())) {
return AVERROR(ENOMEM);
}
*fmt_ctx = ctx;
return 0;
}
int create_format_context(AVFormatContext *fmt_ctx, void* opaque, int flags) {
int err = 0;
uint8_t *avio_buffer = NULL;
AVIOContext *avio_ctx = NULL;
if (!(avio_buffer = av_malloc(BUFFER_SIZE))) {
avformat_free_context(fmt_ctx);
return AVERROR(ENOMEM);
}
void *reader = NULL;
void *seeker = NULL;
int write_flag = 0;
int seekable = 0;
if (flags & READ_PACKET_FLAG) {
reader = goPacketRead;
}
if (flags & SEEK_PACKET_FLAG) {
seeker = goPacketSeek;
seekable = 1;
}
if (!(avio_ctx = avio_alloc_context(avio_buffer, BUFFER_SIZE, write_flag, opaque, reader, NULL, seeker))) {
av_free(avio_buffer);
avformat_free_context(fmt_ctx);
return AVERROR(ENOMEM);
}
fmt_ctx->pb = avio_ctx;
fmt_ctx->pb->seekable = seekable;
err = avformat_open_input(&fmt_ctx, NULL, NULL, NULL);
if (err < 0) {
av_free(avio_ctx->buffer);
avio_context_free(&avio_ctx);
free_format_context(fmt_ctx);
return err;
}
err = pthread_mutex_lock(&mutex);
if (err < 0) {
free_format_context(fmt_ctx);
return err;
}
err = avformat_find_stream_info(fmt_ctx, NULL);
int muErr = pthread_mutex_unlock(&mutex);
if (err < 0 || muErr < 0) {
free_format_context(fmt_ctx);
if (muErr < 0) {
return muErr;
}
}
return err;
}
static int get_orientation(AVStream *video_stream) {
const AVPacketSideData *side_data = NULL;
double theta = 0;
// Use the new API to get side data from codecpar
side_data = av_packet_side_data_get(video_stream->codecpar->coded_side_data,
video_stream->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX);
if (side_data && side_data->size >= 9 * sizeof(int32_t)) {
theta = -av_display_rotation_get((int32_t *) side_data->data);
}
theta -= 360 * floor(theta / 360 + 0.9 / 360);
int rot = (int) (90 * round(theta / 90)) % 360;
switch (rot) {
case 90:
return 6;
case 180:
return 3;
case 270:
return 8;
default:
return 1;
};
}
void get_metadata(AVFormatContext *fmt_ctx, char **artist, char **title) {
AVDictionaryEntry *tag = NULL;
if ((tag = av_dict_get(fmt_ctx->metadata, "artist", NULL, 0))) {
*artist = tag->value;
}
if ((tag = av_dict_get(fmt_ctx->metadata, "title", NULL, 0))) {
*title = tag->value;
}
}
int find_streams(AVFormatContext *fmt_ctx, AVStream **video_stream, int *orientation) {
int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
int audio_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
int video_audio = 0;
if (audio_stream_index >= 0) {
video_audio |= HAS_AUDIO_STREAM;
}
if (video_stream_index >= 0) {
video_audio |= HAS_VIDEO_STREAM;
} else {
if (video_audio) {
return video_audio;
}
return AVERROR_STREAM_NOT_FOUND;
}
*video_stream = fmt_ctx->streams[video_stream_index];
*orientation = get_orientation(*video_stream);
return video_audio;
}
static int open_codec(AVCodecContext *codec_ctx, const AVCodec *codec) {
int err = pthread_mutex_lock(&mutex);
if (err < 0) {
return err;
}
err = avcodec_open2(codec_ctx, codec, NULL);
int muErr = pthread_mutex_unlock(&mutex);
if (muErr < 0) {
return muErr;
}
return err;
}
int create_codec_context(AVStream *video_stream, AVCodecContext **dec_ctx) {
const AVCodec *dec = NULL;
AVCodecParameters *par = video_stream->codecpar;
if (par->codec_id == AV_CODEC_ID_VP8) {
dec = avcodec_find_decoder_by_name("libvpx");
} else if (par->codec_id == AV_CODEC_ID_VP9) {
dec = avcodec_find_decoder_by_name("libvpx-vp9");
}
if (!dec) {
dec = avcodec_find_decoder(par->codec_id);
}
if (dec == NULL) {
return AVERROR_DECODER_NOT_FOUND;
}
if (par->format == -1) {
return AVERROR_INVALIDDATA;
}
if (av_get_bits_per_pixel(av_pix_fmt_desc_get(par->format)) * par->height * par->width > 1 << 30) {
return ERR_TOO_BIG;
}
if (!(*dec_ctx = avcodec_alloc_context3(dec))) {
return AVERROR(ENOMEM);
}
int err = avcodec_parameters_to_context(*dec_ctx, par);
if (err < 0) {
avcodec_free_context(dec_ctx);
return err;
}
err = open_codec(*dec_ctx, dec);
if (err < 0) {
avcodec_free_context(dec_ctx);
}
return err;
}
AVFrame *convert_frame_to_rgb(AVFrame *frame, int alpha) {
int output_fmt = alpha ? AV_PIX_FMT_RGBA : AV_PIX_FMT_RGB24;
struct SwsContext *sws_ctx = NULL;
AVFrame *output_frame = av_frame_alloc();
if (!output_frame) {
return output_frame;
}
output_frame->height = frame->height;
output_frame->width = frame->width;
output_frame->format = output_fmt;
if (av_frame_get_buffer(output_frame, 1) < 0) {
goto free;
}
if (output_fmt == frame->format) {
if (av_frame_copy(output_frame, frame) < 0) {
goto free;
}
goto done;
}
sws_ctx = sws_getContext(frame->width, frame->height, frame->format,
output_frame->width, output_frame->height, output_fmt,
SWS_LANCZOS | SWS_ACCURATE_RND, NULL, NULL, NULL);
if (!sws_ctx) {
goto free;
}
if (sws_scale(sws_ctx, (const uint8_t *const *) frame->data, frame->linesize, 0, frame->height, output_frame->data,
output_frame->linesize) != output_frame->height) {
goto free;
} else {
goto done;
}
free:
av_frame_free(&output_frame);
done:
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
return output_frame;
}
AVPacket *create_packet() {
AVPacket *pkt = av_packet_alloc();
if (!pkt) {
return NULL;
}
pkt->data = NULL;
pkt->size = 0;
return pkt;
}
int obtain_next_frame(AVFormatContext *fmt_ctx, AVCodecContext *dec_ctx, int stream_index, AVPacket *pkt, AVFrame **frame) {
int err = 0, retry = 0;
if (!(*frame) && !(*frame = av_frame_alloc())) {
err = AVERROR(ENOMEM);
return err;
}
// Try to receive a frame from the decoder first
if ((err = avcodec_receive_frame(dec_ctx, *frame)) != AVERROR(EAGAIN)) {
return err;
}
while (1) {
if ((err = av_read_frame(fmt_ctx, pkt)) < 0) {
break;
}
if (pkt->stream_index != stream_index) {
av_packet_unref(pkt);
continue;
}
if ((err = avcodec_send_packet(dec_ctx, pkt)) < 0) {
if (retry++ >= 10) {
break;
}
av_packet_unref(pkt);
continue;
}
if (!(*frame) && !(*frame = av_frame_alloc())) {
err = AVERROR(ENOMEM);
break;
}
err = avcodec_receive_frame(dec_ctx, *frame);
if (err >= 0 || err != AVERROR(EAGAIN)) {
break;
}
av_packet_unref(pkt);
}
if (pkt->data) {
av_packet_unref(pkt);
}
return err;
}
ThumbContext *create_thumb_context(AVStream *stream, AVFrame *frame) {
ThumbContext *thumb_ctx = av_mallocz(sizeof *thumb_ctx);
if (!thumb_ctx) {
return thumb_ctx;
}
thumb_ctx->desc = av_pix_fmt_desc_get(frame->format);
int nb_frames = 100;
if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) {
nb_frames = 1;
} else if (stream->nb_frames && stream->nb_frames < 400) {
nb_frames = (int) (stream->nb_frames >> 2) + 1;
}
int frames_in_128mb = (1 << 30) / (av_get_bits_per_pixel(thumb_ctx->desc) * frame->height * frame->width);
thumb_ctx->max_frames = FFMIN(nb_frames, frames_in_128mb);
int i;
for (i = 0; i < thumb_ctx->desc->nb_components; i++) {
thumb_ctx->hist_size += 1 << thumb_ctx->desc->comp[i].depth;
}
thumb_ctx->median = av_calloc(thumb_ctx->hist_size, sizeof(double));
if (!thumb_ctx->median) {
av_free(thumb_ctx);
return NULL;
}
thumb_ctx->frames = av_malloc_array((size_t) thumb_ctx->max_frames, sizeof *thumb_ctx->frames);
if (!thumb_ctx->frames) {
av_free(thumb_ctx->median);
av_free(thumb_ctx);
return NULL;
}
for (i = 0; i < thumb_ctx->max_frames; i++) {
thumb_ctx->frames[i].frame = NULL;
thumb_ctx->frames[i].hist = av_calloc(thumb_ctx->hist_size, sizeof(int));
if (!thumb_ctx->frames[i].hist) {
for (i--; i >= 0; i--) {
av_free(thumb_ctx->frames[i].hist);
}
av_free(thumb_ctx->median);
av_free(thumb_ctx);
return NULL;
}
}
return thumb_ctx;
}
void free_thumb_context(ThumbContext *thumb_ctx) {
if (!thumb_ctx) {
return;
}
int i;
for (i = 0; i < thumb_ctx->n; i++) {
av_frame_free(&thumb_ctx->frames[i].frame);
av_free(thumb_ctx->frames[i].hist);
}
for (i = thumb_ctx->n; i < thumb_ctx->max_frames; i++) {
av_free(thumb_ctx->frames[i].hist);
}
av_free(thumb_ctx->median);
av_free(thumb_ctx->frames);
av_free(thumb_ctx);
}
static double root_mean_square_error(const int *hist, const double *median, size_t hist_size) {
int i;
double err, sum_sq_err = 0;
for (i = 0; i < hist_size; i++) {
err = median[i] - (double) hist[i];
sum_sq_err += err * err;
}
return sum_sq_err;
}
void populate_frame(ThumbContext *thumb_ctx, int n, AVFrame *frame) {
thumb_ctx->frames[n].frame = frame;
}
void populate_histogram(ThumbContext *thumb_ctx, int n, AVFrame *frame) {
const AVPixFmtDescriptor *desc = thumb_ctx->desc;
thumb_ctx->frames[n].frame = frame;
int *hist = thumb_ctx->frames[n].hist;
AVComponentDescriptor comp;
int w, h, plane, depth, mask, shift, step, height, width;
uint64_t flags;
uint8_t **data = frame->data;
int *linesize = frame->linesize;
for (int c = 0; c < desc->nb_components; c++) {
comp = desc->comp[c];
plane = comp.plane;
depth = comp.depth;
mask = (1 << depth) - 1;
shift = comp.shift;
step = comp.step;
flags = desc->flags;
width = !(desc->log2_chroma_w) || (c != 1 && c != 2) ? frame->width : AV_CEIL_RSHIFT(frame->width,
desc->log2_chroma_w);
height = !(desc->log2_chroma_h) || (c != 1 && c != 2) ? frame->height : AV_CEIL_RSHIFT(frame->height,
desc->log2_chroma_h);
for (h = 0; h < height; h++) {
w = width;
if (flags & AV_PIX_FMT_FLAG_BITSTREAM) {
const uint8_t *p = data[plane] + h * linesize[plane] + (comp.offset >> 3);
shift = 8 - depth - (comp.offset & 7);
while (w--) {
int val = (*p >> shift) & mask;
shift -= step;
p -= shift >> 3;
shift &= 7;
(*(hist + val))++;
}
} else {
const uint8_t *p = data[plane] + h * linesize[plane] + comp.offset;
int is_8bit = shift + depth <= 8;
if (is_8bit)
p += (flags & AV_PIX_FMT_FLAG_BE) != 0;
while (w--) {
int val = is_8bit ? *p :
flags & AV_PIX_FMT_FLAG_BE ? AV_RB16(p) : AV_RL16(p);
val = (val >> shift) & mask;
p += step;
(*(hist + val))++;
}
}
}
hist += 1 << depth;
}
}
int find_best_frame_index(ThumbContext *thumb_ctx) {
int i, j, n = 0, m = thumb_ctx->n, *hist = NULL;
double *median = thumb_ctx->median;
for (j = 0; j < m; j++) {
hist = thumb_ctx->frames[j].hist;
for (i = 0; i < thumb_ctx->hist_size; i++) {
median[i] += (double) hist[i] / m;
}
}
struct thumb_frame *t_frame = NULL;
double min_sum_sq_err = DBL_MAX, sum_sq_err = 0;
for (i = 0; i < thumb_ctx->n; i++) {
t_frame = thumb_ctx->frames + i;
sum_sq_err = root_mean_square_error(t_frame->hist, thumb_ctx->median, thumb_ctx->hist_size);
if (sum_sq_err < min_sum_sq_err) {
min_sum_sq_err = sum_sq_err;
n = i;
}
}
return n;
}
AVFrame *select_frame(ThumbContext *thumb_ctx, int n) {
return thumb_ctx->frames[n].frame;
}

402
ffmpeg/ffmpeg.go Normal file
View file

@ -0,0 +1,402 @@
package ffmpeg
// #cgo pkg-config: libavformat libavutil libavcodec libswscale
// #cgo CFLAGS: -std=c11
// #cgo LDFLAGS: -lm
// #include "ffmpeg.h"
import "C"
import (
"github.com/cshum/vipsgen/pointer"
"io"
"math"
"time"
"unsafe"
)
const (
readPacketFlag = 1
seekPacketFlag = 2
hasVideo = 1
hasAudio = 2
)
// Metadata AV metadata
type Metadata struct {
Orientation int `json:"orientation"`
Duration int `json:"duration,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Title string `json:"title,omitempty"`
Artist string `json:"artist,omitempty"`
FPS float64 `json:"fps,omitempty"`
HasVideo bool `json:"has_video"`
HasAudio bool `json:"has_audio"`
}
// AVContext manages lifecycle of AV contexts and reader stream
type AVContext struct {
opaque unsafe.Pointer
reader io.Reader
seeker io.Seeker
formatContext *C.AVFormatContext
stream *C.AVStream
codecContext *C.AVCodecContext
thumbContext *C.ThumbContext
selectedIndex C.int
selectedDuration time.Duration
frame *C.AVFrame
durationInFormat bool
orientation int
size int64
duration time.Duration
availableIndex C.int
availableDuration time.Duration
width, height int
title, artist string
hasVideo, hasAudio bool
closed bool
}
// LoadAVContext load and create AVContext from reader stream
func LoadAVContext(reader io.Reader, size int64) (*AVContext, error) {
av := &AVContext{
reader: reader,
size: size,
selectedIndex: -1,
}
if seeker, ok := reader.(io.Seeker); ok {
av.seeker = seeker
}
flags := C.int(readPacketFlag)
if av.seeker != nil {
flags |= seekPacketFlag
}
if err := createFormatContext(av, flags); err != nil {
return nil, err
}
if !av.hasVideo {
return av, nil
}
return av, createDecoder(av)
}
// ProcessFrames triggers frame processing
// limit under max num of frames if maxFrames > 0
func (av *AVContext) ProcessFrames(maxFrames int) (err error) {
if av.formatContext == nil || av.codecContext == nil {
return ErrDecoderNotFound
}
if av.thumbContext == nil {
return createThumbContext(av, C.int(maxFrames))
}
return
}
// SelectFrame triggers frame processing and select specific frame index
func (av *AVContext) SelectFrame(n int) (err error) {
nn := C.int(n - 1)
if av.thumbContext != nil && nn > av.availableIndex {
nn = av.availableIndex
}
av.selectedIndex = nn
return av.ProcessFrames(-1)
}
func (av *AVContext) positionToDuration(f float64) time.Duration {
return time.Duration(float64(av.duration) * math.Max(math.Min(f, 1), 0))
}
func (av *AVContext) SelectPosition(f float64) (err error) {
return av.SelectDuration(av.positionToDuration(f))
}
// SelectDuration seeks to keyframe before the specified duration
// then process frames to find precise duration
func (av *AVContext) SelectDuration(ts time.Duration) (err error) {
if ts > 0 {
av.selectedDuration = ts
if err = av.SeekDuration(ts); err != nil {
return
}
return av.ProcessFrames(-1)
} else {
return av.SelectFrame(1)
}
}
// SeekPosition seeks to keyframe before specified position percentage between 0 and 1
// then process frames to find precise position
func (av *AVContext) SeekPosition(f float64) error {
return av.SeekDuration(av.positionToDuration(f))
}
// SeekDuration seeks to keyframe before the specified duration
func (av *AVContext) SeekDuration(ts time.Duration) error {
if av.formatContext == nil || av.codecContext == nil {
return ErrDecoderNotFound
}
return seekDuration(av, ts)
}
// Export frame to RGB or RGBA buffer
func (av *AVContext) Export(bands int) (buf []byte, err error) {
if err = av.ProcessFrames(-1); err != nil {
return
}
if bands < 3 || bands > 4 {
bands = 4
}
if err = convertFrameToRGB(av, bands); err != nil {
return
}
return exportBuffer(av, bands)
}
// Close AVContext objects
func (av *AVContext) Close() {
closeAVContext(av)
}
// Metadata AV metadata
func (av *AVContext) Metadata() *Metadata {
var fps float64
if av.stream != nil {
fps = float64(av.stream.r_frame_rate.num) / float64(av.stream.r_frame_rate.den)
}
return &Metadata{
Orientation: av.orientation,
Duration: int(av.duration / time.Millisecond),
Width: av.width,
Height: av.height,
Title: av.title,
Artist: av.artist,
FPS: fps,
HasVideo: av.hasVideo,
HasAudio: av.hasAudio,
}
}
func closeAVContext(av *AVContext) {
if !av.closed {
if av.frame != nil {
C.av_frame_free(&av.frame)
}
if av.thumbContext != nil {
C.free_thumb_context(av.thumbContext)
}
if av.codecContext != nil {
C.avcodec_free_context(&av.codecContext)
}
if av.formatContext != nil {
C.free_format_context(av.formatContext)
}
pointer.Unref(av.opaque)
av.closed = true
}
}
func createFormatContext(av *AVContext, callbackFlags C.int) error {
intErr := C.allocate_format_context(&av.formatContext)
if intErr < 0 {
return avError(intErr)
}
av.opaque = pointer.Save(av)
intErr = C.create_format_context(av.formatContext, av.opaque, callbackFlags)
if intErr < 0 {
pointer.Unref(av.opaque)
return avError(intErr)
}
metadata(av)
duration(av)
err := findStreams(av)
if err != nil {
C.free_format_context(av.formatContext)
pointer.Unref(av.opaque)
}
return err
}
func metadata(av *AVContext) {
var artist, title *C.char
C.get_metadata(av.formatContext, &artist, &title)
av.artist = C.GoString(artist)
av.title = C.GoString(title)
}
func duration(av *AVContext) {
if av.formatContext.duration > 0 {
av.durationInFormat = true
av.duration = time.Duration(1000 * av.formatContext.duration)
}
}
func findStreams(av *AVContext) error {
var orientation C.int
err := C.find_streams(av.formatContext, &av.stream, &orientation)
if err < 0 {
return avError(err)
}
av.hasVideo = err&hasVideo != 0
av.hasAudio = err&hasAudio != 0
if av.hasVideo {
av.width = int(av.stream.codecpar.width)
av.height = int(av.stream.codecpar.height)
av.orientation = int(orientation)
}
return nil
}
func createDecoder(av *AVContext) error {
err := C.create_codec_context(av.stream, &av.codecContext)
if err < 0 {
return avError(err)
}
return nil
}
func seekDuration(av *AVContext, ts time.Duration) error {
tts := C.int64_t(ts.Milliseconds()) * C.AV_TIME_BASE / 1000
err := C.av_seek_frame(av.formatContext, C.int(-1), tts, C.AVSEEK_FLAG_BACKWARD)
C.avcodec_flush_buffers(av.codecContext)
if err < 0 {
return avError(err)
}
return nil
}
func incrementDuration(av *AVContext, frame *C.AVFrame, i C.int) {
av.availableIndex = i
if frame.pts != C.AV_NOPTS_VALUE {
ptsToNano := C.int64_t(1000000000 * av.stream.time_base.num / av.stream.time_base.den)
newDuration := time.Duration(frame.pts * ptsToNano)
av.availableDuration = newDuration
if !av.durationInFormat && newDuration > av.duration {
av.duration = newDuration
}
}
}
func populateFrames(av *AVContext, frames <-chan *C.AVFrame) <-chan struct{} {
done := make(chan struct{})
var isSelected = av.selectedIndex > -1
go func() {
var n C.int
if !isSelected {
for frame := range frames {
C.populate_histogram(av.thumbContext, n, frame)
n++
}
} else {
for frame := range frames {
C.populate_frame(av.thumbContext, n, frame)
n++
}
}
av.thumbContext.n = n
close(done)
}()
return done
}
func createThumbContext(av *AVContext, maxFrames C.int) error {
pkt := C.create_packet()
if pkt == nil {
return avError(C.int(ErrNoMem))
}
defer C.av_packet_free(&pkt)
var frame *C.AVFrame
err := C.obtain_next_frame(av.formatContext, av.codecContext, av.stream.index, pkt, &frame)
if err >= 0 {
incrementDuration(av, frame, 0)
av.thumbContext = C.create_thumb_context(av.stream, frame)
if av.thumbContext == nil {
err = C.int(ErrNoMem)
}
}
if err < 0 {
if frame != nil {
C.av_frame_free(&frame)
}
return avError(err)
}
n := av.thumbContext.max_frames
if maxFrames > 0 && n > maxFrames {
n = maxFrames
}
if av.selectedIndex > -1 && n > av.selectedIndex+1 {
n = av.selectedIndex + 1
}
if av.selectedDuration > 0 && av.selectedIndex < 0 {
av.selectedIndex = 0
}
frames := make(chan *C.AVFrame, n)
done := populateFrames(av, frames)
frames <- frame
return populateThumbContext(av, frames, n, done)
}
func populateThumbContext(av *AVContext, frames chan *C.AVFrame, n C.int, done <-chan struct{}) error {
pkt := C.create_packet()
if pkt == nil {
return avError(C.int(ErrNoMem))
}
defer C.av_packet_free(&pkt)
var frame *C.AVFrame
var err C.int
for i := C.int(1); i < n; i++ {
err = C.obtain_next_frame(av.formatContext, av.codecContext, av.stream.index, pkt, &frame)
if err < 0 {
break
}
incrementDuration(av, frame, i)
frames <- frame
frame = nil
if av.selectedDuration > 0 {
if av.availableDuration <= av.selectedDuration {
av.selectedIndex = i
} else {
break
}
}
}
if av.selectedIndex > av.availableIndex {
av.selectedIndex = av.availableIndex
}
close(frames)
if frame != nil {
C.av_frame_free(&frame)
}
<-done
if err != 0 && err != C.int(ErrEOF) {
return avError(err)
}
if av.selectedIndex < 0 {
av.selectedIndex = C.find_best_frame_index(av.thumbContext)
}
return nil
}
func convertFrameToRGB(av *AVContext, bands int) error {
var alpha int
if bands == 4 {
alpha = 1
}
av.frame = C.convert_frame_to_rgb(
C.select_frame(av.thumbContext, av.selectedIndex), C.int(alpha))
if av.frame == nil {
return ErrNoMem
}
return nil
}
func exportBuffer(av *AVContext, bands int) ([]byte, error) {
if av.frame == nil {
return nil, ErrInvalidData
}
size := av.height * av.width * bands
buf := C.GoBytes(unsafe.Pointer(av.frame.data[0]), C.int(size))
return buf, nil
}

68
ffmpeg/ffmpeg.h Normal file
View file

@ -0,0 +1,68 @@
#include <math.h>
#include <pthread.h>
#include <float.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/file.h>
#include <libavutil/pixfmt.h>
#include <libswscale/swscale.h>
#include <libavutil/pixdesc.h>
#include <libavutil/intreadwrite.h>
#include <libavutil/imgutils.h>
#include <libavutil/display.h>
#define BUFFER_SIZE 1 << 12
#define READ_PACKET_FLAG 1
#define SEEK_PACKET_FLAG 2
#define HAS_VIDEO_STREAM 1
#define HAS_AUDIO_STREAM 2
#define ERR_TOO_BIG FFERRTAG('H','M','M','M')
struct thumb_frame {
AVFrame *frame;
int *hist;
};
typedef struct ThumbContext {
int n, max_frames;
struct thumb_frame *frames;
double *median;
const AVPixFmtDescriptor *desc;
size_t hist_size;
} ThumbContext;
int allocate_format_context(AVFormatContext **fmt_ctx);
int create_format_context(AVFormatContext *fmt_ctx, void* opaque, int callbacks);
void free_format_context(AVFormatContext *fmt_ctx);
void get_metadata(AVFormatContext *fmt_ctx, char **artist, char **title);
int find_streams(AVFormatContext *fmt_ctx, AVStream **video_stream, int *orientation);
int create_codec_context(AVStream *video_stream, AVCodecContext **dec_ctx);
AVFrame *convert_frame_to_rgb(AVFrame *frame, int alpha);
AVPacket *create_packet();
int obtain_next_frame(AVFormatContext *fmt_ctx, AVCodecContext *dec_ctx, int stream_index, AVPacket *pkt, AVFrame **frame);
ThumbContext *create_thumb_context(AVStream *stream, AVFrame *frame);
void free_thumb_context(ThumbContext *thumb_ctx);
int find_best_frame_index(ThumbContext *thumb_ctx);
AVFrame *select_frame(ThumbContext *thumb_ctx, int i);
void populate_frame(ThumbContext *thumb_ctx, int n, AVFrame *frame);
void populate_histogram(ThumbContext *thumb_ctx, int n, AVFrame *frame);
extern int goPacketRead(void *opaque, uint8_t *buf, int buf_size);
extern int64_t goPacketSeek(void *opaque, int64_t seek, int whence);

188
ffmpeg/ffmpeg_test.go Normal file
View file

@ -0,0 +1,188 @@
package ffmpeg
import (
"encoding/json"
"fmt"
"github.com/cshum/vipsgen/pointer"
"github.com/cshum/vipsgen/vips"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"io"
"os"
"reflect"
"strings"
"testing"
"time"
)
var files = []string{
"everybody-betray-me.mkv",
"alpha-webm.webm",
"schizo.flv",
"macabre.mp4",
"schizo_0.mp4",
"schizo_90.mp4",
"schizo_180.mp4",
"schizo_270.mp4",
"with_cover.mp3",
}
var noVideo = []string{
"no_cover.mp3",
}
var baseDir = "../testdata/"
func TestAVContext(t *testing.T) {
vips.Startup(nil)
SetFFmpegLogLevel(AVLogDebug)
logger := zap.NewExample()
SetLogging(nil)
log(AVLogDebug, "nop logging")
SetLogging(func(level AVLogLevel, message string) {
message = strings.TrimSuffix(message, "\n")
switch level {
case AVLogTrace, AVLogDebug, AVLogVerbose:
logger.Debug("ffmpeg", zap.String("log", message))
case AVLogInfo:
logger.Info("ffmpeg", zap.String("log", message))
case AVLogWarning, AVLogError, AVLogFatal, AVLogPanic:
logger.Warn("ffmpeg", zap.String("log", message))
}
})
require.NoError(t, os.MkdirAll(baseDir+"golden/meta", 0755))
require.NoError(t, os.MkdirAll(baseDir+"golden/export", 0755))
t.Parallel()
for _, filename := range files {
for _, n := range []int{-1, 1, 5, 10, 9999, 99999} {
name := filename
if n > -1 {
name = fmt.Sprintf("%s-%d", filename, n)
}
t.Run(name, func(t *testing.T) {
path := baseDir + filename
reader, err := os.Open(path)
require.NoError(t, err)
stats, err := os.Stat(path)
require.NoError(t, err)
av, err := LoadAVContext(reader, stats.Size())
meta := av.Metadata()
metaBuf, err := json.Marshal(meta)
require.NoError(t, err)
goldenFile := baseDir + "golden/meta/" + name + ".meta.json"
if curr, err := os.ReadFile(goldenFile); err == nil {
assert.Equal(t, string(curr), string(metaBuf))
} else {
require.NoError(t, os.WriteFile(goldenFile, metaBuf, 0666))
}
require.NoError(t, err)
defer av.Close()
if n == 10 {
require.NoError(t, av.ProcessFrames(n))
} else if n == 99999 {
require.NoError(t, av.SelectDuration(time.Second))
} else if n == 9999 {
require.NoError(t, av.SelectPosition(0.7))
} else if n == 1 {
require.NoError(t, av.SelectDuration(0))
} else if n == 5 {
require.NoError(t, av.SelectFrame(n))
} else {
require.NoError(t, av.SeekPosition(0.7))
}
bands := 4
if n == 99999 {
bands = 999
}
buf, err := av.Export(bands)
require.NoError(t, err)
if bands > 4 {
bands = 4
}
img, err := vips.NewImageFromMemory(buf, meta.Width, meta.Height, bands)
require.NoError(t, err)
buf, err = img.JpegsaveBuffer(nil)
require.NoError(t, err)
goldenFile = baseDir + "golden/export/" + name + ".jpg"
if curr, err := os.ReadFile(goldenFile); err == nil {
assert.True(t, reflect.DeepEqual(curr, buf))
} else {
require.NoError(t, os.WriteFile(goldenFile, buf, 0666))
}
})
}
}
}
func TestNoVideo(t *testing.T) {
require.NoError(t, os.MkdirAll(baseDir+"golden/meta", 0755))
require.NoError(t, os.MkdirAll(baseDir+"golden/export", 0755))
for _, filename := range noVideo {
for i := 0; i < 2; i++ {
t.Run(fmt.Sprintf("%s-%d", filename, i), func(t *testing.T) {
path := baseDir + filename
reader, err := os.Open(path)
require.NoError(t, err)
stats, err := os.Stat(path)
require.NoError(t, err)
av, err := LoadAVContext(reader, stats.Size())
require.NoError(t, err)
defer av.Close()
require.Equal(t, ErrDecoderNotFound, av.ProcessFrames(-1))
meta := av.Metadata()
metaBuf, err := json.Marshal(meta)
require.NoError(t, err)
goldenFile := baseDir + "golden/meta/" + filename + ".meta.json"
if curr, err := os.ReadFile(goldenFile); err == nil {
assert.Equal(t, string(curr), string(metaBuf))
} else {
require.NoError(t, os.WriteFile(goldenFile, metaBuf, 0666))
}
if i == 0 {
buf, err := av.Export(3)
require.Empty(t, buf)
assert.Equal(t, ErrDecoderNotFound, err)
} else {
assert.Equal(t, ErrDecoderNotFound, av.SelectFrame(1))
}
})
}
}
}
func TestCorrupted(t *testing.T) {
filename := "macabre.mp4"
path := baseDir + filename
file, err := os.Open(path)
require.NoError(t, err)
reader := &readCloser{
Reader: io.LimitReader(file, 1024),
Closer: file,
}
stats, err := os.Stat(path)
require.NoError(t, err)
av, err := LoadAVContext(reader, stats.Size())
require.Equal(t, ErrInvalidData, err)
require.Empty(t, av)
}
func TestCorruptedOpaque(t *testing.T) {
filename := "macabre.mp4"
path := baseDir + filename
reader, err := os.Open(path)
require.NoError(t, err)
stats, err := os.Stat(path)
require.NoError(t, err)
av, err := LoadAVContext(reader, stats.Size())
require.NoError(t, err)
defer av.Close()
pointer.Unref(av.opaque)
err = av.ProcessFrames(-1)
assert.Equal(t, ErrUnknown, err)
}
type readCloser struct {
io.Reader
io.Closer
}

12
ffmpeg/logging.c Normal file
View file

@ -0,0 +1,12 @@
#include "logging.h"
void goavLogCallback(void *class_ptr, int level, const char *fmt, va_list vl) {
char line[LINE_SZ];
int print_prefix = 1;
av_log_format_line(class_ptr, level, fmt, vl, line, LINE_SZ, &print_prefix);
goAVLoggingHandler(level, line);
}
void goavLogSetup() {
av_log_set_callback(goavLogCallback);
}

55
ffmpeg/logging.go Normal file
View file

@ -0,0 +1,55 @@
package ffmpeg
// #include "ffmpeg.h"
// #include "logging.h"
import "C"
import "sync"
// AVLogLevel defines the ffmpeg threshold for dumping information to stderr.
type AVLogLevel int
// AVLogLevel enum
const (
AVLogQuiet AVLogLevel = (iota - 1) * 8
AVLogPanic
AVLogFatal
AVLogError
AVLogWarning
AVLogInfo
AVLogVerbose
AVLogDebug
AVLogTrace
)
var (
currentLoggingHandlerFunction = noopLoggingHandler
currentLoggingVerbosity AVLogLevel
onceLogging sync.Once
)
// SetFFmpegLogLevel allows you to change the log level from the default (AVLogInfo).
func SetFFmpegLogLevel(logLevel AVLogLevel) {
C.av_log_set_level(C.int(logLevel))
currentLoggingVerbosity = logLevel
}
type LoggingHandlerFunction func(messageLevel AVLogLevel, message string)
// SetLogging set AV logging handler
func SetLogging(handler LoggingHandlerFunction) {
onceLogging.Do(func() {
C.goavLogSetup()
})
if handler != nil {
currentLoggingHandlerFunction = handler
}
}
func noopLoggingHandler(_ AVLogLevel, _ string) {
}
func log(level AVLogLevel, message string) {
if level <= currentLoggingVerbosity {
currentLoggingHandlerFunction(level, message)
}
}

11
ffmpeg/logging.h Normal file
View file

@ -0,0 +1,11 @@
#include <stdarg.h>
#define LINE_SZ 1024
extern void goAVLoggingHandler(int level, char *str);
extern void av_log_set_callback(void (*callback)(void *, int, const char *, va_list));
extern void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl, char *line, int line_size, int *print_prefix);
void goavLogCallback(void *class_ptr, int level, const char *fmt, va_list vl);
void goavLogSetup();

129
go.mod
View file

@ -1,68 +1,95 @@
module git.cef.icu/CEF/imagorextended module github.com/cshum/imagorvideo
go 1.21 go 1.24.2
toolchain go1.21.1
require ( require (
github.com/cshum/imagor v1.4.13 github.com/cshum/imagor v1.6.6
github.com/cshum/imagorvideo v0.4.13 github.com/cshum/vipsgen v1.2.1
github.com/gabriel-vasile/mimetype v1.4.4 github.com/gabriel-vasile/mimetype v1.4.11
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
) )
require ( require (
cloud.google.com/go v0.115.0 // indirect cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.7.1 // indirect cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/iam v1.1.11 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/storage v1.43.0 // indirect cloud.google.com/go/iam v1.5.3 // indirect
github.com/antchfx/htmlquery v1.3.2 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/antchfx/xpath v1.3.1 // indirect cloud.google.com/go/storage v1.57.2 // indirect
github.com/aws/aws-sdk-go v1.54.20 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/TheZeroSlave/zapsentry v1.23.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.20 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/getsentry/sentry-go v0.38.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/common v0.67.3 // indirect
github.com/rs/cors v1.11.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
go.opencensus.io v0.24.0 // indirect github.com/rs/cors v1.11.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/image v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/api v0.188.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.65.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

422
go.sum
View file

@ -1,266 +1,240 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk=
cloud.google.com/go/iam v1.1.11 h1:0mQ8UKSfdHLut6pH9FM3bI55KWR46ketn0PuXleDyxw= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/iam v1.1.11/go.mod h1:biXoiLWYIKntto2joP+62sd9uW5EpkZmKIvfNcTWlnQ= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/longrunning v0.5.9 h1:haH9pAuXdPAMqHvzX0zlWQigXT7B0+CL4/2nXXdBo5k= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/longrunning v0.5.9/go.mod h1:HD+0l9/OOW0za6UWdKJtXoFAX/BGg/3Wj8p10NeWF7c= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/pubsub v1.40.0 h1:0LdP+zj5XaPAGtWr2V6r88VXJlmtaB/+fde1q3TU8M0= cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/pubsub v1.40.0/go.mod h1:BVJI4sI2FyXp36KFKvFwcfDRDfR8MiLT8mMhmIhdAeA= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
github.com/antchfx/htmlquery v1.3.2 h1:85YdttVkR1rAY+Oiv/nKI4FCimID+NXhDn82kz3mEvs= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
github.com/antchfx/htmlquery v1.3.2/go.mod h1:1mbkcEgEarAokJiWhTfr4hR06w/q2ZZjnYLrDt6CTUk= cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
github.com/antchfx/xpath v1.3.1 h1:PNbFuUqHwWl0xRjvUPjJ95Agbmdj2uzzIwmQKgu4oCk= cloud.google.com/go/pubsub/v2 v2.3.0 h1:DgAN907x+sP0nScYfBzneRiIhWoXcpCD8ZAut8WX9vs=
github.com/antchfx/xpath v1.3.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= cloud.google.com/go/pubsub/v2 v2.3.0/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw=
github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo= cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4=
github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/TheZeroSlave/zapsentry v1.23.0 h1:TKyzfEL7LRlRr+7AvkukVLZ+jZPC++ebCUv7ZJHl1AU=
github.com/TheZeroSlave/zapsentry v1.23.0/go.mod h1:3DRFLu4gIpnCTD4V9HMCBSaqYP8gYU7mZickrs2/rIY=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc=
github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2 h1:DhdbtDl4FdNlj31+xiRXANxEE+eC7n8JQz+/ilwQ8Uc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0=
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e h1:gt7U1Igw0xbJdyaCM5H2CnlAlPSkzrhsebQB6WQWjLA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20251110193048-8bfbf64dc13e/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cshum/imagor v1.4.13 h1:BFcSpsTUOJj+Wv5SzDeXa8bhsT/Ehw7EcrFD0UTdpmU= github.com/cshum/imagor v1.6.6 h1:w+pf6hy1qFUkz8/Np8sDrtAs7pgzqZuDjaJX4xn6ams=
github.com/cshum/imagor v1.4.13/go.mod h1:LHxXgks6Y06GzEHitnlO8vcD5gznxIHWPdvGsnlGpMo= github.com/cshum/imagor v1.6.6/go.mod h1:Dt7yj7TLE3VFmGqc+gxfXWtvoHaXHIOeog4zlsHXZiE=
github.com/cshum/imagorvideo v0.4.13 h1:tn+TmdPMvS00XndPX8j/BUMyebErkxXgpJ9CLxNvROE= github.com/cshum/vipsgen v1.2.1 h1:Es305Zf7C9T+8QbsiWn3BtQ+2/uHz6sp/SFnvwnO/kU=
github.com/cshum/imagorvideo v0.4.13/go.mod h1:y2g2GQst6b1X+jvAcxhDusTcYYGJqp+fSRcUo3l+PxU= github.com/cshum/vipsgen v1.2.1/go.mod h1:1GboZQcNmo4NwuNnGogM24m3O+1i6UpnvurqMcsFItE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsouza/fake-gcs-server v1.49.2 h1:fukDqzEQM50QkA0jAbl6cLqeDu3maQjwZBuys759TR4= github.com/fsouza/fake-gcs-server v1.52.3 h1:hXddOPMGDKq5ENmttw6xkodVJy0uVhf7HhWvQgAOH6g=
github.com/fsouza/fake-gcs-server v1.49.2/go.mod h1:17SYzJEXRcaAA5ATwwvgBkSIqIy7r1icnGM0y/y4foY= github.com/fsouza/fake-gcs-server v1.52.3/go.mod h1:A0XtSRX+zz5pLRAt88j9+Of0omQQW+RMqipFbvdNclQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getsentry/sentry-go v0.38.0 h1:S8Xui7gLeAvXINVLMOaX94HnsDf1GexnfXGSNC4+KQs=
github.com/getsentry/sentry-go v0.38.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= github.com/google/renameio/v2 v2.0.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/renameio/v2 v2.0.1/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3 h1:2713fQZ560HxoNVgfJH41GKzjMjIG+DW4hH6nYXfXW8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3/go.mod h1:S4S9jGBVlLri0OeqrSSbCGG5vsI6he06UJyuz1WT1EE=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/johannesboyne/gofakes3 v0.0.0-20240701191259-edd0227ffc37 h1:w/TiKkLc+oLH7mUCpP5DUn8+a0CjhK9yWQLKBA0Iv1w=
github.com/johannesboyne/gofakes3 v0.0.0-20240701191259-edd0227ffc37/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIGMa7oSwp8SQPp7Hb4ZiI38K3RNBKD2LLeEM= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

166
processor_test.go Normal file
View file

@ -0,0 +1,166 @@
package imagorvideo
import (
"context"
"fmt"
"github.com/cshum/imagor"
"github.com/cshum/imagor/imagorpath"
"github.com/cshum/imagor/processor/vipsprocessor"
"github.com/cshum/imagor/storage/filestorage"
"github.com/cshum/vipsgen/vips"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
var testDataDir string
func init() {
_, b, _, _ := runtime.Caller(0)
testDataDir = filepath.Join(filepath.Dir(b), "./testdata")
}
type test struct {
name string
path string
expectCode int
}
func TestProcessor(t *testing.T) {
v := vipsprocessor.NewProcessor(vipsprocessor.WithDebug(true))
require.NoError(t, v.Startup(context.Background()))
t.Cleanup(func() {
require.NoError(t, v.Shutdown(context.Background()))
})
doGoldenTests(t, filepath.Join(testDataDir, "golden/result"), []test{
{name: "mkv", path: "fit-in/100x100/everybody-betray-me.mkv"},
{name: "mkv specific frame", path: "fit-in/100x100/filters:frame(3)/everybody-betray-me.mkv"},
{name: "mkv specific max_frames", path: "fit-in/100x100/filters:max_frames(6)/everybody-betray-me.mkv"},
{name: "mkv specific frame exceeded", path: "fit-in/100x100/filters:frame(99999)/everybody-betray-me.mkv"},
{name: "mkv meta max_frames", path: "meta/filters:max_frames()/everybody-betray-me.mkv"},
{name: "mkv meta max_frames 6", path: "meta/filters:max_frames(6)/everybody-betray-me.mkv"},
{name: "mkv meta", path: "meta/everybody-betray-me.mkv"},
{name: "mp4", path: "200x100/schizo_0.mp4"},
{name: "mp4 orient 90", path: "220x100/schizo_90.mp4"},
{name: "mp4 orient 180", path: "200x100/schizo_180.mp4"},
{name: "mp4 orient 270", path: "200x100/schizo_270.mp4"},
{name: "image", path: "fit-in/100x100/demo.png"},
{name: "alpha", path: "fit-in/filters:format(png)/alpha-webm.webm"},
{name: "alpha frame duration", path: "500x/filters:frame(5s):format(png)/alpha-webm.webm"},
{name: "alpha frame position", path: "500x/filters:frame(0.5):format(png)/alpha-webm.webm"},
{name: "alpha seek duration", path: "500x/filters:seek(5s):format(png)/alpha-webm.webm"},
{name: "alpha seek position", path: "500x/filters:seek(0.5):format(png)/alpha-webm.webm"},
{name: "corrupted", path: "fit-in/100x100/corrupt/everybody-betray-me.mkv", expectCode: 406},
{name: "no cover meta", path: "meta/no_cover.mp3"},
{name: "no cover 406", path: "fit-in/100x100/no_cover.mp3", expectCode: 406},
}, WithDebug(true), WithLogger(zap.NewExample()))
doGoldenTests(t, filepath.Join(testDataDir, "golden/result-fallback-image"), []test{
{name: "corrupted with fallback image", path: "fit-in/100x100/corrupt/everybody-betray-me.mkv", expectCode: 406},
{name: "corrupted with fallback image", path: "filters:seek(0.1)/no_cover.mp3", expectCode: 406},
}, WithDebug(false), WithLogger(zap.NewExample()), WithFallbackImage("demo.png"))
}
func doGoldenTests(t *testing.T, resultDir string, tests []test, opts ...Option) {
resStorage := filestorage.New(resultDir,
filestorage.WithSaveErrIfExists(true))
fileLoader := filestorage.New(testDataDir)
loaders := []imagor.Loader{
fileLoader,
loaderFunc(func(r *http.Request, image string) (blob *imagor.Blob, err error) {
image, _ = fileLoader.Path(image)
return imagor.NewBlob(func() (reader io.ReadCloser, size int64, err error) {
// force read full file by 0 size
reader, err = os.Open(image)
return
}), nil
}),
}
for i, loader := range loaders {
app := imagor.New(
imagor.WithLoaders(loaderFunc(func(r *http.Request, image string) (blob *imagor.Blob, err error) {
if strings.HasPrefix(image, "corrupt/") {
image, _ = fileLoader.Path(strings.TrimPrefix(image, "corrupt/"))
return imagor.NewBlob(func() (reader io.ReadCloser, size int64, err error) {
file, err := os.Open(image)
// truncate so it corrupt
reader = &readCloser{
Reader: io.LimitReader(file, 1024),
Closer: file,
}
return
}), nil
}
return nil, imagor.ErrNotFound
}), loader),
imagor.WithUnsafe(true),
imagor.WithDebug(true),
imagor.WithLogger(zap.NewExample()),
imagor.WithProcessors(NewProcessor(opts...), vipsprocessor.NewProcessor()),
)
require.NoError(t, app.Startup(context.Background()))
t.Cleanup(func() {
assert.NoError(t, app.Shutdown(context.Background()))
})
for _, tt := range tests {
t.Run(fmt.Sprintf("%s-%d", tt.name, i+1), func(t *testing.T) {
w := httptest.NewRecorder()
ctx, cancel := context.WithCancel(context.Background())
req := httptest.NewRequest(
http.MethodGet, fmt.Sprintf("/unsafe/%s", tt.path), nil).WithContext(ctx)
app.ServeHTTP(w, req)
cancel()
if tt.expectCode == 0 {
assert.Equal(t, 200, w.Code)
} else {
assert.Equal(t, tt.expectCode, w.Code)
}
b := imagor.NewBlobFromBytes(w.Body.Bytes())
path := tt.path
if strings.HasPrefix(path, "meta/") {
path += ".json"
}
_ = resStorage.Put(context.Background(), path, b)
path = filepath.Join(resultDir, imagorpath.Normalize(path, nil))
bc := imagor.NewBlobFromFile(path)
buf, err := bc.ReadAll()
require.NoError(t, err)
if reflect.DeepEqual(buf, w.Body.Bytes()) {
return
}
img1, err := vips.NewImageFromBuffer(buf, nil)
require.NoError(t, err)
img2, err := vips.NewImageFromBuffer(w.Body.Bytes(), nil)
require.NoError(t, err)
require.Equal(t, img1.Width(), img2.Width(), "width mismatch")
require.Equal(t, img1.Height(), img2.Height(), "height mismatch")
buf1, err := img1.WebpsaveBuffer(nil)
require.NoError(t, err)
buf2, err := img2.WebpsaveBuffer(nil)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(buf1, buf2), "image mismatch")
})
}
}
}
type loaderFunc func(r *http.Request, image string) (blob *imagor.Blob, err error)
func (f loaderFunc) Get(r *http.Request, image string) (*imagor.Blob, error) {
return f(r, image)
}
type readCloser struct {
io.Reader
io.Closer
}

BIN
testdata/alpha-webm.webm vendored Normal file

Binary file not shown.

BIN
testdata/black.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

BIN
testdata/demo.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
testdata/demo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
testdata/demo2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
testdata/demo3.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
testdata/everybody-betray-me.mkv vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
testdata/golden/export/macabre.mp4-1.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
testdata/golden/export/macabre.mp4-5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
testdata/golden/export/macabre.mp4.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
testdata/golden/export/schizo.flv-1.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
testdata/golden/export/schizo.flv-10.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
testdata/golden/export/schizo.flv-5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
testdata/golden/export/schizo.flv.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
testdata/golden/export/schizo_0.mp4.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
testdata/golden/export/schizo_90.mp4.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":1,"duration":3924,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}

View file

@ -0,0 +1 @@
{"orientation":0,"duration":13536,"has_video":false,"has_audio":true}

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