remove video specific stuff, base off of video, and add support for HTML
|
|
@ -71,7 +71,6 @@ RUN go mod download
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then go test ./...; fi
|
||||
RUN go build -o ${GOPATH}/bin/imagorvideo ./cmd/imagorvideo/main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
imagorvideoextended "git.cef.icu/CEF/imagorextended"
|
||||
"github.com/cshum/imagor/config"
|
||||
"github.com/cshum/imagor/config/awsconfig"
|
||||
"github.com/cshum/imagor/config/gcloudconfig"
|
||||
|
|
@ -12,6 +13,7 @@ import (
|
|||
func main() {
|
||||
var server = config.CreateServer(
|
||||
os.Args[1:],
|
||||
imagorvideoextended.Config,
|
||||
imagorvideo.Config,
|
||||
vipsconfig.WithVips,
|
||||
awsconfig.WithAWS,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package imagorvideo
|
||||
package imagorvideoextended
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
|
@ -9,14 +9,10 @@ import (
|
|||
// Config imagorvideo config.Option
|
||||
func Config(fs *flag.FlagSet, cb func() (*zap.Logger, bool)) imagor.Option {
|
||||
var (
|
||||
ffmpegFallbackImage = fs.String("ffmpeg-fallback-image", "",
|
||||
"FFmpeg fallback image on processing error. Supports image path enabled by loaders or storages")
|
||||
|
||||
logger, isDebug = cb()
|
||||
)
|
||||
return imagor.WithProcessors(
|
||||
NewProcessor(
|
||||
WithFallbackImage(*ffmpegFallbackImage),
|
||||
WithLogger(logger),
|
||||
WithDebug(isDebug),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package imagorvideo
|
||||
package imagorvideoextended
|
||||
|
||||
import (
|
||||
"github.com/cshum/imagor"
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
package ffmpeg
|
||||
|
||||
// #include "ffmpeg.h"
|
||||
import "C"
|
||||
import (
|
||||
"github.com/cshum/imagor/vips/pointer"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//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))
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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())
|
||||
}
|
||||
413
ffmpeg/ffmpeg.c
|
|
@ -1,413 +0,0 @@
|
|||
#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) {
|
||||
uint8_t *display_matrix = av_stream_get_side_data(video_stream, AV_PKT_DATA_DISPLAYMATRIX, NULL);
|
||||
double theta = 0;
|
||||
if (display_matrix) {
|
||||
theta = -av_display_rotation_get((int32_t *) display_matrix);
|
||||
}
|
||||
|
||||
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, 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) {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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->buf) {
|
||||
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;
|
||||
}
|
||||
401
ffmpeg/ffmpeg.go
|
|
@ -1,401 +0,0 @@
|
|||
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/imagor/vips/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()
|
||||
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 pkt.buf != nil {
|
||||
C.av_packet_unref(&pkt)
|
||||
}
|
||||
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
|
||||
if pkt.buf != nil {
|
||||
C.av_packet_unref(&pkt)
|
||||
}
|
||||
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()
|
||||
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 pkt.buf != nil {
|
||||
C.av_packet_unref(&pkt)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
#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);
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
package ffmpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cshum/imagor/vips"
|
||||
"github.com/cshum/imagor/vips/pointer"
|
||||
"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.LoadImageFromMemory(buf, meta.Width, meta.Height, bands)
|
||||
require.NoError(t, err)
|
||||
buf, err = img.ExportJpeg(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
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#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);
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#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();
|
||||
5
go.mod
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/cshum/imagorvideo
|
||||
module git.cef.icu/CEF/imagorextended
|
||||
|
||||
go 1.21
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ toolchain go1.21.1
|
|||
|
||||
require (
|
||||
github.com/cshum/imagor v1.4.13
|
||||
github.com/cshum/imagorvideo v0.4.13
|
||||
github.com/gabriel-vasile/mimetype v1.4.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
|
|
@ -18,6 +19,8 @@ require (
|
|||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.11 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
github.com/antchfx/htmlquery v1.3.2 // indirect
|
||||
github.com/antchfx/xpath v1.3.1 // indirect
|
||||
github.com/aws/aws-sdk-go v1.54.20 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
|
|
|
|||
27
go.sum
|
|
@ -16,6 +16,10 @@ cloud.google.com/go/pubsub v1.40.0/go.mod h1:BVJI4sI2FyXp36KFKvFwcfDRDfR8MiLT8mM
|
|||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/antchfx/htmlquery v1.3.2 h1:85YdttVkR1rAY+Oiv/nKI4FCimID+NXhDn82kz3mEvs=
|
||||
github.com/antchfx/htmlquery v1.3.2/go.mod h1:1mbkcEgEarAokJiWhTfr4hR06w/q2ZZjnYLrDt6CTUk=
|
||||
github.com/antchfx/xpath v1.3.1 h1:PNbFuUqHwWl0xRjvUPjJ95Agbmdj2uzzIwmQKgu4oCk=
|
||||
github.com/antchfx/xpath v1.3.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo=
|
||||
github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
|
@ -27,6 +31,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cshum/imagor v1.4.13 h1:BFcSpsTUOJj+Wv5SzDeXa8bhsT/Ehw7EcrFD0UTdpmU=
|
||||
github.com/cshum/imagor v1.4.13/go.mod h1:LHxXgks6Y06GzEHitnlO8vcD5gznxIHWPdvGsnlGpMo=
|
||||
github.com/cshum/imagorvideo v0.4.13 h1:tn+TmdPMvS00XndPX8j/BUMyebErkxXgpJ9CLxNvROE=
|
||||
github.com/cshum/imagorvideo v0.4.13/go.mod h1:y2g2GQst6b1X+jvAcxhDusTcYYGJqp+fSRcUo3l+PxU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -129,6 +135,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
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/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
|
|
@ -151,6 +158,7 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||
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=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
@ -159,12 +167,17 @@ golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E
|
|||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
|
@ -173,16 +186,27 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
|
|||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
|
|
@ -192,8 +216,11 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
|||
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=
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package imagorvideo
|
||||
package imagorvideoextended
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
|
|
|
|||
148
processor.go
|
|
@ -1,16 +1,14 @@
|
|||
package imagorvideo
|
||||
package imagorvideoextended
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/cshum/imagor"
|
||||
"github.com/cshum/imagor/imagorpath"
|
||||
"github.com/cshum/imagorvideo/ffmpeg"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Processor for imagorvideo that implements imagor.Processor interface
|
||||
|
|
@ -33,22 +31,6 @@ func NewProcessor(options ...Option) *Processor {
|
|||
|
||||
// Startup implements imagor.Processor interface
|
||||
func (p *Processor) Startup(_ context.Context) error {
|
||||
ffmpeg.SetLogging(func(level ffmpeg.AVLogLevel, message string) {
|
||||
message = strings.TrimSuffix(message, "\n")
|
||||
switch level {
|
||||
case ffmpeg.AVLogTrace, ffmpeg.AVLogDebug, ffmpeg.AVLogVerbose:
|
||||
p.Logger.Debug("ffmpeg", zap.String("log", message))
|
||||
case ffmpeg.AVLogInfo:
|
||||
p.Logger.Info("ffmpeg", zap.String("log", message))
|
||||
case ffmpeg.AVLogWarning, ffmpeg.AVLogError, ffmpeg.AVLogFatal, ffmpeg.AVLogPanic:
|
||||
p.Logger.Warn("ffmpeg", zap.String("log", message))
|
||||
}
|
||||
})
|
||||
if p.Debug {
|
||||
ffmpeg.SetFFmpegLogLevel(ffmpeg.AVLogDebug)
|
||||
} else {
|
||||
ffmpeg.SetFFmpegLogLevel(ffmpeg.AVLogError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -75,15 +57,15 @@ func (p *Processor) Process(ctx context.Context, in *imagor.Blob, params imagorp
|
|||
}
|
||||
}
|
||||
}()
|
||||
var filters imagorpath.Filters
|
||||
var mime = mimetype.Detect(in.Sniff())
|
||||
if typ := mime.String(); !strings.HasPrefix(typ, "video/") &&
|
||||
!strings.HasPrefix(typ, "audio/") {
|
||||
|
||||
if typ := mime.String(); !strings.HasPrefix(typ, "text/html") {
|
||||
// forward identical for non video nor audio
|
||||
err = imagor.ErrForward{Params: params}
|
||||
out = in
|
||||
return
|
||||
}
|
||||
|
||||
rs, size, err := in.NewReadSeeker()
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -100,95 +82,61 @@ func (p *Processor) Process(ctx context.Context, in *imagor.Blob, params imagorp
|
|||
return
|
||||
}
|
||||
}
|
||||
av, err := ffmpeg.LoadAVContext(rs, size)
|
||||
all, err := in.ReadAll()
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
defer av.Close()
|
||||
meta := av.Metadata()
|
||||
if params.Meta {
|
||||
out = imagor.NewBlobFromJsonMarshal(Metadata{
|
||||
Format: strings.TrimPrefix(mime.Extension(), "."),
|
||||
ContentType: mime.String(),
|
||||
Metadata: meta,
|
||||
})
|
||||
return
|
||||
|
||||
doc, err := htmlquery.Parse(strings.NewReader(string(all[:])))
|
||||
meta := Metadata{
|
||||
Format: strings.TrimPrefix(mime.Extension(), "."),
|
||||
Title: "",
|
||||
Description: "",
|
||||
Image: "",
|
||||
}
|
||||
bands := 3
|
||||
for _, filter := range params.Filters {
|
||||
switch filter.Name {
|
||||
case "format":
|
||||
switch strings.ToLower(filter.Args) {
|
||||
case "webp", "png", "gif":
|
||||
switch mime.Extension() {
|
||||
case ".webm", ".flv", ".mov", ".avi":
|
||||
bands = 4
|
||||
}
|
||||
}
|
||||
case "frame":
|
||||
if ts, e := time.ParseDuration(filter.Args); e == nil {
|
||||
if err = av.SelectDuration(ts); err != nil {
|
||||
return
|
||||
}
|
||||
} else if f, e := strconv.ParseFloat(filter.Args, 64); e == nil {
|
||||
if strings.Contains(filter.Args, ".") {
|
||||
if err = av.SelectPosition(f); err != nil {
|
||||
return
|
||||
}
|
||||
} else if n := int(f); n >= 1 {
|
||||
if err = av.SelectFrame(n); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case "seek":
|
||||
if ts, e := time.ParseDuration(filter.Args); e == nil {
|
||||
if err = av.SeekDuration(ts); err != nil {
|
||||
return
|
||||
}
|
||||
} else if f, e := strconv.ParseFloat(filter.Args, 64); e == nil {
|
||||
if err = av.SeekPosition(f); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "max_frames":
|
||||
n, _ := strconv.Atoi(filter.Args)
|
||||
if err = av.ProcessFrames(n); err != nil {
|
||||
return
|
||||
}
|
||||
metaTags := htmlquery.Find(doc, "//meta[@property]")
|
||||
for _, metaTag := range metaTags {
|
||||
var property = htmlquery.SelectAttr(metaTag, "property")
|
||||
var val = htmlquery.SelectAttr(metaTag, "content")
|
||||
|
||||
switch property {
|
||||
case "og:image":
|
||||
fallthrough
|
||||
case "twitter:image:src":
|
||||
meta.Image = val
|
||||
break
|
||||
|
||||
case "og:title":
|
||||
fallthrough
|
||||
case "twitter:title":
|
||||
meta.Title = val
|
||||
break
|
||||
|
||||
case "twitter:description":
|
||||
fallthrough
|
||||
case "og:description":
|
||||
meta.Description = val
|
||||
}
|
||||
}
|
||||
if meta.Title == "" {
|
||||
title := htmlquery.FindOne(doc, "//title")
|
||||
if title != nil {
|
||||
meta.Title = htmlquery.InnerText(title)
|
||||
} else {
|
||||
meta.Title = in.FilePath()
|
||||
}
|
||||
}
|
||||
|
||||
switch meta.Orientation {
|
||||
case 3:
|
||||
filters = append(filters, imagorpath.Filter{Name: "orient", Args: "180"})
|
||||
case 6:
|
||||
filters = append(filters, imagorpath.Filter{Name: "orient", Args: "270"})
|
||||
case 8:
|
||||
filters = append(filters, imagorpath.Filter{Name: "orient", Args: "90"})
|
||||
}
|
||||
buf, err := av.Export(bands)
|
||||
if err != nil || len(buf) == 0 {
|
||||
if err == nil {
|
||||
err = imagor.ErrUnsupportedFormat
|
||||
}
|
||||
return
|
||||
}
|
||||
out = imagor.NewBlobFromMemory(buf, meta.Width, meta.Height, bands)
|
||||
|
||||
if len(filters) > 0 {
|
||||
params.Filters = append(params.Filters, filters...)
|
||||
params.Path = imagorpath.GeneratePath(params)
|
||||
}
|
||||
err = imagor.ErrForward{Params: params}
|
||||
out = imagor.NewBlobFromJsonMarshal(meta)
|
||||
return
|
||||
}
|
||||
|
||||
// Metadata imagorvideo metadata
|
||||
type Metadata struct {
|
||||
Format string `json:"format"`
|
||||
ContentType string `json:"content_type"`
|
||||
*ffmpeg.Metadata
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
var transPixel = []byte("\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package imagorvideo
|
||||
package imagorvideoextended
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
|||
BIN
testdata/alpha-webm.webm
vendored
BIN
testdata/black.jpg
vendored
|
Before Width: | Height: | Size: 853 B |
BIN
testdata/demo.jpg
vendored
|
Before Width: | Height: | Size: 7.4 KiB |
BIN
testdata/demo.png
vendored
|
Before Width: | Height: | Size: 132 KiB |
BIN
testdata/demo2.jpg
vendored
|
Before Width: | Height: | Size: 12 KiB |
BIN
testdata/demo3.jpg
vendored
|
Before Width: | Height: | Size: 15 KiB |
BIN
testdata/everybody-betray-me.mkv
vendored
BIN
testdata/golden/export/alpha-webm.webm-1.jpg
vendored
|
Before Width: | Height: | Size: 3.1 KiB |
BIN
testdata/golden/export/alpha-webm.webm-10.jpg
vendored
|
Before Width: | Height: | Size: 7.9 KiB |
BIN
testdata/golden/export/alpha-webm.webm-5.jpg
vendored
|
Before Width: | Height: | Size: 7.9 KiB |
BIN
testdata/golden/export/alpha-webm.webm-9999.jpg
vendored
|
Before Width: | Height: | Size: 16 KiB |
BIN
testdata/golden/export/alpha-webm.webm-99999.jpg
vendored
|
Before Width: | Height: | Size: 54 KiB |
BIN
testdata/golden/export/alpha-webm.webm.jpg
vendored
|
Before Width: | Height: | Size: 17 KiB |
BIN
testdata/golden/export/everybody-betray-me.mkv-1.jpg
vendored
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
testdata/golden/export/everybody-betray-me.mkv-5.jpg
vendored
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
BIN
testdata/golden/export/everybody-betray-me.mkv.jpg
vendored
|
Before Width: | Height: | Size: 19 KiB |
BIN
testdata/golden/export/macabre.mp4-1.jpg
vendored
|
Before Width: | Height: | Size: 13 KiB |
BIN
testdata/golden/export/macabre.mp4-10.jpg
vendored
|
Before Width: | Height: | Size: 13 KiB |
BIN
testdata/golden/export/macabre.mp4-5.jpg
vendored
|
Before Width: | Height: | Size: 13 KiB |
BIN
testdata/golden/export/macabre.mp4-9999.jpg
vendored
|
Before Width: | Height: | Size: 14 KiB |
BIN
testdata/golden/export/macabre.mp4-99999.jpg
vendored
|
Before Width: | Height: | Size: 14 KiB |
BIN
testdata/golden/export/macabre.mp4.jpg
vendored
|
Before Width: | Height: | Size: 14 KiB |
BIN
testdata/golden/export/schizo.flv-1.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo.flv-10.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo.flv-5.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo.flv-9999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo.flv-99999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo.flv.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_0.mp4-1.jpg
vendored
|
Before Width: | Height: | Size: 27 KiB |
BIN
testdata/golden/export/schizo_0.mp4-10.jpg
vendored
|
Before Width: | Height: | Size: 27 KiB |
BIN
testdata/golden/export/schizo_0.mp4-5.jpg
vendored
|
Before Width: | Height: | Size: 27 KiB |
BIN
testdata/golden/export/schizo_0.mp4-9999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_0.mp4-99999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_0.mp4.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4-1.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4-10.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4-5.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4-9999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4-99999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_180.mp4.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4-1.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4-10.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4-5.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4-9999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4-99999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_270.mp4.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4-1.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4-10.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4-5.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4-9999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4-99999.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/schizo_90.mp4.jpg
vendored
|
Before Width: | Height: | Size: 28 KiB |
BIN
testdata/golden/export/with_cover.mp3-1.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
BIN
testdata/golden/export/with_cover.mp3-10.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
BIN
testdata/golden/export/with_cover.mp3-5.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
BIN
testdata/golden/export/with_cover.mp3-9999.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
BIN
testdata/golden/export/with_cover.mp3-99999.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
BIN
testdata/golden/export/with_cover.mp3.jpg
vendored
|
Before Width: | Height: | Size: 45 KiB |
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
1
testdata/golden/meta/macabre.mp4-1.meta.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
1
testdata/golden/meta/macabre.mp4-5.meta.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
1
testdata/golden/meta/macabre.mp4.meta.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||
1
testdata/golden/meta/no_cover.mp3.meta.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"orientation":0,"duration":13536,"has_video":false,"has_audio":true}
|
||||
1
testdata/golden/meta/schizo.flv-1.meta.json
vendored
|
|
@ -1 +0,0 @@
|
|||
{"orientation":1,"duration":2560,"width":480,"height":360,"fps":29.97002997002997,"has_video":true,"has_audio":true}
|
||||