/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2001-2002 NaN Holding BV. All rights reserved. */ /** \file * \ingroup imbuf */ #ifdef _WIN32 # include "BLI_winstuff.h" # include # undef AVIIF_KEYFRAME /* redefined in AVI_avi.h */ # undef AVIIF_LIST /* redefined in AVI_avi.h */ # define FIXCC(fcc) \ { \ if (fcc == 0) { \ fcc = mmioFOURCC('N', 'o', 'n', 'e'); \ } \ if (fcc == BI_RLE8) { \ fcc = mmioFOURCC('R', 'l', 'e', '8'); \ } \ } \ (void)0 #endif #include #include #include #include #include #include #ifndef _WIN32 # include #else # include #endif #include "BLI_path_util.h" #include "BLI_string.h" #include "BLI_threads.h" #include "BLI_utildefines.h" #include "DNA_scene_types.h" #include "MEM_guardedalloc.h" #ifdef WITH_AVI # include "AVI_avi.h" #endif #include "IMB_imbuf.h" #include "IMB_imbuf_types.h" #include "IMB_colormanagement.h" #include "IMB_colormanagement_intern.h" #include "IMB_anim.h" #include "IMB_indexer.h" #include "IMB_metadata.h" #ifdef WITH_FFMPEG # include "BKE_global.h" /* ENDIAN_ORDER */ # include # include # include # include # include # include "ffmpeg_compat.h" #endif /* WITH_FFMPEG */ int ismovie(const char *UNUSED(filepath)) { return 0; } /* never called, just keep the linker happy */ static int startmovie(struct anim *UNUSED(anim)) { return 1; } static ImBuf *movie_fetchibuf(struct anim *UNUSED(anim), int UNUSED(position)) { return NULL; } static void free_anim_movie(struct anim *UNUSED(anim)) { /* pass */ } #if defined(_WIN32) # define PATHSEPARATOR '\\' #else # define PATHSEPARATOR '/' #endif static int an_stringdec(const char *string, char *head, char *tail, ushort *numlen) { ushort len, nume, nums = 0; short i; bool found = false; len = strlen(string); nume = len; for (i = len - 1; i >= 0; i--) { if (string[i] == PATHSEPARATOR) { break; } if (isdigit(string[i])) { if (found) { nums = i; } else { nume = i; nums = i; found = true; } } else { if (found) { break; } } } if (found) { strcpy(tail, &string[nume + 1]); strcpy(head, string); head[nums] = '\0'; *numlen = nume - nums + 1; return (int)atoi(&(string[nums])); } tail[0] = '\0'; strcpy(head, string); *numlen = 0; return true; } static void an_stringenc(char *string, const char *head, const char *tail, ushort numlen, int pic) { BLI_path_sequence_encode(string, head, tail, numlen, pic); } #ifdef WITH_AVI static void free_anim_avi(struct anim *anim) { # if defined(_WIN32) int i; # endif if (anim == NULL) { return; } if (anim->avi == NULL) { return; } AVI_close(anim->avi); MEM_freeN(anim->avi); anim->avi = NULL; # if defined(_WIN32) if (anim->pgf) { AVIStreamGetFrameClose(anim->pgf); anim->pgf = NULL; } for (i = 0; i < anim->avistreams; i++) { AVIStreamRelease(anim->pavi[i]); } anim->avistreams = 0; if (anim->pfileopen) { AVIFileRelease(anim->pfile); anim->pfileopen = 0; AVIFileExit(); } # endif anim->duration_in_frames = 0; } #endif /* WITH_AVI */ #ifdef WITH_FFMPEG static void free_anim_ffmpeg(struct anim *anim); #endif void IMB_free_anim(struct anim *anim) { if (anim == NULL) { printf("free anim, anim == NULL\n"); return; } free_anim_movie(anim); #ifdef WITH_AVI free_anim_avi(anim); #endif #ifdef WITH_FFMPEG free_anim_ffmpeg(anim); #endif IMB_free_indices(anim); IMB_metadata_free(anim->metadata); MEM_freeN(anim); } void IMB_close_anim(struct anim *anim) { if (anim == NULL) { return; } IMB_free_anim(anim); } void IMB_close_anim_proxies(struct anim *anim) { if (anim == NULL) { return; } IMB_free_indices(anim); } struct IDProperty *IMB_anim_load_metadata(struct anim *anim) { switch (anim->curtype) { case ANIM_FFMPEG: { #ifdef WITH_FFMPEG AVDictionaryEntry *entry = NULL; BLI_assert(anim->pFormatCtx != NULL); av_log(anim->pFormatCtx, AV_LOG_DEBUG, "METADATA FETCH\n"); while (true) { entry = av_dict_get(anim->pFormatCtx->metadata, "", entry, AV_DICT_IGNORE_SUFFIX); if (entry == NULL) { break; } /* Delay creation of the property group until there is actual metadata to put in there. */ IMB_metadata_ensure(&anim->metadata); IMB_metadata_set_field(anim->metadata, entry->key, entry->value); } #endif break; } case ANIM_SEQUENCE: case ANIM_AVI: case ANIM_MOVIE: /* TODO */ break; case ANIM_NONE: default: break; } return anim->metadata; } struct anim *IMB_open_anim(const char *name, int ib_flags, int streamindex, char colorspace[IM_MAX_SPACE]) { struct anim *anim; BLI_assert(!BLI_path_is_rel(name)); anim = (struct anim *)MEM_callocN(sizeof(struct anim), "anim struct"); if (anim != NULL) { if (colorspace) { colorspace_set_default_role(colorspace, IM_MAX_SPACE, COLOR_ROLE_DEFAULT_BYTE); BLI_strncpy(anim->colorspace, colorspace, sizeof(anim->colorspace)); } else { colorspace_set_default_role( anim->colorspace, sizeof(anim->colorspace), COLOR_ROLE_DEFAULT_BYTE); } BLI_strncpy(anim->name, name, sizeof(anim->name)); anim->ib_flags = ib_flags; anim->streamindex = streamindex; } return anim; } bool IMB_anim_can_produce_frames(const struct anim *anim) { #if !(defined(WITH_AVI) || defined(WITH_FFMPEG)) UNUSED_VARS(anim); #endif #ifdef WITH_AVI if (anim->avi != NULL) { return true; } #endif #ifdef WITH_FFMPEG if (anim->pCodecCtx != NULL) { return true; } #endif return false; } void IMB_suffix_anim(struct anim *anim, const char *suffix) { BLI_strncpy(anim->suffix, suffix, sizeof(anim->suffix)); } #ifdef WITH_AVI static int startavi(struct anim *anim) { AviError avierror; # if defined(_WIN32) HRESULT hr; int i, firstvideo = -1; int streamcount; BYTE abFormat[1024]; LONG l; LPBITMAPINFOHEADER lpbi; AVISTREAMINFO avis; streamcount = anim->streamindex; # endif anim->avi = MEM_callocN(sizeof(AviMovie), "animavi"); if (anim->avi == NULL) { printf("Can't open avi: %s\n", anim->name); return -1; } avierror = AVI_open_movie(anim->name, anim->avi); # if defined(_WIN32) if (avierror == AVI_ERROR_COMPRESSION) { AVIFileInit(); hr = AVIFileOpen(&anim->pfile, anim->name, OF_READ, 0L); if (hr == 0) { anim->pfileopen = 1; for (i = 0; i < MAXNUMSTREAMS; i++) { if (AVIFileGetStream(anim->pfile, &anim->pavi[i], 0L, i) != AVIERR_OK) { break; } AVIStreamInfo(anim->pavi[i], &avis, sizeof(avis)); if ((avis.fccType == streamtypeVIDEO) && (firstvideo == -1)) { if (streamcount > 0) { streamcount--; continue; } anim->pgf = AVIStreamGetFrameOpen(anim->pavi[i], NULL); if (anim->pgf) { firstvideo = i; /* get stream length */ anim->avi->header->TotalFrames = AVIStreamLength(anim->pavi[i]); /* get information about images inside the stream */ l = sizeof(abFormat); AVIStreamReadFormat(anim->pavi[i], 0, &abFormat, &l); lpbi = (LPBITMAPINFOHEADER)abFormat; anim->avi->header->Height = lpbi->biHeight; anim->avi->header->Width = lpbi->biWidth; } else { FIXCC(avis.fccHandler); FIXCC(avis.fccType); printf("Can't find AVI decoder for type : %4.4hs/%4.4hs\n", (LPSTR)&avis.fccType, (LPSTR)&avis.fccHandler); } } } /* register number of opened avistreams */ anim->avistreams = i; /* * Couldn't get any video streams out of this file */ if ((anim->avistreams == 0) || (firstvideo == -1)) { avierror = AVI_ERROR_FORMAT; } else { avierror = AVI_ERROR_NONE; anim->firstvideo = firstvideo; } } else { AVIFileExit(); } } # endif if (avierror != AVI_ERROR_NONE) { AVI_print_error(avierror); printf("Error loading avi: %s\n", anim->name); free_anim_avi(anim); return -1; } anim->duration_in_frames = anim->avi->header->TotalFrames; anim->start_offset = 0.0f; anim->params = NULL; anim->x = anim->avi->header->Width; anim->y = anim->avi->header->Height; anim->interlacing = 0; anim->orientation = 0; anim->framesize = anim->x * anim->y * 4; anim->cur_position = 0; # if 0 printf("x:%d y:%d size:%d interlace:%d dur:%d\n", anim->x, anim->y, anim->framesize, anim->interlacing, anim->duration_in_frames); # endif return 0; } #endif /* WITH_AVI */ #ifdef WITH_AVI static ImBuf *avi_fetchibuf(struct anim *anim, int position) { ImBuf *ibuf = NULL; int *tmp; int y; if (anim == NULL) { return NULL; } # if defined(_WIN32) if (anim->avistreams) { LPBITMAPINFOHEADER lpbi; if (anim->pgf) { lpbi = AVIStreamGetFrame(anim->pgf, position + AVIStreamStart(anim->pavi[anim->firstvideo])); if (lpbi) { ibuf = IMB_ibImageFromMemory( (const uchar *)lpbi, 100, IB_rect, anim->colorspace, ""); /* Oh brother... */ } } } else # endif { ibuf = IMB_allocImBuf(anim->x, anim->y, 24, IB_rect); tmp = AVI_read_frame( anim->avi, AVI_FORMAT_RGB32, position, AVI_get_stream(anim->avi, AVIST_VIDEO, 0)); if (tmp == NULL) { printf("Error reading frame from AVI: '%s'\n", anim->name); IMB_freeImBuf(ibuf); return NULL; } for (y = 0; y < anim->y; y++) { memcpy(&(ibuf->rect)[((anim->y - y) - 1) * anim->x], &tmp[y * anim->x], anim->x * 4); } MEM_freeN(tmp); } ibuf->rect_colorspace = colormanage_colorspace_get_named(anim->colorspace); return ibuf; } #endif /* WITH_AVI */ #ifdef WITH_FFMPEG static int startffmpeg(struct anim *anim) { int i, video_stream_index; const AVCodec *pCodec; AVFormatContext *pFormatCtx = NULL; AVCodecContext *pCodecCtx; AVRational frame_rate; AVStream *video_stream; int frs_num; double frs_den; int streamcount; /* The following for color space determination */ int srcRange, dstRange, brightness, contrast, saturation; int *table; const int *inv_table; if (anim == NULL) { return (-1); } streamcount = anim->streamindex; if (avformat_open_input(&pFormatCtx, anim->name, NULL, NULL) != 0) { return -1; } if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { avformat_close_input(&pFormatCtx); return -1; } av_dump_format(pFormatCtx, 0, anim->name, 0); /* Find the video stream */ video_stream_index = -1; for (i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { if (streamcount > 0) { streamcount--; continue; } video_stream_index = i; break; } } if (video_stream_index == -1) { avformat_close_input(&pFormatCtx); return -1; } video_stream = pFormatCtx->streams[video_stream_index]; /* Find the decoder for the video stream */ pCodec = avcodec_find_decoder(video_stream->codecpar->codec_id); if (pCodec == NULL) { avformat_close_input(&pFormatCtx); return -1; } pCodecCtx = avcodec_alloc_context3(NULL); avcodec_parameters_to_context(pCodecCtx, video_stream->codecpar); pCodecCtx->workaround_bugs = FF_BUG_AUTODETECT; if (pCodec->capabilities & AV_CODEC_CAP_AUTO_THREADS) { pCodecCtx->thread_count = 0; } else { pCodecCtx->thread_count = BLI_system_thread_count(); } if (pCodec->capabilities & AV_CODEC_CAP_FRAME_THREADS) { pCodecCtx->thread_type = FF_THREAD_FRAME; } else if (pCodec->capabilities & AV_CODEC_CAP_SLICE_THREADS) { pCodecCtx->thread_type = FF_THREAD_SLICE; } if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { avformat_close_input(&pFormatCtx); return -1; } if (pCodecCtx->pix_fmt == AV_PIX_FMT_NONE) { avcodec_free_context(&anim->pCodecCtx); avformat_close_input(&pFormatCtx); return -1; } double video_start = 0; double pts_time_base = av_q2d(video_stream->time_base); if (video_stream->start_time != AV_NOPTS_VALUE) { video_start = video_stream->start_time * pts_time_base; } frame_rate = av_guess_frame_rate(pFormatCtx, video_stream, NULL); anim->duration_in_frames = 0; /* Take from the stream if we can. */ if (video_stream->nb_frames != 0) { anim->duration_in_frames = video_stream->nb_frames; /* Sanity check on the detected duration. This is to work around corruption like reported in * T68091. */ if (frame_rate.den != 0 && pFormatCtx->duration > 0) { double stream_sec = anim->duration_in_frames * av_q2d(frame_rate); double container_sec = pFormatCtx->duration / (double)AV_TIME_BASE; if (stream_sec > 4.0 * container_sec) { /* The stream is significantly longer than the container duration, which is * suspicious. */ anim->duration_in_frames = 0; } } } /* Fall back to manually estimating the video stream duration. * This is because the video stream duration can be shorter than the pFormatCtx->duration. */ if (anim->duration_in_frames == 0) { double stream_dur; if (video_stream->duration != AV_NOPTS_VALUE) { stream_dur = video_stream->duration * pts_time_base; } else { double audio_start = 0; /* Find audio stream to guess the duration of the video. * Sometimes the audio AND the video stream have a start offset. * The difference between these is the offset we want to use to * calculate the video duration. */ for (i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { AVStream *audio_stream = pFormatCtx->streams[i]; if (audio_stream->start_time != AV_NOPTS_VALUE) { audio_start = audio_stream->start_time * av_q2d(audio_stream->time_base); } break; } } if (video_start > audio_start) { stream_dur = (double)pFormatCtx->duration / AV_TIME_BASE - (video_start - audio_start); } else { /* The video stream starts before or at the same time as the audio stream! * We have to assume that the video stream is as long as the full pFormatCtx->duration. */ stream_dur = (double)pFormatCtx->duration / AV_TIME_BASE; } } anim->duration_in_frames = (int)(stream_dur * av_q2d(frame_rate) + 0.5f); } frs_num = frame_rate.num; frs_den = frame_rate.den; frs_den *= AV_TIME_BASE; while (frs_num % 10 == 0 && frs_den >= 2.0 && frs_num > 10) { frs_num /= 10; frs_den /= 10; } anim->frs_sec = frs_num; anim->frs_sec_base = frs_den; /* Save the relative start time for the video. IE the start time in relation to where playback * starts. */ anim->start_offset = video_start; anim->params = 0; anim->x = pCodecCtx->width; anim->y = pCodecCtx->height; anim->pFormatCtx = pFormatCtx; anim->pCodecCtx = pCodecCtx; anim->pCodec = pCodec; anim->videoStream = video_stream_index; anim->interlacing = 0; anim->orientation = 0; anim->framesize = anim->x * anim->y * 4; anim->cur_position = 0; anim->cur_frame_final = 0; anim->cur_pts = -1; anim->cur_key_frame_pts = -1; anim->cur_packet = av_packet_alloc(); anim->cur_packet->stream_index = -1; anim->pFrame = av_frame_alloc(); anim->pFrame_backup = av_frame_alloc(); anim->pFrame_backup_complete = false; anim->pFrame_complete = false; anim->pFrameDeinterlaced = av_frame_alloc(); anim->pFrameRGB = av_frame_alloc(); anim->pFrameRGB->format = AV_PIX_FMT_RGBA; anim->pFrameRGB->width = anim->x; anim->pFrameRGB->height = anim->y; if (av_frame_get_buffer(anim->pFrameRGB, 0) < 0) { fprintf(stderr, "Could not allocate frame data.\n"); avcodec_free_context(&anim->pCodecCtx); avformat_close_input(&anim->pFormatCtx); av_packet_free(&anim->cur_packet); av_frame_free(&anim->pFrameRGB); av_frame_free(&anim->pFrameDeinterlaced); av_frame_free(&anim->pFrame); av_frame_free(&anim->pFrame_backup); anim->pCodecCtx = NULL; return -1; } if (av_image_get_buffer_size(AV_PIX_FMT_RGBA, anim->x, anim->y, 1) != anim->x * anim->y * 4) { fprintf(stderr, "ffmpeg has changed alloc scheme ... ARGHHH!\n"); avcodec_free_context(&anim->pCodecCtx); avformat_close_input(&anim->pFormatCtx); av_packet_free(&anim->cur_packet); av_frame_free(&anim->pFrameRGB); av_frame_free(&anim->pFrameDeinterlaced); av_frame_free(&anim->pFrame); av_frame_free(&anim->pFrame_backup); anim->pCodecCtx = NULL; return -1; } if (anim->ib_flags & IB_animdeinterlace) { av_image_fill_arrays(anim->pFrameDeinterlaced->data, anim->pFrameDeinterlaced->linesize, MEM_callocN(av_image_get_buffer_size(anim->pCodecCtx->pix_fmt, anim->pCodecCtx->width, anim->pCodecCtx->height, 1), "ffmpeg deinterlace"), anim->pCodecCtx->pix_fmt, anim->pCodecCtx->width, anim->pCodecCtx->height, 1); } anim->img_convert_ctx = sws_getContext(anim->x, anim->y, anim->pCodecCtx->pix_fmt, anim->x, anim->y, AV_PIX_FMT_RGBA, SWS_BILINEAR | SWS_PRINT_INFO | SWS_FULL_CHR_H_INT, NULL, NULL, NULL); if (!anim->img_convert_ctx) { fprintf(stderr, "Can't transform color space??? Bailing out...\n"); avcodec_free_context(&anim->pCodecCtx); avformat_close_input(&anim->pFormatCtx); av_packet_free(&anim->cur_packet); av_frame_free(&anim->pFrameRGB); av_frame_free(&anim->pFrameDeinterlaced); av_frame_free(&anim->pFrame); av_frame_free(&anim->pFrame_backup); anim->pCodecCtx = NULL; return -1; } /* Try do detect if input has 0-255 YCbCR range (JFIF Jpeg MotionJpeg) */ if (!sws_getColorspaceDetails(anim->img_convert_ctx, (int **)&inv_table, &srcRange, &table, &dstRange, &brightness, &contrast, &saturation)) { srcRange = srcRange || anim->pCodecCtx->color_range == AVCOL_RANGE_JPEG; inv_table = sws_getCoefficients(anim->pCodecCtx->colorspace); if (sws_setColorspaceDetails(anim->img_convert_ctx, (int *)inv_table, srcRange, table, dstRange, brightness, contrast, saturation)) { fprintf(stderr, "Warning: Could not set libswscale colorspace details.\n"); } } else { fprintf(stderr, "Warning: Could not set libswscale colorspace details.\n"); } return 0; } static double ffmpeg_steps_per_frame_get(struct anim *anim) { AVStream *v_st = anim->pFormatCtx->streams[anim->videoStream]; AVRational time_base = v_st->time_base; AVRational frame_rate = av_guess_frame_rate(anim->pFormatCtx, v_st, NULL); return av_q2d(av_inv_q(av_mul_q(frame_rate, time_base))); } /* Store backup frame. * With VFR movies, if PTS is not matched perfectly, scanning continues to look for next PTS. * It is likely to overshoot and scanning stops. Having previous frame backed up, it is possible * to use it when overshoot happens. */ static void ffmpeg_double_buffer_backup_frame_store(struct anim *anim, int64_t pts_to_search) { /* `anim->pFrame` is beyond `pts_to_search`. Don't store it. */ if (anim->pFrame_backup_complete && anim->cur_pts >= pts_to_search) { return; } if (!anim->pFrame_complete) { return; } if (anim->pFrame_backup_complete) { av_frame_unref(anim->pFrame_backup); } av_frame_move_ref(anim->pFrame_backup, anim->pFrame); anim->pFrame_backup_complete = true; } /* Free stored backup frame. */ static void ffmpeg_double_buffer_backup_frame_clear(struct anim *anim) { if (anim->pFrame_backup_complete) { av_frame_unref(anim->pFrame_backup); } anim->pFrame_backup_complete = false; } /* Return recently decoded frame. If it does not exist, return frame from backup buffer. */ static AVFrame *ffmpeg_double_buffer_frame_fallback_get(struct anim *anim) { av_log(anim->pFormatCtx, AV_LOG_ERROR, "DECODE UNHAPPY: PTS not matched!\n"); if (anim->pFrame_complete) { return anim->pFrame; } if (anim->pFrame_backup_complete) { return anim->pFrame_backup; } return NULL; } /* postprocess the image in anim->pFrame and do color conversion * and deinterlacing stuff. * * Output is anim->cur_frame_final */ static void ffmpeg_postprocess(struct anim *anim, AVFrame *input) { ImBuf *ibuf = anim->cur_frame_final; int filter_y = 0; /* This means the data wasn't read properly, * this check stops crashing */ if (input->data[0] == 0 && input->data[1] == 0 && input->data[2] == 0 && input->data[3] == 0) { fprintf(stderr, "ffmpeg_fetchibuf: " "data not read properly...\n"); return; } av_log(anim->pFormatCtx, AV_LOG_DEBUG, " POSTPROC: AVFrame planes: %p %p %p %p\n", input->data[0], input->data[1], input->data[2], input->data[3]); if (anim->ib_flags & IB_animdeinterlace) { if (av_image_deinterlace(anim->pFrameDeinterlaced, anim->pFrame, anim->pCodecCtx->pix_fmt, anim->pCodecCtx->width, anim->pCodecCtx->height) < 0) { filter_y = true; } else { input = anim->pFrameDeinterlaced; } } sws_scale(anim->img_convert_ctx, (const uint8_t *const *)input->data, input->linesize, 0, anim->y, anim->pFrameRGB->data, anim->pFrameRGB->linesize); /* Copy the valid bytes from the aligned buffer vertically flipped into ImBuf */ int aligned_stride = anim->pFrameRGB->linesize[0]; const uint8_t *const src[4] = { anim->pFrameRGB->data[0] + (anim->y - 1) * aligned_stride, 0, 0, 0}; /* NOTE: Negative linesize is used to copy and flip image at once with function * `av_image_copy_to_buffer`. This could cause issues in future and image may need to be flipped * explicitly. */ const int src_linesize[4] = {-anim->pFrameRGB->linesize[0], 0, 0, 0}; int dst_size = av_image_get_buffer_size( anim->pFrameRGB->format, anim->pFrameRGB->width, anim->pFrameRGB->height, 1); av_image_copy_to_buffer( (uint8_t *)ibuf->rect, dst_size, src, src_linesize, AV_PIX_FMT_RGBA, anim->x, anim->y, 1); if (filter_y) { IMB_filtery(ibuf); } } static void final_frame_log(struct anim *anim, int64_t frame_pts_start, int64_t frame_pts_end, const char *str) { av_log(anim->pFormatCtx, AV_LOG_INFO, "DECODE HAPPY: %s frame PTS range %" PRId64 " - %" PRId64 ".\n", str, frame_pts_start, frame_pts_end); } static bool ffmpeg_pts_isect(int64_t pts_start, int64_t pts_end, int64_t pts_to_search) { return pts_start <= pts_to_search && pts_to_search < pts_end; } /* Return frame that matches `pts_to_search`, NULL if matching frame does not exist. */ static AVFrame *ffmpeg_frame_by_pts_get(struct anim *anim, int64_t pts_to_search) { /* NOTE: `frame->pts + frame->pkt_duration` does not always match pts of next frame. * See footage from T86361. Here it is OK to use, because PTS must match current or backup frame. * If there is no current frame, return NULL. */ if (!anim->pFrame_complete) { return NULL; } const bool backup_frame_ready = anim->pFrame_backup_complete; const int64_t recent_start = av_get_pts_from_frame(anim->pFrame); const int64_t recent_end = recent_start + anim->pFrame->pkt_duration; const int64_t backup_start = backup_frame_ready ? av_get_pts_from_frame(anim->pFrame_backup) : 0; AVFrame *best_frame = NULL; if (ffmpeg_pts_isect(recent_start, recent_end, pts_to_search)) { final_frame_log(anim, recent_start, recent_end, "Recent"); best_frame = anim->pFrame; } else if (backup_frame_ready && ffmpeg_pts_isect(backup_start, recent_start, pts_to_search)) { final_frame_log(anim, backup_start, recent_start, "Backup"); best_frame = anim->pFrame_backup; } return best_frame; } static void ffmpeg_decode_store_frame_pts(struct anim *anim) { anim->cur_pts = av_get_pts_from_frame(anim->pFrame); if (anim->pFrame->key_frame) { anim->cur_key_frame_pts = anim->cur_pts; } av_log(anim->pFormatCtx, AV_LOG_DEBUG, " FRAME DONE: cur_pts=%" PRId64 ", guessed_pts=%" PRId64 "\n", av_get_pts_from_frame(anim->pFrame), (int64_t)anim->cur_pts); } static int ffmpeg_read_video_frame(struct anim *anim, AVPacket *packet) { int ret = 0; while ((ret = av_read_frame(anim->pFormatCtx, packet)) >= 0) { if (packet->stream_index == anim->videoStream) { break; } av_packet_unref(packet); packet->stream_index = -1; } return ret; } /* decode one video frame also considering the packet read into cur_packet */ static int ffmpeg_decode_video_frame(struct anim *anim) { av_log(anim->pFormatCtx, AV_LOG_DEBUG, " DECODE VIDEO FRAME\n"); /* Sometimes, decoder returns more than one frame per sent packet. Check if frames are available. * This frames must be read, otherwise decoding will fail. See T91405. */ anim->pFrame_complete = avcodec_receive_frame(anim->pCodecCtx, anim->pFrame) == 0; if (anim->pFrame_complete) { av_log(anim->pFormatCtx, AV_LOG_DEBUG, " DECODE FROM CODEC BUFFER\n"); ffmpeg_decode_store_frame_pts(anim); return 1; } int rval = 0; if (anim->cur_packet->stream_index == anim->videoStream) { av_packet_unref(anim->cur_packet); anim->cur_packet->stream_index = -1; } while ((rval = ffmpeg_read_video_frame(anim, anim->cur_packet)) >= 0) { if (anim->cur_packet->stream_index != anim->videoStream) { continue; } av_log(anim->pFormatCtx, AV_LOG_DEBUG, "READ: strID=%d dts=%" PRId64 " pts=%" PRId64 " %s\n", anim->cur_packet->stream_index, (anim->cur_packet->dts == AV_NOPTS_VALUE) ? -1 : (int64_t)anim->cur_packet->dts, (anim->cur_packet->pts == AV_NOPTS_VALUE) ? -1 : (int64_t)anim->cur_packet->pts, (anim->cur_packet->flags & AV_PKT_FLAG_KEY) ? " KEY" : ""); avcodec_send_packet(anim->pCodecCtx, anim->cur_packet); anim->pFrame_complete = avcodec_receive_frame(anim->pCodecCtx, anim->pFrame) == 0; if (anim->pFrame_complete) { ffmpeg_decode_store_frame_pts(anim); break; } av_packet_unref(anim->cur_packet); anim->cur_packet->stream_index = -1; } if (rval == AVERROR_EOF) { /* Flush any remaining frames out of the decoder. */ avcodec_send_packet(anim->pCodecCtx, NULL); anim->pFrame_complete = avcodec_receive_frame(anim->pCodecCtx, anim->pFrame) == 0; if (anim->pFrame_complete) { ffmpeg_decode_store_frame_pts(anim); rval = 0; } } if (rval < 0) { av_packet_unref(anim->cur_packet); anim->cur_packet->stream_index = -1; av_log(anim->pFormatCtx, AV_LOG_ERROR, " DECODE READ FAILED: av_read_frame() " "returned error: %s\n", av_err2str(rval)); } return (rval >= 0); } static int match_format(const char *name, AVFormatContext *pFormatCtx) { const char *p; int len, namelen; const char *names = pFormatCtx->iformat->name; if (!name || !names) { return 0; } namelen = strlen(name); while ((p = strchr(names, ','))) { len = MAX2(p - names, namelen); if (!BLI_strncasecmp(name, names, len)) { return 1; } names = p + 1; } return !BLI_strcasecmp(name, names); } static int ffmpeg_seek_by_byte(AVFormatContext *pFormatCtx) { static const char *byte_seek_list[] = {"mpegts", 0}; const char **p; if (pFormatCtx->iformat->flags & AVFMT_TS_DISCONT) { return true; } p = byte_seek_list; while (*p) { if (match_format(*p++, pFormatCtx)) { return true; } } return false; } static int64_t ffmpeg_get_seek_pts(struct anim *anim, int64_t pts_to_search) { /* FFmpeg seeks internally using DTS values instead of PTS. In some files DTS and PTS values are * offset and sometimes ffmpeg fails to take this into account when seeking. * Therefore we need to seek backwards a certain offset to make sure the frame we want is in * front of us. It is not possible to determine the exact needed offset, this value is determined * experimentally. Note: Too big offset can impact performance. Current 3 frame offset has no * measurable impact. */ int64_t seek_pts = pts_to_search - (ffmpeg_steps_per_frame_get(anim) * 3); if (seek_pts < 0) { seek_pts = 0; } return seek_pts; } /* This gives us an estimate of which pts our requested frame will have. * Note that this might be off a bit in certain video files, but it should still be close enough. */ static int64_t ffmpeg_get_pts_to_search(struct anim *anim, struct anim_index *tc_index, int position) { int64_t pts_to_search; if (tc_index) { int new_frame_index = IMB_indexer_get_frame_index(tc_index, position); pts_to_search = IMB_indexer_get_pts(tc_index, new_frame_index); } else { AVStream *v_st = anim->pFormatCtx->streams[anim->videoStream]; int64_t start_pts = v_st->start_time; pts_to_search = round(position * ffmpeg_steps_per_frame_get(anim)); if (start_pts != AV_NOPTS_VALUE) { pts_to_search += start_pts; } } return pts_to_search; } static bool ffmpeg_is_first_frame_decode(struct anim *anim) { return anim->pFrame_complete == false; } static void ffmpeg_scan_log(struct anim *anim, int64_t pts_to_search) { int64_t frame_pts_start = av_get_pts_from_frame(anim->pFrame); int64_t frame_pts_end = frame_pts_start + anim->pFrame->pkt_duration; av_log(anim->pFormatCtx, AV_LOG_DEBUG, " SCAN WHILE: PTS range %" PRId64 " - %" PRId64 " in search of %" PRId64 "\n", frame_pts_start, frame_pts_end, pts_to_search); } /* Decode frames one by one until its PTS matches pts_to_search. */ static void ffmpeg_decode_video_frame_scan(struct anim *anim, int64_t pts_to_search) { const int64_t start_gop_frame = anim->cur_key_frame_pts; bool decode_error = false; while (!decode_error && anim->cur_pts < pts_to_search) { ffmpeg_scan_log(anim, pts_to_search); ffmpeg_double_buffer_backup_frame_store(anim, pts_to_search); decode_error = ffmpeg_decode_video_frame(anim) < 1; /* We should not get a new GOP keyframe while scanning if seeking is working as intended. * If this condition triggers, there may be and error in our seeking code. * NOTE: This seems to happen if DTS value is used for seeking in ffmpeg internally. There * seems to be no good way to handle such case. */ if (anim->seek_before_decode && start_gop_frame != anim->cur_key_frame_pts) { av_log(anim->pFormatCtx, AV_LOG_ERROR, "SCAN: Frame belongs to an unexpected GOP!\n"); } } } /* Wrapper over av_seek_frame(), for formats that doesn't have its own read_seek() or * read_seek2() functions defined. When seeking in these formats, rule to seek to last * necessary I-frame is not honored. It is not even guaranteed that I-frame, that must be * decoded will be read. See https://trac.ffmpeg.org/ticket/1607 and * https://developer.blender.org/T86944. */ static int ffmpeg_generic_seek_workaround(struct anim *anim, int64_t *requested_pts, int64_t pts_to_search) { int64_t current_pts = *requested_pts; int64_t offset = 0; int64_t cur_pts, prev_pts = -1; /* Step backward frame by frame until we find the key frame we are looking for. */ while (current_pts != 0) { current_pts = *requested_pts - (int64_t)round(offset * ffmpeg_steps_per_frame_get(anim)); current_pts = MAX2(current_pts, 0); /* Seek to timestamp. */ if (av_seek_frame(anim->pFormatCtx, anim->videoStream, current_pts, AVSEEK_FLAG_BACKWARD) < 0) { break; } /* Read first video stream packet. */ AVPacket *read_packet = av_packet_alloc(); while (av_read_frame(anim->pFormatCtx, read_packet) >= 0) { if (read_packet->stream_index == anim->videoStream) { break; } av_packet_unref(read_packet); } /* If this packet contains an I-frame, this could be the frame that we need. */ bool is_key_frame = read_packet->flags & AV_PKT_FLAG_KEY; /* We need to check the packet timestamp as the key frame could be for a GOP forward in the * video stream. So if it has a larger timestamp than the frame we want, ignore it. */ cur_pts = timestamp_from_pts_or_dts(read_packet->pts, read_packet->dts); av_packet_free(&read_packet); if (is_key_frame) { if (cur_pts <= pts_to_search) { /* We found the I-frame we were looking for! */ break; } } if (cur_pts == prev_pts) { /* We got the same key frame packet twice. * This probably means that we have hit the beginning of the stream. */ break; } prev_pts = cur_pts; offset++; } *requested_pts = current_pts; /* Re-seek to timestamp that gave I-frame, so it can be read by decode function. */ return av_seek_frame(anim->pFormatCtx, anim->videoStream, current_pts, AVSEEK_FLAG_BACKWARD); } /* Read packet until timestamp matches `anim->cur_packet`, thus recovering internal `anim` stream * position state. */ static void ffmpeg_seek_recover_stream_position(struct anim *anim) { AVPacket *temp_packet = av_packet_alloc(); while (ffmpeg_read_video_frame(anim, temp_packet) >= 0) { int64_t current_pts = timestamp_from_pts_or_dts(anim->cur_packet->pts, anim->cur_packet->dts); int64_t temp_pts = timestamp_from_pts_or_dts(temp_packet->pts, temp_packet->dts); av_packet_unref(temp_packet); if (current_pts == temp_pts) { break; } } av_packet_free(&temp_packet); } /* Check if seeking and mainly flushing codec buffers is needed. */ static bool ffmpeg_seek_buffers_need_flushing(struct anim *anim, int position, int64_t seek_pos) { /* Get timestamp of packet read after seeking. */ AVPacket *temp_packet = av_packet_alloc(); ffmpeg_read_video_frame(anim, temp_packet); int64_t gop_pts = timestamp_from_pts_or_dts(temp_packet->pts, temp_packet->dts); av_packet_unref(temp_packet); av_packet_free(&temp_packet); /* Seeking gives packet, that is currently read. No seeking was necessary, so buffers don't have * to be flushed. */ if (gop_pts == timestamp_from_pts_or_dts(anim->cur_packet->pts, anim->cur_packet->dts)) { return false; } /* Packet after seeking is same key frame as current, and further in time. No seeking was * necessary, so buffers don't have to be flushed. But stream position has to be recovered. */ if (gop_pts == anim->cur_key_frame_pts && position > anim->cur_position) { ffmpeg_seek_recover_stream_position(anim); return false; } /* Seeking was necessary, but we have read packets. Therefore we must seek again. */ av_seek_frame(anim->pFormatCtx, anim->videoStream, seek_pos, AVSEEK_FLAG_BACKWARD); anim->cur_key_frame_pts = gop_pts; return true; } /* Seek to last necessary key frame. */ static int ffmpeg_seek_to_key_frame(struct anim *anim, int position, struct anim_index *tc_index, int64_t pts_to_search) { int64_t seek_pos; int ret; if (tc_index) { /* We can use timestamps generated from our indexer to seek. */ int new_frame_index = IMB_indexer_get_frame_index(tc_index, position); int old_frame_index = IMB_indexer_get_frame_index(tc_index, anim->cur_position); if (IMB_indexer_can_scan(tc_index, old_frame_index, new_frame_index)) { /* No need to seek, return early. */ return 0; } uint64_t pts; uint64_t dts; seek_pos = IMB_indexer_get_seek_pos(tc_index, new_frame_index); pts = IMB_indexer_get_seek_pos_pts(tc_index, new_frame_index); dts = IMB_indexer_get_seek_pos_dts(tc_index, new_frame_index); anim->cur_key_frame_pts = timestamp_from_pts_or_dts(pts, dts); av_log(anim->pFormatCtx, AV_LOG_DEBUG, "TC INDEX seek seek_pos = %" PRId64 "\n", seek_pos); av_log(anim->pFormatCtx, AV_LOG_DEBUG, "TC INDEX seek pts = %" PRIu64 "\n", pts); av_log(anim->pFormatCtx, AV_LOG_DEBUG, "TC INDEX seek dts = %" PRIu64 "\n", dts); if (ffmpeg_seek_by_byte(anim->pFormatCtx)) { av_log(anim->pFormatCtx, AV_LOG_DEBUG, "... using BYTE seek_pos\n"); ret = av_seek_frame(anim->pFormatCtx, -1, seek_pos, AVSEEK_FLAG_BYTE); } else { av_log(anim->pFormatCtx, AV_LOG_DEBUG, "... using PTS seek_pos\n"); ret = av_seek_frame( anim->pFormatCtx, anim->videoStream, anim->cur_key_frame_pts, AVSEEK_FLAG_BACKWARD); } } else { /* We have to manually seek with ffmpeg to get to the key frame we want to start decoding from. */ seek_pos = ffmpeg_get_seek_pts(anim, pts_to_search); av_log( anim->pFormatCtx, AV_LOG_DEBUG, "NO INDEX final seek seek_pos = %" PRId64 "\n", seek_pos); AVFormatContext *format_ctx = anim->pFormatCtx; if (format_ctx->iformat->read_seek2 || format_ctx->iformat->read_seek) { ret = av_seek_frame(anim->pFormatCtx, anim->videoStream, seek_pos, AVSEEK_FLAG_BACKWARD); } else { ret = ffmpeg_generic_seek_workaround(anim, &seek_pos, pts_to_search); av_log(anim->pFormatCtx, AV_LOG_DEBUG, "Adjusted final seek seek_pos = %" PRId64 "\n", seek_pos); } if (ret <= 0 && !ffmpeg_seek_buffers_need_flushing(anim, position, seek_pos)) { return 0; } } if (ret < 0) { av_log(anim->pFormatCtx, AV_LOG_ERROR, "FETCH: " "error while seeking to DTS = %" PRId64 " (frameno = %d, PTS = %" PRId64 "): errcode = %d\n", seek_pos, position, pts_to_search, ret); } /* Flush the internal buffers of ffmpeg. This needs to be done after seeking to avoid decoding * errors. */ avcodec_flush_buffers(anim->pCodecCtx); ffmpeg_double_buffer_backup_frame_clear(anim); anim->cur_pts = -1; if (anim->cur_packet->stream_index == anim->videoStream) { av_packet_unref(anim->cur_packet); anim->cur_packet->stream_index = -1; } return ret; } static bool ffmpeg_must_seek(struct anim *anim, int position) { bool must_seek = position != anim->cur_position + 1 || ffmpeg_is_first_frame_decode(anim); anim->seek_before_decode = must_seek; return must_seek; } static ImBuf *ffmpeg_fetchibuf(struct anim *anim, int position, IMB_Timecode_Type tc) { if (anim == NULL) { return NULL; } av_log(anim->pFormatCtx, AV_LOG_DEBUG, "FETCH: seek_pos=%d\n", position); struct anim_index *tc_index = IMB_anim_open_index(anim, tc); int64_t pts_to_search = ffmpeg_get_pts_to_search(anim, tc_index, position); AVStream *v_st = anim->pFormatCtx->streams[anim->videoStream]; double frame_rate = av_q2d(v_st->r_frame_rate); double pts_time_base = av_q2d(v_st->time_base); int64_t start_pts = v_st->start_time; av_log(anim->pFormatCtx, AV_LOG_DEBUG, "FETCH: looking for PTS=%" PRId64 " (pts_timebase=%g, frame_rate=%g, start_pts=%" PRId64 ")\n", (int64_t)pts_to_search, pts_time_base, frame_rate, start_pts); if (ffmpeg_must_seek(anim, position)) { ffmpeg_seek_to_key_frame(anim, position, tc_index, pts_to_search); } ffmpeg_decode_video_frame_scan(anim, pts_to_search); /* Update resolution as it can change per-frame with WebM. See T100741 & T100081. */ anim->x = anim->pCodecCtx->width; anim->y = anim->pCodecCtx->height; IMB_freeImBuf(anim->cur_frame_final); /* Certain versions of FFmpeg have a bug in libswscale which ends up in crash * when destination buffer is not properly aligned. For example, this happens * in FFmpeg 4.3.1. It got fixed later on, but for compatibility reasons is * still best to avoid crash. * * This is achieved by using own allocation call rather than relying on * IMB_allocImBuf() to do so since the IMB_allocImBuf() is not guaranteed * to perform aligned allocation. * * In theory this could give better performance, since SIMD operations on * aligned data are usually faster. * * Note that even though sometimes vertical flip is required it does not * affect on alignment of data passed to sws_scale because if the X dimension * is not 32 byte aligned special intermediate buffer is allocated. * * The issue was reported to FFmpeg under ticket #8747 in the FFmpeg tracker * and is fixed in the newer versions than 4.3.1. */ const AVPixFmtDescriptor *pix_fmt_descriptor = av_pix_fmt_desc_get(anim->pCodecCtx->pix_fmt); int planes = R_IMF_PLANES_RGBA; if ((pix_fmt_descriptor->flags & AV_PIX_FMT_FLAG_ALPHA) == 0) { planes = R_IMF_PLANES_RGB; } anim->cur_frame_final = IMB_allocImBuf(anim->x, anim->y, planes, 0); anim->cur_frame_final->rect = MEM_mallocN_aligned( (size_t)4 * anim->x * anim->y, 32, "ffmpeg ibuf"); anim->cur_frame_final->mall |= IB_rect; anim->cur_frame_final->rect_colorspace = colormanage_colorspace_get_named(anim->colorspace); AVFrame *final_frame = ffmpeg_frame_by_pts_get(anim, pts_to_search); if (final_frame == NULL) { /* No valid frame was decoded for requested PTS, fall back on most recent decoded frame, even * if it is incorrect. */ final_frame = ffmpeg_double_buffer_frame_fallback_get(anim); } /* Even with the fallback from above it is possible that the current decode frame is NULL. In * this case skip post-processing and return current image buffer. */ if (final_frame != NULL) { ffmpeg_postprocess(anim, final_frame); } anim->cur_position = position; IMB_refImBuf(anim->cur_frame_final); return anim->cur_frame_final; } static void free_anim_ffmpeg(struct anim *anim) { if (anim == NULL) { return; } if (anim->pCodecCtx) { avcodec_free_context(&anim->pCodecCtx); avformat_close_input(&anim->pFormatCtx); av_packet_free(&anim->cur_packet); av_frame_free(&anim->pFrame); av_frame_free(&anim->pFrame_backup); av_frame_free(&anim->pFrameRGB); av_frame_free(&anim->pFrameDeinterlaced); sws_freeContext(anim->img_convert_ctx); IMB_freeImBuf(anim->cur_frame_final); } anim->duration_in_frames = 0; } #endif /** * Try to initialize the #anim struct. * Returns true on success. */ static bool anim_getnew(struct anim *anim) { BLI_assert(anim->curtype == ANIM_NONE); if (anim == NULL) { /* Nothing to initialize. */ return false; } free_anim_movie(anim); #ifdef WITH_AVI free_anim_avi(anim); #endif #ifdef WITH_FFMPEG free_anim_ffmpeg(anim); #endif anim->curtype = imb_get_anim_type(anim->name); switch (anim->curtype) { case ANIM_SEQUENCE: { ImBuf *ibuf = IMB_loadiffname(anim->name, anim->ib_flags, anim->colorspace); if (ibuf) { BLI_strncpy(anim->first, anim->name, sizeof(anim->first)); anim->duration_in_frames = 1; IMB_freeImBuf(ibuf); } else { return false; } break; } case ANIM_MOVIE: if (startmovie(anim)) { return false; } break; #ifdef WITH_AVI case ANIM_AVI: if (startavi(anim)) { printf("couldn't start avi\n"); return false; } break; #endif #ifdef WITH_FFMPEG case ANIM_FFMPEG: if (startffmpeg(anim)) { return false; } break; #endif } return true; } struct ImBuf *IMB_anim_previewframe(struct anim *anim) { struct ImBuf *ibuf = NULL; int position = 0; ibuf = IMB_anim_absolute(anim, 0, IMB_TC_NONE, IMB_PROXY_NONE); if (ibuf) { IMB_freeImBuf(ibuf); position = anim->duration_in_frames / 2; ibuf = IMB_anim_absolute(anim, position, IMB_TC_NONE, IMB_PROXY_NONE); } return ibuf; } struct ImBuf *IMB_anim_absolute(struct anim *anim, int position, IMB_Timecode_Type tc, IMB_Proxy_Size preview_size) { struct ImBuf *ibuf = NULL; char head[256], tail[256]; ushort digits; int pic; int filter_y; if (anim == NULL) { return NULL; } filter_y = (anim->ib_flags & IB_animdeinterlace); if (preview_size == IMB_PROXY_NONE) { if (anim->curtype == ANIM_NONE) { if (!anim_getnew(anim)) { return NULL; } } if (position < 0) { return NULL; } if (position >= anim->duration_in_frames) { return NULL; } } else { struct anim *proxy = IMB_anim_open_proxy(anim, preview_size); if (proxy) { position = IMB_anim_index_get_frame_index(anim, tc, position); return IMB_anim_absolute(proxy, position, IMB_TC_NONE, IMB_PROXY_NONE); } } switch (anim->curtype) { case ANIM_SEQUENCE: pic = an_stringdec(anim->first, head, tail, &digits); pic += position; an_stringenc(anim->name, head, tail, digits, pic); ibuf = IMB_loadiffname(anim->name, IB_rect, anim->colorspace); if (ibuf) { anim->cur_position = position; } break; case ANIM_MOVIE: ibuf = movie_fetchibuf(anim, position); if (ibuf) { anim->cur_position = position; IMB_convert_rgba_to_abgr(ibuf); } break; #ifdef WITH_AVI case ANIM_AVI: ibuf = avi_fetchibuf(anim, position); if (ibuf) { anim->cur_position = position; } break; #endif #ifdef WITH_FFMPEG case ANIM_FFMPEG: ibuf = ffmpeg_fetchibuf(anim, position, tc); if (ibuf) { anim->cur_position = position; } filter_y = 0; /* done internally */ break; #endif } if (ibuf) { if (filter_y) { IMB_filtery(ibuf); } BLI_snprintf(ibuf->name, sizeof(ibuf->name), "%s.%04d", anim->name, anim->cur_position + 1); } return ibuf; } /***/ int IMB_anim_get_duration(struct anim *anim, IMB_Timecode_Type tc) { struct anim_index *idx; if (tc == IMB_TC_NONE) { return anim->duration_in_frames; } idx = IMB_anim_open_index(anim, tc); if (!idx) { return anim->duration_in_frames; } return IMB_indexer_get_duration(idx); } double IMD_anim_get_offset(struct anim *anim) { return anim->start_offset; } bool IMB_anim_get_fps(struct anim *anim, short *frs_sec, float *frs_sec_base, bool no_av_base) { double frs_sec_base_double; if (anim->frs_sec) { if (anim->frs_sec > SHRT_MAX) { /* We cannot store original rational in our short/float format, * we need to approximate it as best as we can... */ *frs_sec = SHRT_MAX; frs_sec_base_double = anim->frs_sec_base * (double)SHRT_MAX / (double)anim->frs_sec; } else { *frs_sec = anim->frs_sec; frs_sec_base_double = anim->frs_sec_base; } #ifdef WITH_FFMPEG if (no_av_base) { *frs_sec_base = (float)(frs_sec_base_double / AV_TIME_BASE); } else { *frs_sec_base = (float)frs_sec_base_double; } #else UNUSED_VARS(no_av_base); *frs_sec_base = (float)frs_sec_base_double; #endif BLI_assert(*frs_sec > 0); BLI_assert(*frs_sec_base > 0.0f); return true; } return false; } int IMB_anim_get_image_width(struct anim *anim) { return anim->x; } int IMB_anim_get_image_height(struct anim *anim) { return anim->y; }