feat: imagorvideo init

This commit is contained in:
Adrian Shum 2022-09-08 20:44:10 +08:00
commit 2451fa1b5a
20 changed files with 2601 additions and 0 deletions

54
ffmpeg/callback.go Normal file
View file

@ -0,0 +1,54 @@
package ffmpeg
// #include "ffmpeg.h"
import "C"
import (
"github.com/cshum/imagorvideo/ffmpeg/pointer"
"io"
"unsafe"
)
//export goPacketRead
func goPacketRead(opaque unsafe.Pointer, buf *C.uint8_t, bufSize C.int) C.int {
ctx, ok := pointer.Restore(opaque).(*AVContext)
if !ok || ctx.reader == nil {
return C.int(ErrUnknown)
}
p := (*[1 << 30]byte)(unsafe.Pointer(buf))[:bufSize:bufSize]
n, err := ctx.reader.Read(p)
if err == io.EOF {
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 && ctx.size > 0 {
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 goInterrupt
func goInterrupt(opaque unsafe.Pointer) C.int {
if ctx, ok := pointer.Restore(opaque).(*AVContext); ok {
select {
case <-ctx.context.Done():
return 1
default:
return 0
}
}
return 0
}

39
ffmpeg/errors.go Normal file
View file

@ -0,0 +1,39 @@
package ffmpeg
// #include "ffmpeg.h"
import "C"
import (
"strconv"
"unsafe"
)
type avError int
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 {
if e == ErrNoMem {
return "cannot allocate memory"
}
if e == ErrTooBig {
return "video or cover art size exceeds maximum allowed dimensions"
}
errString := (*C.char)(C.av_malloc(C.AV_ERROR_MAX_STRING_SIZE))
if errString == nil {
return "cannot allocate memory for error string, error code: " + strconv.Itoa(int(e))
}
defer C.av_free(unsafe.Pointer(errString))
C.av_make_error_string(errString, C.AV_ERROR_MAX_STRING_SIZE, C.int(e))
return C.GoString(errString)
}
func (e avError) Error() string {
return "ffmpeg: " + e.errorString()
}

480
ffmpeg/ffmpeg.c Normal file
View file

@ -0,0 +1,480 @@
#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 (flags & INTERRUPT_FLAG) {
fmt_ctx->interrupt_callback.callback = goInterrupt;
fmt_ctx->interrupt_callback.opaque = opaque;
}
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;
}
int encode_frame_to_image(AVFormatContext *fmt_ctx, AVFrame *frame, AVPacket *pkt) {
AVCodec *enc = avcodec_find_encoder(AV_CODEC_ID_PNG);
if (!enc) {
return AVERROR_ENCODER_NOT_FOUND;
}
AVCodecContext *enc_ctx = avcodec_alloc_context3(enc);
if (!enc_ctx) {
return AVERROR(ENOMEM);
}
enc_ctx->width = frame->width;
enc_ctx->height = frame->height;
enc_ctx->pix_fmt = frame->format;
enc_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
enc_ctx->time_base = (AVRational) {1, 1};
enc_ctx->compression_level = INT_MAX;
int err = open_codec(enc_ctx, enc);
if (err < 0) {
avcodec_free_context(&enc_ctx);
return err;
}
err = avcodec_send_frame(enc_ctx, frame);
if (err < 0) {
avcodec_free_context(&enc_ctx);
return err;
}
err = avcodec_receive_packet(enc_ctx, pkt);
avcodec_free_context(&enc_ctx);
return err;
}
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;
}
int64_t find_duration(AVFormatContext *fmt_ctx) {
AVPacket pkt = create_packet();
int err = 0;
int64_t duration = 0;
while (err >= 0) {
err = av_read_frame(fmt_ctx, &pkt);
if (pkt.pts != AV_NOPTS_VALUE) {
AVRational time_base = fmt_ctx->streams[pkt.stream_index]->time_base;
duration = FFMAX(duration, pkt.pts * 1000000000 * time_base.num / time_base.den);
}
av_packet_unref(&pkt);
}
if (err == AVERROR_EOF) {
return duration;
}
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->n = 0;
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);
// thumb_ctx->hist_size = 0;
// thumb_ctx->alpha = 0;
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;
}
static int alpha_check(const AVFrame *frame, const uint64_t flags, const int last_hist_num) {
if (flags & AV_PIX_FMT_FLAG_PAL) {
for (int i = 3; i <= 1023; i += 4) {
if (frame->data[1][i] != 255) {
return 1;
}
}
} else if (flags & AV_PIX_FMT_FLAG_ALPHA && last_hist_num < frame->width * frame->height) {
return 1;
}
return 0;
}
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;
}
}
static AVFrame *get_best_frame(ThumbContext *thumb_ctx) {
struct thumb_frame *t_frame = NULL;
double min_sum_sq_err = DBL_MAX, sum_sq_err = 0;
int i, n = 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;
}
}
thumb_ctx->alpha = alpha_check(thumb_ctx->frames[n].frame, thumb_ctx->desc->flags,
thumb_ctx->frames[n].hist[thumb_ctx->hist_size - 1]);
return thumb_ctx->frames[n].frame;
}
AVFrame *process_frames(ThumbContext *thumb_ctx) {
int i, j, *hist = NULL, n = thumb_ctx->n;
double *median = thumb_ctx->median;
for (j = 0; j < n; j++) {
hist = thumb_ctx->frames[j].hist;
for (i = 0; i < thumb_ctx->hist_size; i++) {
median[i] += (double) hist[i] / n;
}
}
return get_best_frame(thumb_ctx);
}

290
ffmpeg/ffmpeg.go Normal file
View file

@ -0,0 +1,290 @@
package ffmpeg
// #cgo pkg-config: libavformat libavutil libavcodec libswscale
// #cgo CFLAGS: -std=c11
// #cgo LDFLAGS: -lm
// #include "ffmpeg.h"
import "C"
import (
"context"
"github.com/cshum/imagorvideo/ffmpeg/pointer"
"io"
"time"
"unsafe"
)
const (
readPacketFlag = 1
seekPacketFlag = 2
interruptFlag = 3
hasVideo = 1
hasAudio = 2
)
type Metadata struct {
Orientation int `json:"orientation"`
Duration time.Duration `json:"duration,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Title string `json:"title,omitempty"`
Artist string `json:"artist,omitempty"`
HasVideo bool `json:"has_video"`
HasAudio bool `json:"has_audio"`
HasAlpha bool `json:"has_alpha"`
}
type AVContext struct {
context context.Context
opaque unsafe.Pointer
reader io.Reader
seeker io.Seeker
formatContext *C.AVFormatContext
stream *C.AVStream
codecContext *C.AVCodecContext
thumbContext *C.ThumbContext
frame *C.AVFrame
durationInFormat bool
orientation int
size int64
duration time.Duration
width, height int
title, artist string
hasVideo, hasAudio bool
hasFrame, hasAlpha bool
}
func LoadAVContext(ctx context.Context, reader io.Reader, size int64) (*AVContext, error) {
av := &AVContext{
context: ctx,
reader: reader,
size: size,
}
if seeker, ok := reader.(io.Seeker); ok {
av.seeker = seeker
}
flags := C.int(readPacketFlag | interruptFlag)
if av.seeker != nil {
flags |= seekPacketFlag
}
err := createFormatContext(av, flags)
if err != nil {
return nil, err
}
if !av.hasVideo {
return av, nil
}
if err = createDecoder(av); err == ErrTooBig || err == ErrDecoderNotFound {
return av, err
}
return av, nil
}
func (av *AVContext) ExportImage() ([]byte, error) {
if !av.hasFrame {
return nil, nil
}
return encodeFrameImage(av)
}
func (av *AVContext) Close() {
freeFormatContext(av)
}
func (av *AVContext) Metadata() *Metadata {
return &Metadata{
Orientation: av.orientation,
Duration: av.duration,
Width: av.width,
Height: av.height,
Title: av.title,
Artist: av.artist,
HasVideo: av.hasVideo,
HasAudio: av.hasAudio,
HasAlpha: av.hasAlpha,
}
}
func freeFormatContext(av *AVContext) {
C.free_format_context(av.formatContext)
pointer.Unref(av.opaque)
}
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 {
freeFormatContext(av)
}
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 fullDuration(av *AVContext) error {
if av.durationInFormat {
return nil
}
newDuration := time.Duration(C.find_duration(av.formatContext))
if newDuration < 0 {
return avError(newDuration)
}
if newDuration > av.duration {
av.duration = newDuration
}
return nil
}
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)
}
defer C.avcodec_free_context(&av.codecContext)
return createThumbContext(av)
}
func incrementDuration(av *AVContext, frame *C.AVFrame) {
if !av.durationInFormat && 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)
if newDuration > av.duration {
av.duration = newDuration
}
}
}
func populateHistogram(av *AVContext, frames <-chan *C.AVFrame) <-chan struct{} {
done := make(chan struct{})
go func() {
var n C.int
for frame := range frames {
C.populate_histogram(av.thumbContext, n, frame)
n++
}
av.thumbContext.n = n
done <- struct{}{}
close(done)
}()
return done
}
func createThumbContext(av *AVContext) 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)
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)
}
defer C.free_thumb_context(av.thumbContext)
frames := make(chan *C.AVFrame, av.thumbContext.max_frames)
done := populateHistogram(av, frames)
frames <- frame
if pkt.buf != nil {
C.av_packet_unref(&pkt)
}
return populateThumbContext(av, frames, done)
}
func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan struct{}) error {
pkt := C.create_packet()
var frame *C.AVFrame
var err C.int
for i := C.int(1); i < av.thumbContext.max_frames; i++ {
err = C.obtain_next_frame(av.formatContext, av.codecContext, av.stream.index, &pkt, &frame)
if err < 0 {
break
}
incrementDuration(av, frame)
frames <- frame
frame = nil
}
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)
}
return convertFrameToRGB(av)
}
func convertFrameToRGB(av *AVContext) error {
outputFrame := C.convert_frame_to_rgb(C.process_frames(av.thumbContext), av.thumbContext.alpha)
if outputFrame == nil {
return ErrNoMem
}
av.frame = outputFrame
av.hasFrame = true
av.hasAlpha = av.thumbContext.alpha != 0
return nil
}
func encodeFrameImage(av *AVContext) ([]byte, error) {
pkt := C.create_packet()
err := C.encode_frame_to_image(av.formatContext, av.frame, &pkt)
if err < 0 {
return nil, avError(err)
}
p := (*[1 << 30]byte)(unsafe.Pointer(pkt.data))[:pkt.size:pkt.size]
if pkt.buf != nil {
C.av_packet_unref(&pkt)
}
return p, nil
}

72
ffmpeg/ffmpeg.h Normal file
View file

@ -0,0 +1,72 @@
#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 INTERRUPT_FLAG 3
#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, alpha, 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);
int encode_frame_to_image(AVFormatContext *fmt_ctx, AVFrame *frame, AVPacket *pkt);
AVPacket create_packet();
int
obtain_next_frame(AVFormatContext *fmt_ctx, AVCodecContext *dec_ctx, int stream_index, AVPacket *pkt, AVFrame **frame);
int64_t find_duration(AVFormatContext *fmt_ctx);
ThumbContext *create_thumb_context(AVStream *stream, AVFrame *frame);
void free_thumb_context(ThumbContext *thumb_ctx);
AVFrame *process_frames(ThumbContext *thumb_ctx);
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);
extern int goInterrupt(void *opaque);

29
ffmpeg/logging.go Normal file
View file

@ -0,0 +1,29 @@
package ffmpeg
// #include "ffmpeg.h"
import "C"
// AVLogLevel defines the ffmpeg threshold for dumping information to stderr.
type AVLogLevel int
// Possible values for AVLogLevel.
const (
AVLogQuiet AVLogLevel = (iota - 1) * 8
AVLogPanic
AVLogFatal
AVLogError
AVLogWarning
AVLogInfo
AVLogVerbose
AVLogDebug
AVLogTrace
)
func logLevel() AVLogLevel {
return AVLogLevel(C.av_log_get_level())
}
// SetFFmpegLogLevel allows you to change the log level from the default (AVLogInfo).
func SetFFmpegLogLevel(logLevel AVLogLevel) {
C.av_log_set_level(C.int(logLevel))
}

100
ffmpeg/pointer/pointer.go Normal file
View file

@ -0,0 +1,100 @@
package pointer
// #include <stdlib.h>
import "C"
import (
"sync"
"unsafe"
)
const blockSize = 1024
var (
mutex sync.RWMutex
store = map[unsafe.Pointer]interface{}{}
free []unsafe.Pointer
blocks []unsafe.Pointer
)
func allocMem() {
mem := C.malloc(blockSize)
if mem == nil {
panic("can't allocate memory block for C pointers")
}
blocks = append(blocks, mem)
for i := 0; i < blockSize; i++ {
p := unsafe.Pointer(uintptr(mem) + uintptr(blockSize-1-i))
free = append(free, p)
}
}
func getPtr() unsafe.Pointer {
// Generate real fake C pointer.
// This pointer will not store any data, but will be used for indexing
// purposes. Since Go doesn't allow to cast dangling pointer to
// unsafe.Pointer, we do really allocate memory. Why we need indexing? Because
// Go doest allow C code to store pointers to Go data.
if len(free) == 0 {
allocMem()
}
n := len(free) - 1
p := free[n]
free = free[:n]
return p
}
// Save an object in the storage and return an index pointer to it.
func Save(v interface{}) unsafe.Pointer {
if v == nil {
return nil
}
mutex.Lock()
ptr := getPtr()
store[ptr] = v
mutex.Unlock()
return ptr
}
// Restore an object from the storage by its index pointer.
func Restore(ptr unsafe.Pointer) (v interface{}) {
if ptr == nil {
return nil
}
mutex.RLock()
v = store[ptr]
mutex.RUnlock()
return
}
// Unref removes an object from the storage and returns the index pointer to the
// pool for reuse.
func Unref(ptr unsafe.Pointer) {
if ptr == nil {
return
}
mutex.Lock()
if _, ok := store[ptr]; ok {
delete(store, ptr)
free = append(free, ptr)
}
mutex.Unlock()
}
// Clear storage and free all memory
func Clear() {
mutex.Lock()
for p := range store {
delete(store, p)
}
free = nil
for _, p := range blocks {
C.free(p)
}
blocks = nil
mutex.Unlock()
}

View file

@ -0,0 +1,176 @@
package pointer
import (
"sync"
"testing"
"unsafe"
"github.com/stretchr/testify/assert"
)
func TestPointer(t *testing.T) {
t.Cleanup(Clear)
// assert := makeAssert(t)
assert.Len(t, store, 0)
assert.Len(t, free, 0)
assert.Len(t, blocks, 0)
mutex.Lock()
assert.Equal(t, unsafe.Pointer(nil), Save(nil))
assert.Nil(t, Restore(nil))
Unref(nil)
mutex.Unlock()
i1 := Save("foo")
i2 := Save("bar")
i3 := Save("baz")
assert.Len(t, store, 3)
assert.Len(t, free, blockSize-3)
assert.Len(t, blocks, 1)
var x interface{}
x = Restore(i1)
assert.NotNil(t, x)
if s, ok := x.(string); ok {
assert.Equal(t, "foo", s)
} else {
t.Fail()
}
x = Restore(unsafe.Pointer(&x))
assert.Nil(t, x)
x = Restore(i3)
assert.NotNil(t, x)
if s, ok := x.(string); ok {
assert.Equal(t, "baz", s)
} else {
t.Fail()
}
Unref(i3)
x = Restore(i3)
assert.Nil(t, x)
Unref(i2)
Unref(i1)
assert.Len(t, store, 0)
assert.Len(t, free, blockSize)
assert.Len(t, blocks, 1)
i3 = Save("baz")
assert.Len(t, store, 1)
assert.Len(t, free, blockSize-1)
Clear()
assert.Len(t, store, 0)
assert.Len(t, free, 0)
assert.Len(t, blocks, 0)
}
func TestPointerIndexing(t *testing.T) {
t.Cleanup(Clear)
assert.Len(t, store, 0)
assert.Len(t, free, 0)
assert.Len(t, blocks, 0)
i1 := Save("foo")
i2 := Save("bar")
_ = Save("baz")
_ = Save("wibble")
_ = Save("wabble")
assert.Len(t, store, 5)
assert.Len(t, free, blockSize-5)
// Check that when we remove the first items inserted into the map there are
// no subsequent issues
Unref(i1)
Unref(i2)
assert.Len(t, free, blockSize-3)
_ = Save("flim")
ilast := Save("flam")
assert.Len(t, store, 5)
assert.Len(t, free, blockSize-5)
x := Restore(ilast)
assert.NotNil(t, x)
if s, ok := x.(string); ok {
assert.Equal(t, "flam", s)
}
}
func TestCallbacksData(t *testing.T) {
t.Cleanup(Clear)
assert.Len(t, store, 0)
assert.Len(t, free, 0)
assert.Len(t, blocks, 0)
// insert a plain function
i1 := Save(func(v int) int { return v + 1 })
// insert a type "containing" a function, note that it doesn't
// actually have a callable function. Users of the type must
// check that themselves
type flup struct {
Stuff int
Junk func(int, int) error
}
i2 := Save(flup{
Stuff: 55,
})
// did we get a function back
x1 := Restore(i1)
if assert.NotNil(t, x1) {
if f, ok := x1.(func(v int) int); ok {
assert.Equal(t, 2, f(1))
} else {
t.Fatalf("conversion failed")
}
}
// did we get our data structure back
x2 := Restore(i2)
if assert.NotNil(t, x2) {
if d, ok := x2.(flup); ok {
assert.Equal(t, 55, d.Stuff)
assert.Nil(t, d.Junk)
} else {
t.Fatalf("conversion failed")
}
}
}
func BenchmarkPointer(t *testing.B) {
t.Cleanup(Clear)
assert.Len(t, store, 0)
assert.Len(t, free, 0)
assert.Len(t, blocks, 0)
const workers = 1000
var wg sync.WaitGroup
f := func() {
defer wg.Done()
var x interface{}
var i1, i2, i3 unsafe.Pointer
for i := 0; i < t.N/workers; i++ {
i1 = Save("foo")
i2 = Save("bar")
i3 = Save("baz")
x = Restore(i1)
assert.NotNil(t, x)
if s, ok := x.(string); ok {
assert.Equal(t, "foo", s)
} else {
t.Fail()
}
x = Restore(i3)
assert.NotNil(t, x)
if s, ok := x.(string); ok {
assert.Equal(t, "baz", s)
} else {
t.Fail()
}
Unref(i3)
Unref(i2)
Unref(i1)
}
}
for i := 0; i < workers; i++ {
wg.Add(1)
go f()
}
wg.Wait()
assert.Len(t, store, 0)
assert.Len(t, free, len(blocks)*blockSize)
}