/* mtn - movie thumbnailer http://moviethumbnail.sourceforge.net/ Copyright (C) 2007-2008 tuit , et al. based on "Using libavformat and libavcodec" by Martin Böhme: http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html http://www.inb.uni-luebeck.de/~boehme/libavcodec_update.html and "An ffmpeg and SDL Tutorial": http://www.dranger.com/ffmpeg/ and "Using GD with FFMPeg": http://cvs.php.net/viewvc.cgi/gd/playground/ffmpeg/ and ffplay.c in ffmpeg Copyright (c) 2003 Fabrice Bellard http://ffmpeg.mplayerhq.hu/ and gd.c in libGD http://cvs.php.net/viewvc.cgi/php-src/ext/gd/libgd/gd.c?revision=1.111&view=markup please excuse the mess, i am a very rusty programmer! helps, comments or patches are very welcomed. :) tested with ffmpeg-r14005 and gd-2.0.34 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // enable unicode functions in mingw #ifdef WIN32 #define UNICODE #define _UNICODE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" #include "gd.h" #define UTF8_FILENAME_SIZE (FILENAME_MAX*4) #ifdef WIN32 unsigned int _CRT_fmode = _O_BINARY; // default binary file including stdin, stdout, stderr #include #include #ifdef _UNICODE #define UTF8_2_WC(wdst, src, size) MultiByteToWideChar(CP_UTF8, 0, (src), -1, (wdst), (size)) #define WC_2_UTF8(dst, wsrc, size) WideCharToMultiByte(CP_UTF8, 0, (wsrc), -1, (dst), (size), NULL, NULL) #else #define UTF8_2_WC(dst, src, size) ((dst) = (src)) // cant be used to check required size #define WC_2_UTF8(dst, src, size) ((dst) = (src)) #endif #else #include "fake_tchar.h" #define UTF8_2_WC(dst, src, size) ((dst) = (src)) // cant be used to check required size #define WC_2_UTF8(dst, src, size) ((dst) = (src)) #endif // newline character for info file #ifdef WIN32 #define NEWLINE "\r\n" #else #define NEWLINE "\n" #endif #define EDGE_PARTS 6 // # of parts used in edge detection #define EDGE_FOUND 0.001f // edge is considered found typedef struct rgb_color { // GD uses int, so we'll use int too int r; // red int g; // green int b; // blue } rgb_color; typedef char color_str[7]; // "RRGGBB" (in hex) #define COLOR_BLACK (rgb_color){0, 0, 0} #define COLOR_GREY (rgb_color){128, 128, 128} #define COLOR_WHITE (rgb_color){255, 255, 255} #define COLOR_INFO (rgb_color){85, 85, 85} typedef struct shot { gdImagePtr ip; int64_t eff_target; int64_t found_pts; double blank; // blank detection float edge[EDGE_PARTS]; // edge detection } shot; // shot typedef struct thumbnail { gdImagePtr out_ip; char out_filename[UTF8_FILENAME_SIZE]; char info_filename[UTF8_FILENAME_SIZE]; int out_saved; // 1 = out file is successfully saved int width, height; int txt_height; int column, row; int step; int shot_width, shot_height; int center_gap; // horizontal gap to center the shots int idx; // index of the last shot; -1 = no shot // dynamic int64_t *ppts; // array of pts value of each shot } thumbnail; // thumbnail data & info /* LIBAVUTIL_VERSION_INT is too low for VERBOSE & INFO, so we'll define our own */ #define LOG_INFO 0 /* command line options & default values */ #define GB_A_RATIO (AVRational){0, 1} AVRational gb_a_ratio = GB_A_RATIO; #define GB_B_BLANK 0.8 double gb_b_blank = GB_B_BLANK; #define GB_B_BEGIN 0.0 double gb_B_begin = GB_B_BEGIN; // skip this seconds from the beginning #define GB_C_COLUMN 3 int gb_c_column = GB_C_COLUMN; #define GB_C_CUT -1 double gb_C_cut = GB_C_CUT; // cut movie; <=0 off #define GB_D_EDGE 12 int gb_D_edge = GB_D_EDGE; // edge detection; 0 off; >0 on #define GB_E_EXT NULL char *gb_e_ext = GB_E_EXT; #define GB_E_END 0.0 double gb_E_end = GB_E_END; // skip this seconds at the end #define GB_F_FONTNAME "tahomabd.ttf" char *gb_f_fontname = GB_F_FONTNAME; rgb_color gb_F_info_color = COLOR_INFO; // info color double gb_F_info_font_size = 9; // info font size char *gb_F_ts_fontname = GB_F_FONTNAME; // time stamp fontname rgb_color gb_F_ts_color = COLOR_WHITE; // time stamp color rgb_color gb_F_ts_shadow = COLOR_BLACK; // time stamp shadow color double gb_F_ts_font_size = 8; // time stamp font size #define GB_G_GAP 0 int gb_g_gap = GB_G_GAP; #define GB_H_HEIGHT 150 int gb_h_height = GB_H_HEIGHT; // mininum height of each shot; will reduce # of column to meet this height #define GB_I_INFO 1 int gb_i_info = GB_I_INFO; // 1 on; 0 off #define GB_I_INDIVIDUAL 0 int gb_I_individual = GB_I_INDIVIDUAL; // 1 on; 0 off #define GB_J_QUALITY 90 int gb_j_quality = GB_J_QUALITY; #define GB_K_BCOLOR COLOR_WHITE rgb_color gb_k_bcolor = GB_K_BCOLOR; // background color #define GB_L_INFO_LOCATION 4 int gb_L_info_location = GB_L_INFO_LOCATION; #define GB_L_TIME_LOCATION 1 int gb_L_time_location = GB_L_TIME_LOCATION; #define GB_N_NORMAL 0 int gb_n_normal = GB_N_NORMAL; // normal priority; 1 normal; 0 lower #define GB_N_SUFFIX NULL char *gb_N_suffix = GB_N_SUFFIX; // info text file suffix #define GB_O_SUFFIX "_s.jpg" char *gb_o_suffix = GB_O_SUFFIX; #define GB_O_OUTDIR NULL char *gb_O_outdir = GB_O_OUTDIR; #ifdef WIN32 #define GB_P_PAUSE 1 #else #define GB_P_PAUSE 0 #endif int gb_p_pause = GB_P_PAUSE; // pause before exiting; 1 pause; 0 dont pause #define GB_P_DONTPAUSE 0 int gb_P_dontpause = GB_P_DONTPAUSE; // dont pause; overide gb_p_pause #define GB_Q_QUIET 0 int gb_q_quiet = GB_Q_QUIET; // 1 on; 0 off #define GB_R_ROW 0 int gb_r_row = GB_R_ROW; // 0 = as many rows as needed #define GB_S_STEP 120 int gb_s_step = GB_S_STEP; // less than 0 = every frame; 0 = step evenly to get column x row #define GB_T_TIME 1 int gb_t_timestamp = GB_T_TIME; // 1 on; 0 off #define GB_T_TEXT NULL char *gb_T_text = GB_T_TEXT; #define GB_V_VERBOSE 0 int gb_v_verbose = GB_V_VERBOSE; // 1 on; 0 off int gb_V = GB_V_VERBOSE; // 1 on; 0 off #define GB_W_WIDTH 1024 int gb_w_width = GB_W_WIDTH; // 0 = column * movie width #define GB_W_OVERWRITE 1 int gb_W_overwrite = GB_W_OVERWRITE; // 1 = overwrite; 0 = dont overwrite #define GB_Z_SEEK 0 int gb_z_seek = GB_Z_SEEK; // always use seek mode; 1 on; 0 off #define GB_Z_NONSEEK 0 int gb_Z_nonseek = GB_Z_NONSEEK; // always use non-seek mode; 1 on; 0 off /* more global variables */ char *gb_argv0 = NULL; char *gb_version = "200808a copyright (c) 2007-2008 tuit, et al."; time_t gb_st_start = 0; // start time of program /* misc functions */ /* strrstr not in mingw */ char *strlaststr (char *haystack, char *needle) { // If needle is an empty string, the function returns haystack. -- from glibc doc if (0 == strlen(needle)) { return haystack; } char *start = haystack; char *found = NULL; char *prev = NULL; while ((found = strstr(start, needle)) != NULL) { prev = found; start++; } return prev; } char *format_color(rgb_color col) { static char buf[7]; // FIXME sprintf(buf, "%02X%02X%02X", col.r, col.g, col.b); return buf; // FIXME } void format_time(double duration, char *str, char sep) { if (duration < 0) { sprintf(str, "N/A"); } else { int hours, mins, secs; secs = duration; mins = secs / 60; secs %= 60; hours = mins / 60; mins %= 60; sprintf(str, "%02d%c%02d%c%02d", hours, sep, mins, sep, secs); } } char *format_size(int64_t size, char *unit) { static char buf[20]; // FIXME if (size < 1024) { sprintf(buf, "%"PRId64" %s", size, unit); } else if (size < 1024*1024) { sprintf(buf, "%.2f Ki%s", size/1024.0, unit); } else if (size < 1024*1024*1024) { sprintf(buf, "%.2f Mi%s", size/1024.0/1024, unit); } else { sprintf(buf, "%.2f Gi%s", size/1024.0/1024/1024, unit); } return buf; } /* return only the file name of the full path FIXME: wont work in unix if filename has '\\', e.g. path = "hello \\ world"; */ char *path_2_file(char *path) { int len = strlen(path); char *slash = strrchr(path, '/'); char *backslash = strrchr(path, '\\'); if (NULL != slash || NULL != backslash) { char *last = (slash > backslash) ? slash : backslash; if (last - path + 1 < len) { // make sure last char is not '/' or '\\' return last + 1; } } return path; } /* copy n strings to dst ... must be char * dst must be large enough */ char *strcpy_va(char *dst, int n, ...) { va_list ap; int pos = 0; dst[pos] = '\0'; va_start(ap, n); int i; for (i=0; i < n; i++) { char *s = va_arg(ap, char *); assert(NULL != s); int len = strlen(s); strncpy(dst + pos, s, len + 1); // for '\0' pos += len; } va_end(ap); return dst; } /* return 1 if file is a regular file return 0 if fail or is not */ int is_reg(char *file) { #if defined(WIN32) && defined(_UNICODE) wchar_t file_w[FILENAME_MAX]; UTF8_2_WC(file_w, file, FILENAME_MAX); #else char *file_w = file; #endif struct _stat buf; if (0 != _tstat(file_w, &buf)) { return 0; } return S_ISREG(buf.st_mode); } /* return 1 if file is a regular file and modified time >= st_time return 0 if fail or is not */ int is_reg_newer(char *file, time_t st_time) { #if defined(WIN32) && defined(_UNICODE) wchar_t file_w[FILENAME_MAX]; UTF8_2_WC(file_w, file, FILENAME_MAX); #else char *file_w = file; #endif struct _stat buf; if (0 != _tstat(file_w, &buf)) { return 0; } return S_ISREG(buf.st_mode) && (difftime(buf.st_mtime, st_time) >= 0); } /* return 1 if file is a directory return 0 if fail or is not a directory FIXME: /c under msys is not a directory. why? */ int is_dir(char *file) { #if defined(WIN32) && defined(_UNICODE) wchar_t file_w[FILENAME_MAX]; UTF8_2_WC(file_w, file, FILENAME_MAX); #else char *file_w = file; #endif struct _stat buf; if (0 != _tstat(file_w, &buf)) { return 0; } return S_ISDIR(buf.st_mode); } /* */ char *rem_trailing_slash(char *str) { #ifdef WIN32 // mingw doesn't seem to be happy about trailing '/' or '\\' // strip trailing '/' or '\\' that might get added by shell filename completion for directories int last_index = strlen(str) - 1; // we need last '/' or '\\' for root drive "c:\" while (last_index > 2 && ('/' == str[last_index] || '\\' == str[last_index])) { str[last_index--] = '\0'; } #endif return str; } /* mtn */ /* return pointer to a new cropped image. the original one is freed. if error, return original and the original stays intact */ gdImagePtr crop_image(gdImagePtr ip, int new_width, int new_height) { // cant find GD's crop, so we'll need to create a smaller image gdImagePtr new_ip = gdImageCreateTrueColor(new_width, new_height); if (NULL == new_ip) { //return NULL; // return the original should be better return ip; } gdImageCopy(new_ip, ip, 0, 0, 0, 0, new_width, new_height); gdImageDestroy(ip); return new_ip; } /* returns height, or 0 if error */ int image_string_height(char *text, char *font, double size) { int brect[8]; if (NULL == text || 0 == strlen(text)) { return 0; } char *err = gdImageStringFT(NULL, &brect[0], 0, font, size, 0, 0, 0, text); if (NULL != err) { return 0; } return brect[3] - brect[7]; } /* position can be: 1: lower left 2: lower right 3: upper right 4: upper left returns NULL if success, otherwise returns error message */ char *image_string(gdImagePtr ip, char *font, rgb_color color, double size, int position, int gap, char *text, int shadow, rgb_color shadow_color) { int brect[8]; int gd_color = gdImageColorResolve(ip, color.r, color.g, color.b); char *err = gdImageStringFT(NULL, &brect[0], gd_color, font, size, 0, 0, 0, text); if (NULL != err) { return err; } /* int width = brect[2] - brect[6]; int height = brect[3] - brect[7]; */ int x, y; switch (position) { case 1: // lower left x = -brect[0] + gap; y = gdImageSY(ip) - brect[1] - gap; break; case 2: // lower right x = gdImageSX(ip) - brect[2] - gap; y = gdImageSY(ip) - brect[3] - gap; break; case 3: // upper right x = gdImageSX(ip) - brect[4] - gap; y = -brect[5] + gap; break; case 4: // upper left x = -brect[6] + gap; y = -brect[7] + gap; break; default: return "image_string's position can only be 1, 2, 3, or 4"; } if (shadow) { int shadowx, shadowy; switch (position) { case 1: // lower left shadowx = x+1; shadowy = y; y = y-1; break; case 2: // lower right shadowx = x; shadowy = y; x = x-1; y = y-1; break; case 3: // upper right shadowx = x; shadowy = y+1; x = x-1; break; case 4: // upper left shadowx = x+1; shadowy = y+1; break; default: return "image_string's position can only be 1, 2, 3, or 4"; } int gd_shadow = gdImageColorResolve(ip, shadow_color.r, shadow_color.g, shadow_color.b); err = gdImageStringFT(ip, &brect[0], gd_shadow, font, size, 0, shadowx, shadowy, text); if (NULL != err) { return err; } } return gdImageStringFT(ip, &brect[0], gd_color, font, size, 0, x, y, text); } /* return 0 if can save jpg */ int save_jpg(gdImagePtr ip, char *outname) { #if defined(WIN32) && defined(_UNICODE) wchar_t outname_w[FILENAME_MAX]; UTF8_2_WC(outname_w, outname, FILENAME_MAX); #else char *outname_w = outname; #endif int done = -1; FILE *fp = _tfopen(outname_w, _TEXT("wb")); if (NULL == fp) { goto cleanup; } errno = 0; gdImageJpeg(ip, fp, gb_j_quality); /* how to check if write was successful? */ if (0 != errno) { // FIXME: valid check? goto cleanup; } done = 0; // 0 = ok cleanup: if (NULL != fp && 0 != fclose(fp)) { done = -1; } return done; } /* pFrame must be a PIX_FMT_RGB24 frame */ void FrameRGB_2_gdImage(AVFrame *pFrame, gdImagePtr ip, int width, int height) { uint8_t *src = pFrame->data[0]; int x, y; for (y = 0; y < height; y++) { for (x = 0; x < width * 3; x += 3) { gdImageSetPixel(ip, x / 3, y, gdImageColorResolve(ip, src[x], src[x + 1], src[x + 2])); //gdImageSetPixel(ip, x/3, y, gdTrueColor(src[x], src[x+1], src[x+2])); } src += width * 3; } } /* */ void shot_new(shot *psh) { psh->ip = NULL; psh->eff_target = AV_NOPTS_VALUE; psh->found_pts = AV_NOPTS_VALUE; psh->blank = 0; int i; for (i=0; iedge[i] = 1; } } /* initialize */ void thumb_new(thumbnail *ptn) { ptn->out_ip = NULL; ptn->out_filename[0] = '\0'; ptn->info_filename[0] = '\0'; ptn->out_saved = 0; ptn->width = ptn->height = 0; ptn->txt_height = 0; ptn->column = ptn->row = 0; ptn->step = 0; ptn->shot_width = ptn->shot_height = 0; ptn->center_gap = 0; ptn->idx = -1; // dynamic ptn->ppts = NULL; } /* alloc dynamic data; must be called after all required static data is filled in return -1 if failed */ int thumb_alloc_dynamic(thumbnail *ptn) { ptn->ppts = malloc(ptn->column * ptn->row * sizeof(*(ptn->ppts))); if (NULL == ptn->ppts) { return -1; } return 0; } void thumb_cleanup_dynamic(thumbnail *ptn) { if (NULL != ptn->ppts) { free(ptn->ppts); ptn->ppts = NULL; } } /* add shot because ptn->idx is the last index, this function assumes that shots will be added in increasing order. */ void thumb_add_shot(thumbnail *ptn, gdImagePtr ip, int idx, int64_t pts) { int dstX = idx%ptn->column * (ptn->shot_width+gb_g_gap) + gb_g_gap + ptn->center_gap; int dstY = idx/ptn->column * (ptn->shot_height+gb_g_gap) + gb_g_gap + ((3 == gb_L_info_location || 4 == gb_L_info_location) ? ptn->txt_height : 0); gdImageCopy(ptn->out_ip, ip, dstX, dstY, 0, 0, ptn->shot_width, ptn->shot_height); ptn->idx = idx; ptn->ppts[idx] = pts; } #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) #endif #ifndef MAX #define MAX(a,b) ((a)<(b)?(b):(a)) #endif /* perform convolution on pFrame and store result in ip pFrame must be a PIX_FMT_RGB24 frame ip must be of the same size as pFrame begin = upper left, end = lower right filter should be a 2-dimensional but since we cant pass it without knowning the size, we'll use 1 dimension modified from: http://cvs.php.net/viewvc.cgi/php-src/ext/gd/libgd/gd.c?revision=1.111&view=markup */ void FrameRGB_convolution(AVFrame *pFrame, int width, int height, float *filter, int filter_size, float filter_div, float offset, gdImagePtr ip, int xbegin, int ybegin, int xend, int yend) { int x, y, i, j; float new_r, new_g, new_b; uint8_t *src = pFrame->data[0]; for (y=ybegin; y<=yend; y++) { for(x=xbegin; x<=xend; x++) { new_r = new_g = new_b = 0; //float grey = 0; for (j=0; j 255.0f)? 255.0f : ((new_r < 0.0f)? 0.0f:new_r); new_g = (new_g > 255.0f)? 255.0f : ((new_g < 0.0f)? 0.0f:new_g); new_b = (new_b > 255.0f)? 255.0f : ((new_b < 0.0f)? 0.0f:new_b); //grey = (grey > 255.0f)? 255.0f : ((grey < 0.0f)? 0.0f:grey); gdImageSetPixel(ip, x, y, gdImageColorResolve(ip, (int)new_r, (int)new_g, (int)new_b)); //gdImageSetPixel(ip, x, y, gdTrueColor((int)new_r, (int)new_g, (int)new_b)); //gdImageSetPixel(ip, x, y, gdTrueColor((int)grey, (int)grey, (int)grey)); } } } /* begin = upper left, end = lower right */ float cmp_edge(gdImagePtr ip, int xbegin, int ybegin, int xend, int yend) { #define CMP_EDGE 208 int count = 0; int i, j; for (j = ybegin; j <= yend; j++) { for (i = xbegin; i <= xend; i++) { int pixel = gdImageGetPixel(ip, i, j); if (gdImageRed(ip, pixel) >= CMP_EDGE && gdImageGreen(ip, pixel) >= CMP_EDGE && gdImageBlue(ip, pixel) >= CMP_EDGE) { count++; } } } return (float)count / (yend - ybegin + 1) / (xend - xbegin + 1); } int is_edge(float *edge, float edge_found) { if (gb_V) { // DEBUG return 1; } int count = 0; int i; for (i = 0; i < EDGE_PARTS; i++) { if (edge[i] >= edge_found) { count++; } } if (count >= 2) { return count; } return 0; } /* pFrame must be an PIX_FMT_RGB24 frame http://student.kuleuven.be/~m0216922/CG/ http://www.pages.drexel.edu/~weg22/edge.html http://student.kuleuven.be/~m0216922/CG/filtering.html http://cvs.php.net/viewvc.cgi/php-src/ext/gd/libgd/gd.c?revision=1.111&view=markup */ gdImagePtr detect_edge(AVFrame *pFrame, int width, int height, float *edge, float edge_found) { static float filter[] = { 0,-1, 0, -1, 4,-1, 0,-1, 0 }; #define FILTER_SIZE 3 // 3x3 #define FILTER_DIV 1 #define OFFSET 128 static int init_filter = 0; // FIXME if (0 == init_filter) { init_filter = 1; filter[1] = -gb_D_edge/4.0f; filter[3] = -gb_D_edge/4.0f; filter[4] = gb_D_edge; filter[5] = -gb_D_edge/4.0f; filter[7] = -gb_D_edge/4.0f; } gdImagePtr ip = gdImageCreateTrueColor(width, height); if (NULL == ip) { av_log(NULL, AV_LOG_ERROR, " gdImageCreateTrueColor failed\n"); return NULL; } if (gb_v_verbose > 0) { FrameRGB_2_gdImage(pFrame, ip, width, height); } int i; for (i = 0; i < EDGE_PARTS; i++) { edge[i] = 1; } // check 6 parts to speed this up & to improve correctness int y_size = height/10; int ya = y_size*2; int yb = y_size*4; int yc = y_size*6; int x_crop = width/8; // only find edge if neccessary int parts[EDGE_PARTS][4] = { //xbegin, ybegin, xend, yend {x_crop, ya, width/2, ya+y_size}, {width/2+1, ya+y_size, width-x_crop, ya+2*y_size}, {x_crop, yb, width/2, yb+y_size}, {width/2+1, yb+y_size, width-x_crop, yb+2*y_size}, {x_crop, yc, width/2, yc+y_size}, {width/2+1, yc+y_size, width-x_crop, yc+2*y_size}, }; int count = 0; for (i = 0; i < EDGE_PARTS && count < 2; i++) { FrameRGB_convolution(pFrame, width, height, filter, FILTER_SIZE, FILTER_DIV, OFFSET, ip, parts[i][0], parts[i][1], parts[i][2], parts[i][3]); edge[i] = cmp_edge(ip, parts[i][0], parts[i][1], parts[i][2], parts[i][3]); if (edge[i] >= edge_found) { count++; } } return ip; } /* for debuging */ void save_AVFrame(AVFrame *pFrame, int src_width, int src_height, int pix_fmt, char *filename, int dst_width, int dst_height) { AVFrame *pFrameRGB = NULL; uint8_t *rgb_buffer = NULL; struct SwsContext *pSwsCtx = NULL; gdImagePtr ip = NULL; pFrameRGB = avcodec_alloc_frame(); if (pFrameRGB == NULL) { av_log(NULL, AV_LOG_ERROR, " couldn't allocate a video frame\n"); goto cleanup; } int rgb_bufsize = avpicture_get_size(PIX_FMT_RGB24, dst_width, dst_height); rgb_buffer = av_malloc(rgb_bufsize); if (NULL == rgb_buffer) { av_log(NULL, AV_LOG_ERROR, " av_malloc %d bytes failed\n", rgb_bufsize); goto cleanup; } avpicture_fill((AVPicture *) pFrameRGB, rgb_buffer, PIX_FMT_RGB24, dst_width, dst_height); pSwsCtx = sws_getContext(src_width, src_height, pix_fmt, dst_width, dst_height, PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); if (NULL == pSwsCtx) { // sws_getContext is not documented av_log(NULL, AV_LOG_ERROR, " sws_getContext failed\n"); goto cleanup; } sws_scale(pSwsCtx, pFrame->data, pFrame->linesize, 0, src_height, pFrameRGB->data, pFrameRGB->linesize); ip = gdImageCreateTrueColor(dst_width, dst_height); if (NULL == ip) { av_log(NULL, AV_LOG_ERROR, " gdImageCreateTrueColor failed: width %d, height %d\n", dst_width, dst_height); goto cleanup; } FrameRGB_2_gdImage(pFrameRGB, ip, dst_width, dst_height); int ret = save_jpg(ip, filename); if (0 != ret) { av_log(NULL, AV_LOG_ERROR, " save_jpg failed: %s\n", filename); goto cleanup; } cleanup: if (NULL != ip) gdImageDestroy(ip); if (NULL != pSwsCtx) sws_freeContext(pSwsCtx); // do we need to do this? if (NULL != rgb_buffer) av_free(rgb_buffer); if (NULL != pFrameRGB) av_free(pFrameRGB); } /* av_pkt_dump_log()?? */ void dump_packet(AVPacket *p, AVStream * ps) { /* from av_read_frame() pkt->pts, pkt->dts and pkt->duration are always set to correct values in AVStream.timebase units (and guessed if the format cannot provided them). pkt->pts can be AV_NOPTS_VALUE if the video format has B frames, so it is better to rely on pkt->dts if you do not decompress the payload. */ av_log(NULL, AV_LOG_VERBOSE, "***dump_packet: pos:%"PRId64"\n", p->pos); av_log(NULL, AV_LOG_VERBOSE, "pts tb: %"PRId64", dts tb: %"PRId64", duration tb: %d\n", p->pts, p->dts, p->duration); av_log(NULL, AV_LOG_VERBOSE, "pts s: %.2f, dts s: %.2f, duration s: %.2f\n", p->pts * av_q2d(ps->time_base), p->dts * av_q2d(ps->time_base), p->duration * av_q2d(ps->time_base)); // pts can be AV_NOPTS_VALUE } void dump_codec_context(AVCodecContext * p) { av_log(NULL, AV_LOG_VERBOSE, "***dump_codec_context %s, time_base: %d / %d\n", p->codec_name, p->time_base.num, p->time_base.den); av_log(NULL, AV_LOG_VERBOSE, "frame_number: %d, width: %d, height: %d, sample_aspect_ratio %d/%d%s\n", p->frame_number, p->width, p->height, p->sample_aspect_ratio.num, p->sample_aspect_ratio.den, (0 == p->sample_aspect_ratio.num) ? "" : "**a**"); } void dump_index_entries(AVStream * p) { // index_entries are only used if the format does not support seeking natively int i; double diff = 0; for (i=0; i < p->nb_index_entries; i++) { AVIndexEntry *e = p->index_entries + i; double prev_ts = 0, cur_ts = 0; cur_ts = e->timestamp * av_q2d(p->time_base); //assert(cur_ts > 0); diff += cur_ts - prev_ts; if (i < 20) { // show only first 20 av_log(NULL, AV_LOG_VERBOSE, " i: %d, pos: %"PRId64", timestamp tb: %"PRId64", timestamp s: %.2f, flags: %d, size: %d, min_distance: %d\n", i, e->pos, e->timestamp, e->timestamp * av_q2d(p->time_base), e->flags, e->size, e->min_distance); } prev_ts = cur_ts; } av_log(NULL, AV_LOG_VERBOSE, " *** nb_index_entries: %d, avg. timestamp s diff: %.2f\n", p->nb_index_entries, diff / p->nb_index_entries); } void dump_stream(AVStream * p) { av_log(NULL, AV_LOG_VERBOSE, "***dump_stream, time_base: %d / %d\n", p->time_base.num, p->time_base.den); av_log(NULL, AV_LOG_VERBOSE, "cur_dts tb?: %"PRId64", start_time tb: %"PRId64", duration tb: %"PRId64", nb_frames: %"PRId64"\n", p->cur_dts, p->start_time, p->duration, p->nb_frames); // get funny results here. use format_context's. av_log(NULL, AV_LOG_VERBOSE, "cur_dts s?: %.2f, start_time s: %.2f, duration s: %.2f\n", p->cur_dts * av_q2d(p->time_base), p->start_time * av_q2d(p->time_base), p->duration * av_q2d(p->time_base)); // duration can be AV_NOPTS_VALUE // field pts in AVStream is for encoding } /* set scale source width & height (scaled_w and scaled_h) */ void calc_scale_src(int width, int height, AVRational ratio, int *scaled_w, int *scaled_h) { // mplayer dont downscale horizontally. however, we'll always scale // horizontally, up or down, which is the same as mpc's display and // vlc's snapshot. this should make square pixel for both pal & ntsc. *scaled_w = width; *scaled_h = height; if (0 != ratio.num) { // ratio is defined assert(ratio.den != 0); *scaled_w = av_q2d(ratio) * width + 0.5; // round nearest } } /* modified from libavformat's dump_format */ void get_stream_info_type(AVFormatContext *ic, enum CodecType type, char *buf, AVRational sample_aspect_ratio) { char sub_buf[1024] = ""; // FIXME unsigned int i; for(i=0; inb_streams; i++) { char codec_buf[256]; int flags = ic->iformat->flags; AVStream *st = ic->streams[i]; if (type != st->codec->codec_type) { continue; } if (CODEC_TYPE_SUBTITLE == st->codec->codec_type) { if (strlen(st->language) > 0) { sprintf(sub_buf + strlen(sub_buf), "%s ", st->language); } else { // FIXME: ignore for now; language seem to be missing in .vob files //sprintf(sub_buf + strlen(sub_buf), "? "); } continue; } if (gb_v_verbose > 0) { sprintf(buf + strlen(buf), "Stream %d", i); if (flags & AVFMT_SHOW_IDS) { sprintf(buf + strlen(buf), "[0x%x]", st->id); } /* int g = ff_gcd(st->time_base.num, st->time_base.den); sprintf(buf + strlen(buf), ", %d/%d", st->time_base.num/g, st->time_base.den/g); */ sprintf(buf + strlen(buf), ": "); } avcodec_string(codec_buf, sizeof(codec_buf), st->codec, 0); // remove [PAR DAR] from string, it's not very useful. char *begin = NULL, *end = NULL; if ((begin=strstr(codec_buf, " [PAR")) != NULL && (end=strchr(begin, ']')) != NULL) { while (*++end != '\0') { *begin++ = *end; } *begin = '\0'; } sprintf(buf + strlen(buf), codec_buf); if (st->codec->codec_type == CODEC_TYPE_VIDEO){ if (st->r_frame_rate.den && st->r_frame_rate.num) sprintf(buf + strlen(buf), ", %5.2f fps(r)", av_q2d(st->r_frame_rate)); //else if(st->time_base.den && st->time_base.num) // sprintf(buf + strlen(buf), ", %5.2f fps(m)", 1/av_q2d(st->time_base)); else sprintf(buf + strlen(buf), ", %5.2f fps(c)", 1/av_q2d(st->codec->time_base)); // show aspect ratio int scaled_src_width, scaled_src_height; calc_scale_src(st->codec->width, st->codec->height, sample_aspect_ratio, &scaled_src_width, &scaled_src_height); if (scaled_src_width != st->codec->width || scaled_src_height != st->codec->height) { sprintf(buf + strlen(buf), " => %dx%d", scaled_src_width, scaled_src_height); } } if (strlen(st->language) > 0) { sprintf(buf + strlen(buf), " (%s)", st->language); } sprintf(buf + strlen(buf), NEWLINE); } if (0 < strlen(sub_buf)) { sprintf(buf + strlen(buf), "Subtitles: %s\n", sub_buf); } } /* modified from libavformat's dump_format */ char *get_stream_info(AVFormatContext *ic, char *url, int strip_path, AVRational sample_aspect_ratio) { static char buf[4096]; // FIXME: this is also used for all text at the top int duration = -1; char *file_name = url; if (1 == strip_path) { file_name = path_2_file(url); } sprintf(buf, "File: %s", file_name); //sprintf(buf + strlen(buf), " (%s)", ic->iformat->name); sprintf(buf + strlen(buf), "%sSize: %"PRId64" bytes (%s)", NEWLINE, ic->file_size, format_size(ic->file_size, "B")); if (ic->duration != AV_NOPTS_VALUE) { // FIXME: gcc warning: comparison between signed and unsigned int hours, mins, secs; duration = secs = ic->duration / AV_TIME_BASE; mins = secs / 60; secs %= 60; hours = mins / 60; mins %= 60; sprintf(buf + strlen(buf), ", duration: %02d:%02d:%02d", hours, mins, secs); } else { sprintf(buf + strlen(buf), ", duration: N/A"); } /* if (ic->start_time != AV_NOPTS_VALUE) { int secs, us; secs = ic->start_time / AV_TIME_BASE; us = ic->start_time % AV_TIME_BASE; sprintf(buf + strlen(buf), ", start: %d.%06d", secs, (int)av_rescale(us, 1000000, AV_TIME_BASE)); } */ // some formats, eg. flv, dont seem to support bit_rate, so we'll prefer to // calculate from duration. // is this ok? probably not ok with .vob files when duration is wrong. DEBUG if (duration > 0) { sprintf(buf + strlen(buf), ", avg.bitrate: %.0f kb/s%s", (double) ic->file_size * 8 / duration / 1000, NEWLINE); } else if (ic->bit_rate) { sprintf(buf + strlen(buf), ", bitrate: %d kb/s%s", ic->bit_rate / 1000, NEWLINE); } else { sprintf(buf + strlen(buf), ", bitrate: N/A%s", NEWLINE); } get_stream_info_type(ic, CODEC_TYPE_AUDIO, buf, sample_aspect_ratio); get_stream_info_type(ic, CODEC_TYPE_VIDEO, buf, sample_aspect_ratio); get_stream_info_type(ic, CODEC_TYPE_SUBTITLE, buf, sample_aspect_ratio); // CODEC_TYPE_DATA FIXME: what is this type? // CODEC_TYPE_NB FIXME: what is this type? //strfmon(buf + strlen(buf), 100, "strfmon: %!i\n", ic->file_size); return buf; } void dump_format_context(AVFormatContext *p, int __attribute__((unused)) index, char *url, int __attribute__((unused)) is_output) { //av_log(NULL, AV_LOG_ERROR, "\n"); av_log(NULL, AV_LOG_VERBOSE, "***dump_format_context, name: %s, long_name: %s\n", p->iformat->name, p->iformat->long_name); //dump_format(p, index, url, is_output); // dont show scaling info at this time because we dont have the proper sample_aspect_ratio av_log(NULL, LOG_INFO, get_stream_info(p, url, 0, GB_A_RATIO)); av_log(NULL, AV_LOG_VERBOSE, "start_time av: %"PRId64", duration av: %"PRId64", file_size: %"PRId64"\n", p->start_time, p->duration, p->file_size); av_log(NULL, AV_LOG_VERBOSE, "start_time s: %.2f, duration s: %.2f\n", (double) p->start_time / AV_TIME_BASE, (double) p->duration / AV_TIME_BASE); if (p->track != 0) av_log(NULL, LOG_INFO, " Track: %d\n", p->track); if (p->title[0] != '\0') av_log(NULL, LOG_INFO, " Title: %s\n", p->title); if (p->author[0] != '\0') av_log(NULL, LOG_INFO, " Author: %s\n", p->author); if (p->copyright[0] != '\0') av_log(NULL, LOG_INFO, " Copyright: %s\n", p->copyright); if (p->comment[0] != '\0') av_log(NULL, LOG_INFO, " Comment: %s\n", p->comment); if (p->album[0] != '\0') av_log(NULL, LOG_INFO, " Album: %s\n", p->album); if (p->year != 0) av_log(NULL, LOG_INFO, " Year: %d\n", p->year); if (p->genre[0] != '\0') av_log(NULL, LOG_INFO, " Genre: %s\n", p->genre); } /* */ double uint8_cmp(uint8_t *pa, uint8_t *pb, uint8_t *pc, int n) { int i, same = 0; for (i=0; i -20) && (diffab < 20) && (diffac > -20) && (diffac < 20) && (diffbc > -20) && (diffbc < 20)) { same++; } } return (double)same / n; } /* return sameness of the frame; 1 means the frame is the same in all directions, i.e. blank pFrame must be an PIX_FMT_RGB24 frame */ double blank_frame(AVFrame *pFrame, int width, int height) { uint8_t *src = pFrame->data[0]; int hor_size = height/11 * width * 3; uint8_t *pa = src+hor_size*2; uint8_t *pb = src+hor_size*5; uint8_t *pc = src+hor_size*8; double same = .4*uint8_cmp(pa, pb, pc, hor_size); int ver_size = hor_size/3; same += .6/3*uint8_cmp(pa, pa + ver_size, pa + ver_size*2, ver_size); same += .6/3*uint8_cmp(pb, pb + ver_size, pb + ver_size*2, ver_size); same += .6/3*uint8_cmp(pc, pc + ver_size, pc + ver_size*2, ver_size); return same; } /* global */ uint64_t gb_video_pkt_pts = AV_NOPTS_VALUE; /* These are called whenever we allocate a frame * buffer. We use this to store the global_pts in * a frame at the time it is allocated. */ int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) { int ret = avcodec_default_get_buffer(c, pic); uint64_t *pts = av_malloc(sizeof(uint64_t)); *pts = gb_video_pkt_pts; pic->opaque = pts; av_log(NULL, AV_LOG_VERBOSE, "*coping gb_video_pkt_pts: %"PRId64" to opaque\n", gb_video_pkt_pts); return ret; } void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) { if(pic) av_freep(&pic->opaque); avcodec_default_release_buffer(c, pic); } /* set first to 1 when calling this for the first time of a file return >0 if can read packet(s) & decode a frame, *pPts is set to packet's pts return 0 if end of file return <0 if error */ int read_and_decode(AVFormatContext *pFormatCtx, int video_index, AVCodecContext *pCodecCtx, AVFrame *pFrame, int64_t *pPts, int key_only, int first) { //double pts = -99999; AVPacket packet; AVStream *pStream = pFormatCtx->streams[video_index]; int decoded_frame = 0; static int run = 0; // # of times read_and_decode has been called for a file static double avg_decoded_frame = 0; // average # of decoded frame static int skip_non_key = 0; if (first) { // reset stats run = 0; avg_decoded_frame = 0; skip_non_key = 0; } int got_picture; int pkt_without_pic = 0; // # of video packet read without getting a picture //for (got_picture = 0; 0 == got_picture; av_free_packet(&packet)) { // keep decoding until we get a key frame for (got_picture = 0; 0 == got_picture //|| (1 == key_only && !(1 == pFrame->key_frame && FF_I_TYPE == pFrame->pict_type)); // same as version 0.61 || (1 == key_only && !(1 == pFrame->key_frame || FF_I_TYPE == pFrame->pict_type)); // same as version 2.42 //|| (1 == key_only && 1 != pFrame->key_frame); // is there a reason why not use this? t_warhawk_review_gt_h264.mov (svq3) seems to set only pict_type av_free_packet(&packet)) { if (0 != av_read_frame(pFormatCtx, &packet)) { if (url_ferror(pFormatCtx->pb) != 0) { // from ffplay - not documented return -1; } return 0; } // Is this a packet from the video stream? if (packet.stream_index != video_index) { continue; } // skip all non-key packet? would this work? // FIXME // this seems to slow down nike files. why? // so we'll use it only when a key frame is difficult to find. // hope this wont break anything. :) // this seems to help a lot for files with vorbis audio if (1 == skip_non_key && 1 == key_only && !(packet.flags & PKT_FLAG_KEY)) { continue; } dump_packet(&packet, pStream); //dump_codec_context(pCodecCtx); // Save global pts to be stored in pFrame in first call av_log(NULL, AV_LOG_VERBOSE, "*saving gb_video_pkt_pts: %"PRId64"\n", packet.pts); gb_video_pkt_pts = packet.pts; // Decode video frame avcodec_decode_video(pCodecCtx, pFrame, &got_picture, packet.data, packet.size); // error is ignored. perhaps packets read are not enough. av_log(NULL, AV_LOG_VERBOSE, "*avcodec_decode_video: got_picture: %d, key_frame: %d, pict_type: %d\n", got_picture, pFrame->key_frame, pFrame->pict_type); // FIXME: with some .dat files, got_picture is never set, why?? if (0 == got_picture) { pkt_without_pic++; if (0 == pkt_without_pic%50) { av_log(NULL, LOG_INFO, " no picture in %d packets\n", pkt_without_pic); } if (1000 == pkt_without_pic) { // is 1000 enough? // FIXME av_log(NULL, AV_LOG_ERROR, " * avcodec_decode_video couldn't decode picture\n"); av_free_packet(&packet); return -1; } } else { pkt_without_pic = 0; decoded_frame++; // some codecs, e.g avisyth, dont seem to set key_frame if (1 == key_only && 0 == decoded_frame%200) { av_log(NULL, LOG_INFO, " a key frame is not found in %d frames\n", decoded_frame); } if (1 == key_only && 400 == decoded_frame) { // is there a way to know when a frame has no missing pieces // even though it's not a key frame?? // FIXME av_log(NULL, LOG_INFO, " * using a non-key frame; file problem? ffmpeg's codec problem?\n"); break; } } // WTH?? why copy pts from dts? /* if(AV_NOPTS_VALUE == packet.dts && NULL != pFrame->opaque && AV_NOPTS_VALUE != *(uint64_t *) pFrame->opaque) { pts = *(uint64_t *)pFrame->opaque; } else if(packet.dts != AV_NOPTS_VALUE) { pts = packet.dts; } else { pts = 0; } pts *= av_q2d(pStream->time_base); //av_log(NULL, AV_LOG_VERBOSE, "*after avcodec_decode_video pts: %.2f\n", pts); */ } av_free_packet(&packet); // stats & enable skipping of non key packets run++; avg_decoded_frame = (avg_decoded_frame*(run-1) + decoded_frame) / run; //av_log(NULL, LOG_INFO, " decoded frames: %d, avg. decoded frames: %.2f, pict_type: %d\n", // decoded_frame, avg_decoded_frame, pFrame->pict_type); // DEBUG if (0 == skip_non_key && run >= 3 && avg_decoded_frame > 30) { skip_non_key = 1; av_log(NULL, LOG_INFO, " skipping non key packets for this file\n"); } av_log(NULL, AV_LOG_VERBOSE, "*****got picture, repeat_pict: %d%s, key_frame: %d, pict_type: %d\n", pFrame->repeat_pict, (pFrame->repeat_pict > 0) ? "**r**" : "", pFrame->key_frame, pFrame->pict_type); if(NULL != pFrame->opaque && AV_NOPTS_VALUE != *(uint64_t *) pFrame->opaque) { //av_log(NULL, AV_LOG_VERBOSE, "*pts: %.2f, value in opaque: %"PRId64"\n", pts, *(uint64_t *) pFrame->opaque); av_log(NULL, AV_LOG_VERBOSE, "*value in opaque: %"PRId64"\n", *(uint64_t *) pFrame->opaque); } dump_stream(pStream); dump_codec_context(pCodecCtx); *pPts = packet.pts; return 1; } /* calculate timestamp to display to users */ double calc_time(int64_t timestamp, AVRational time_base, double start_time) { // for files with start_time > 0, we need to subtract the start_time // from timestamp. this should match the time display by MPC & VLC. // however, for .vob files of dvds, after subtracting start_time // each file will start with timestamp 0 instead of continuing from the previous file. // for unknown start_time, we ignore it //if (start_time < 0) { // return av_rescale(timestamp, time_base.num, time_base.den); //} else { return av_rescale(timestamp, time_base.num, time_base.den) - start_time; //} } /* return the duration. guess when unknown. must be called after codec has been opened */ double guess_duration(AVFormatContext *pFormatCtx, int index, AVCodecContext __attribute__((unused)) *pCodecCtx, AVFrame __attribute__((unused)) *pFrame) { double duration = (double) pFormatCtx->duration / AV_TIME_BASE; // can be incorrect for .vob files if (duration > 0) { return duration; } AVStream *pStream = pFormatCtx->streams[index]; double guess; // if stream bitrate is known we'll interpolate from file size. // pFormatCtx->start_time would be incorrect for .vob file with multiple titles. // pStream->start_time doesn't work either. so we'll need to disable timestamping. assert(NULL != pStream && NULL != pStream->codec); if (pStream->codec->bit_rate > 0 && pFormatCtx->file_size > 0) { guess = 0.9 * pFormatCtx->file_size / (pStream->codec->bit_rate / 8); if (guess > 0) { av_log(NULL, AV_LOG_ERROR, " ** duration is unknown: %.2f; guessing: %.2f s from bit_rate\n", duration, guess); return guess; } } return -1; // the following doesn't work. /* // we'll guess the duration by seeking to near the end of the file and // decode a frame. the timestamp of that frame is the guess. // things get more complicated for dvd's .vob files. each .vob file // can contain more than 1 title. and each title will have its own pts. // for example, 90% of a .vob might be for title 1 and the last 10% // might be for title 2; seeking to near the end will end up getting // title 2's pts. this problem cannot be solved if we just look at the // .vob files. need to process other info outside .vob files too. // as a result, the following will probably never work. // .vob files weirdness will make our assumption to seek by byte incorrect too. if (pFormatCtx->file_size <= 0) { return -1; } int64_t byte_pos = 0.9 * pFormatCtx->file_size; int ret = av_seek_frame(pFormatCtx, index, byte_pos, AVSEEK_FLAG_BYTE); if (ret < 0) { // failed return -1; } avcodec_flush_buffers(pCodecCtx); int64_t pts; ret = read_and_decode(pFormatCtx, index, pCodecCtx, pFrame, &pts, 0); // FIXME: key or not? if (ret <= 0) { // end of file or error av_log(NULL, AV_LOG_VERBOSE, " read_and_decode during guessing duration failed\n"); return -1; } double start_time = (double) pFormatCtx->start_time / AV_TIME_BASE; // FIXME: can be unknown? guess = calc_time(pts, pStream->time_base, start_time); if (guess <= 0) { return -1; } av_log(NULL, AV_LOG_ERROR, " ** duration is unknown: %.2f; guessing: %.2f s.\n", duration, guess); // seek back to 0 & flush buffer; FIXME: is 0 correct? av_seek_frame(pFormatCtx, index, 0, AVSEEK_FLAG_BYTE); // ignore errors avcodec_flush_buffers(pCodecCtx); return guess; */ } /* try hard to seek assume flags can be either 0 or AVSEEK_FLAG_BACKWARD */ int really_seek(AVFormatContext *pFormatCtx, int index, int64_t timestamp, int flags, double duration) { assert(flags == 0 || flags == AVSEEK_FLAG_BACKWARD); int ret; /* first try av_seek_frame */ ret = av_seek_frame(pFormatCtx, index, timestamp, flags); if (ret >= 0) { // success return ret; } /* then we try seeking to any (non key) frame AVSEEK_FLAG_ANY */ ret = av_seek_frame(pFormatCtx, index, timestamp, flags | AVSEEK_FLAG_ANY); if (ret >= 0) { // success av_log(NULL, LOG_INFO, "AVSEEK_FLAG_ANY: timestamp: %"PRId64"\n", timestamp); // DEBUG return ret; } /* and then we try seeking by byte (AVSEEK_FLAG_BYTE) */ // here we assume that the whole file has duration seconds. // so we'll interpolate accordingly. AVStream *pStream = pFormatCtx->streams[index]; int64_t duration_tb = duration / av_q2d(pStream->time_base); // in time_base unit double start_time = (double) pFormatCtx->start_time / AV_TIME_BASE; // in seconds // if start_time is negative, we ignore it; FIXME: is this ok? if (start_time < 0) { start_time = 0; } // normally when seeking by timestamp we add start_time to timestamp // before seeking, but seeking by byte we need to subtract the added start_time timestamp -= start_time / av_q2d(pStream->time_base); if (pFormatCtx->file_size <= 0) { return -1; } if (duration > 0) { int64_t byte_pos = av_rescale(timestamp, pFormatCtx->file_size, duration_tb); av_log(NULL, LOG_INFO, "AVSEEK_FLAG_BYTE: byte_pos: %"PRId64", timestamp: %"PRId64", file_size: %"PRId64", duration_tb: %"PRId64"\n", byte_pos, timestamp, pFormatCtx->file_size, duration_tb); return av_seek_frame(pFormatCtx, index, byte_pos, AVSEEK_FLAG_BYTE); } return -1; } /* modify name so that it'll (hopefully) be unique by inserting a unique string before suffix. if unum is != 0, it'll be used returns the unique number */ int make_unique_name(char *name, char *suffix, int unum) { // tmpnam() in mingw always return names which start with \ -- unuseable. // so we'll use random number instead. char unique[FILENAME_MAX]; if (unum == 0) { unum = rand(); } sprintf(unique, "_%d", unum); char *found = strlaststr(name, suffix); if (NULL == found || found == name) { strcat(name, unique); // this shouldn't happen } else { strcat(unique, found); strcpy(found, unique); } return unum; } /* */ void make_thumbnail(char *file) { av_log(NULL, AV_LOG_VERBOSE, "make_thumbnail: %s\n", file); static int nb_file = 0; // FIXME: static nb_file++; struct timeval tstart; gettimeofday(&tstart, NULL); int i; thumbnail tn; // thumbnail data & info thumb_new(&tn); // shot sh; // shot info shot fill_buffer[gb_c_column-1]; // skipped shots to fill the last row for (i=0; i 0) { strcpy_va(tn.out_filename, 3, gb_O_outdir, "/", path_2_file(file)); if (NULL != gb_N_suffix) strcpy_va(tn.info_filename, 3, gb_O_outdir, "/", path_2_file(file)); } else { strcpy(tn.out_filename, file); if (NULL != gb_N_suffix) strcpy(tn.info_filename, file); } char *suffix = strrchr(tn.out_filename, '.'); if (NULL == suffix) { strcat(tn.out_filename, gb_o_suffix); } else { strcpy(suffix, gb_o_suffix); } if (NULL != gb_N_suffix) { suffix = strrchr(tn.info_filename, '.'); if (NULL == suffix) { strcat(tn.info_filename, gb_N_suffix); } else { strcpy(suffix, gb_N_suffix); } } // if output files exist and modified time >= program start time, // we'll not overwrite and use a new name int unum = 0; if (is_reg_newer(tn.out_filename, gb_st_start)) { unum = make_unique_name(tn.out_filename, gb_o_suffix, unum); av_log(NULL, LOG_INFO, "%s: output file already exists. using: %s\n", gb_argv0, tn.out_filename); } if (NULL != gb_N_suffix && is_reg_newer(tn.info_filename, gb_st_start)) { unum = make_unique_name(tn.info_filename, gb_N_suffix, unum); av_log(NULL, LOG_INFO, "%s: info file already exists. using: %s\n", gb_argv0, tn.info_filename); } if (0 == gb_W_overwrite) { // dont overwrite mode if (is_reg(tn.out_filename)) { av_log(NULL, LOG_INFO, "%s: output file %s already exists. omitted.\n", gb_argv0, tn.out_filename); goto cleanup; } if (NULL != gb_N_suffix && is_reg(tn.info_filename)) { av_log(NULL, LOG_INFO, "%s: info file %s already exists. omitted.\n", gb_argv0, tn.info_filename); goto cleanup; } } #if defined(WIN32) && defined(_UNICODE) wchar_t out_filename_w[FILENAME_MAX]; UTF8_2_WC(out_filename_w, tn.out_filename, FILENAME_MAX); wchar_t info_filename_w[FILENAME_MAX]; UTF8_2_WC(info_filename_w, tn.info_filename, FILENAME_MAX); #else char *out_filename_w = tn.out_filename; char *info_filename_w = tn.info_filename; #endif out_fp = _tfopen(out_filename_w, _TEXT("wb")); if (NULL == out_fp) { av_log(NULL, AV_LOG_ERROR, "\n%s: creating output image '%s' failed: %s\n", gb_argv0, tn.out_filename, strerror(errno)); goto cleanup; } if (NULL != gb_N_suffix) { info_fp = _tfopen(info_filename_w, _TEXT("wb")); if (NULL == info_fp) { av_log(NULL, AV_LOG_ERROR, "\n%s: creating info file '%s' failed: %s\n", gb_argv0, tn.info_filename, strerror(errno)); goto cleanup; } } // Open video file ret = av_open_input_file(&pFormatCtx, file, NULL, 0, NULL); if (0 != ret) { av_log(NULL, AV_LOG_ERROR, "\n%s: av_open_input_file %s failed: %d\n", gb_argv0, file, ret); goto cleanup; } // generate pts?? -- from ffplay, not documented // it should make av_read_frame() generate pts for unknown value assert(NULL != pFormatCtx); pFormatCtx->flags |= AVFMT_FLAG_GENPTS; // Retrieve stream information ret = av_find_stream_info(pFormatCtx); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, "\n%s: av_find_stream_info %s failed: %d\n", gb_argv0, file, ret); goto cleanup; } dump_format_context(pFormatCtx, nb_file, file, 0); // Find the first video stream // int av_find_default_stream_index(AVFormatContext *s) int video_index = -1; for (i = 0; i < pFormatCtx->nb_streams; i++) { if (CODEC_TYPE_VIDEO == pFormatCtx->streams[i]->codec->codec_type) { video_index = i; break; } } if (video_index == -1) { av_log(NULL, AV_LOG_ERROR, " couldn't find a video stream\n"); goto cleanup; } AVStream *pStream = pFormatCtx->streams[video_index]; pCodecCtx = pStream->codec; dump_stream(pStream); dump_index_entries(pStream); dump_codec_context(pCodecCtx); av_log(NULL, AV_LOG_VERBOSE, "\n"); // Find the decoder for the video stream AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { av_log(NULL, AV_LOG_ERROR, " couldn't find a decoder for codec_id: %d\n", pCodecCtx->codec_id); goto cleanup; } // discard frames; is this OK?? // FIXME if (gb_s_step >= 0) { // nonkey & bidir cause program crash with some files, e.g. tokyo 275 . // codec bugs??? //pCodecCtx->skip_frame = AVDISCARD_NONKEY; // slower with nike 15-11-07 //pCodecCtx->skip_frame = AVDISCARD_BIDIR; // this seems to speed things up pCodecCtx->skip_frame = AVDISCARD_NONREF; // internal err msg but not crash } // Open codec ret = avcodec_open(pCodecCtx, pCodec); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, " couldn't open codec %s id %d: %d\n", pCodec->name, pCodec->id, ret); goto cleanup; } pCodecCtx->get_buffer = our_get_buffer; pCodecCtx->release_buffer = our_release_buffer; // Allocate video frame pFrame = avcodec_alloc_frame(); if (pFrame == NULL) { av_log(NULL, AV_LOG_ERROR, " couldn't allocate a video frame\n"); goto cleanup; } // keep a copy of sample_aspect_ratio because it might be changed after // decoding a frame, e.g. Dragonball Z 001 (720x480 H264 AAC).mkv // is this a codec bug? it seem this value can be in the header or in the stream. AVRational sample_aspect_ratio = pCodecCtx->sample_aspect_ratio; double duration = (double) pFormatCtx->duration / AV_TIME_BASE; // can be unknown & can be incorrect (e.g. .vob files) if (duration <= 0) { duration = guess_duration(pFormatCtx, video_index, pCodecCtx, pFrame); // have to turn timestamping off because it'll be incorrect if (1 == gb_t_timestamp) { // on t_timestamp = 0; av_log(NULL, AV_LOG_ERROR, " turning time stamp off because of duration\n"); } } if (duration <= 0) { av_log(NULL, AV_LOG_ERROR, " duration is unknown: %.2f\n", duration); goto cleanup; } int64_t duration_tb = duration / av_q2d(pStream->time_base); // in time_base unit double start_time = (double) pFormatCtx->start_time / AV_TIME_BASE; // in seconds // VTS_01_2.VOB & beyond from DVD seem to be like this //if (start_time > duration) { //av_log(NULL, AV_LOG_VERBOSE, " start_time: %.2f is more than duration: %.2f\n", start_time, duration); //goto cleanup; //} // if start_time is negative, we ignore it; FIXME: is this ok? if (start_time < 0) { start_time = 0; } int64_t start_time_tb = start_time * pStream->time_base.den / pStream->time_base.num; // in time_base unit //av_log(NULL, AV_LOG_ERROR, " start_time_tb: %"PRId64"\n", start_time_tb); // decode the first frame without seeking. // without doing this, avcodec_decode_video wont be able to decode any picture // with some files, eg. http://download.pocketmovies.net/movies/3d/twittwit_320x184.mpg // bug reported by: swmaherl, jake_o from sourceforge // and pCodecCtx->width and pCodecCtx->height might not be correct without this // for .flv files. bug reported by: dragonbook int64_t found_pts = -1; int64_t first_pts = -1; // pts of first frame ret = read_and_decode(pFormatCtx, video_index, pCodecCtx, pFrame, &first_pts, 0, 1); if (0 == ret) { // end of file goto eof; } else if (ret < 0) { // error av_log(NULL, AV_LOG_ERROR, " read_and_decode first failed!\n"); goto cleanup; } //av_log(NULL, LOG_INFO, "first_pts: %"PRId64" (%.2f s)\n", first_pts, calc_time(first_pts, pStream->time_base, start_time)); // DEBUG // set sample_aspect_ratio // assuming sample_y = display_y if (gb_a_ratio.num != 0) { // use cmd line arg if specified sample_aspect_ratio.num = (double) pCodecCtx->height * av_q2d(gb_a_ratio) / pCodecCtx->width * 10000; sample_aspect_ratio.den = 10000; av_log(NULL, LOG_INFO, " *** using sample_aspect_ratio: %d/%d because of -a %.4f option\n", sample_aspect_ratio.num, sample_aspect_ratio.den, av_q2d(gb_a_ratio)); } else { if (sample_aspect_ratio.num != 0 && pCodecCtx->sample_aspect_ratio.num != 0 && av_q2d(sample_aspect_ratio) != av_q2d(pCodecCtx->sample_aspect_ratio)) { av_log(NULL, LOG_INFO, " *** conflicting sample_aspect_ratio: %.2f vs %.2f: using %.2f\n", av_q2d(sample_aspect_ratio), av_q2d(pCodecCtx->sample_aspect_ratio), av_q2d(sample_aspect_ratio)); av_log(NULL, LOG_INFO, " to use sample_aspect_ratio %.2f use: -a %.4f option\n", av_q2d(pCodecCtx->sample_aspect_ratio), av_q2d(pCodecCtx->sample_aspect_ratio) * pCodecCtx->width / pCodecCtx->height); // we'll continue with existing value. is this ok? FIXME // this is the same as mpc's and vlc's. } if (sample_aspect_ratio.num == 0) { // not defined sample_aspect_ratio = pCodecCtx->sample_aspect_ratio; } } /* calc options */ // FIXME: make sure values are ok when movies are very short or very small double net_duration; if (gb_C_cut > 0) { net_duration = gb_C_cut; if (net_duration + gb_B_begin > duration) { net_duration = duration - gb_B_begin; av_log(NULL, AV_LOG_ERROR, " -C %.2f s is too long, using %.2f s.\n", gb_C_cut, net_duration); } } else { //double net_duration = duration - start_time - gb_B_begin - gb_E_end; net_duration = duration - gb_B_begin - gb_E_end; // DVD if (net_duration <= 0) { av_log(NULL, AV_LOG_ERROR, " duration: %.2f s, net duration after -B & -E is negative: %.2f s.\n", duration, net_duration); goto cleanup; } } /* scale according to sample_aspect_ratio. */ int scaled_src_width, scaled_src_height; calc_scale_src(pCodecCtx->width, pCodecCtx->height, sample_aspect_ratio, &scaled_src_width, &scaled_src_height); if (scaled_src_width != pCodecCtx->width || scaled_src_height != pCodecCtx->height) { av_log(NULL, LOG_INFO, " * scaling input * %dx%d => %dx%d according to sample_aspect_ratio %d/%d\n", pCodecCtx->width, pCodecCtx->height, scaled_src_width, scaled_src_height, sample_aspect_ratio.num, sample_aspect_ratio.den); } tn.column = gb_c_column + 1; // will be -1 in the loop int seek_mode = 1; // 1 = seek; 0 = non-seek tn.step = -99999; // seconds tn.row = -99999; tn.width = -99999; tn.shot_width = -99999; tn.shot_height = -99999; // reduce # of column until we meet minimum height except when movie is too small while (tn.shot_height < gb_h_height && tn.column > 0 && tn.shot_width != scaled_src_width) { tn.column--; if (gb_s_step == 0) { // step evenly to get column x row tn.step = net_duration / (tn.column * gb_r_row + 1); } else { tn.step = gb_s_step; } if (gb_r_row > 0) { tn.row = gb_r_row; // if # of columns is reduced, we should increase # of rows so # of tiles would be almost the same // could some users not want this? } else { // as many rows as needed tn.row = floor(net_duration / tn.column / tn.step + 0.5); // round nearest } if (tn.row < 1) { tn.row = 1; } // make sure last row is full tn.step = net_duration / (tn.column * tn.row + 1); int full_width = tn.column * (scaled_src_width + gb_g_gap) + gb_g_gap; if (gb_w_width > 0 && gb_w_width < full_width) { tn.width = gb_w_width; } else { tn.width = full_width; } tn.shot_width = floor((tn.width - gb_g_gap*(tn.column+1)) / (double)tn.column + 0.5); // round nearest tn.shot_width -= tn.shot_width%2; // floor to even number tn.shot_height = floor((double) scaled_src_height / scaled_src_width * tn.shot_width + 0.5); // round nearest tn.shot_height -= tn.shot_height%2; // floor to even number tn.center_gap = (tn.width - gb_g_gap*(tn.column+1) - tn.shot_width * tn.column) / 2.0; } if (tn.step == 0) { av_log(NULL, AV_LOG_ERROR, " step is zero; movie is too short?\n"); goto cleanup; } if (tn.column != gb_c_column) { av_log(NULL, LOG_INFO, " changing # of column to %d to meet minimum height of %d; see -h option\n", tn.column, gb_h_height); } if (gb_w_width > 0 && gb_w_width != tn.width) { av_log(NULL, LOG_INFO, " changing width to %d to match movie's size (%dx%d)\n", tn.width, scaled_src_width, tn.column); } char *all_text = get_stream_info(pFormatCtx, file, 1, sample_aspect_ratio); // FIXME: using function's static buffer if (NULL != info_fp) { fprintf(info_fp, all_text); } if (0 == gb_i_info) { // off *all_text = '\0'; } if (NULL != gb_T_text) { all_text = strcat(all_text, gb_T_text); if (NULL != info_fp) { fprintf(info_fp, "%s%s", gb_T_text, NEWLINE); } } tn.txt_height = image_string_height(all_text, gb_f_fontname, gb_F_info_font_size) + gb_g_gap; tn.height = tn.shot_height*tn.row + gb_g_gap*(tn.row+1) + tn.txt_height; av_log(NULL, LOG_INFO, " step: %d s; # tiles: %dx%d, tile size: %dx%d; total size: %dx%d\n", tn.step, tn.column, tn.row, tn.shot_width, tn.shot_height, tn.width, tn.height); // jpeg seems to have max size of 65500 pixels if (tn.width > 65500 || tn.height > 65500) { av_log(NULL, AV_LOG_ERROR, " jpeg only supports max size of 65500\n"); goto cleanup; } int evade_step = MIN(10, tn.step / 14); // seconds to evade blank screen ; max 10 s // FIXME: what's the min value? 1? if (evade_step <= 0) { av_log(NULL, LOG_INFO, " step is less than 14 s; blank & blur evasion is turned off.\n"); } /* prepare for resize & conversion to PIX_FMT_RGB24 */ pFrameRGB = avcodec_alloc_frame(); if (pFrameRGB == NULL) { av_log(NULL, AV_LOG_ERROR, " couldn't allocate a video frame\n"); goto cleanup; } int rgb_bufsize = avpicture_get_size(PIX_FMT_RGB24, tn.shot_width, tn.shot_height); rgb_buffer = av_malloc(rgb_bufsize); if (NULL == rgb_buffer) { av_log(NULL, AV_LOG_ERROR, " av_malloc %d bytes failed\n", rgb_bufsize); goto cleanup; } avpicture_fill((AVPicture *) pFrameRGB, rgb_buffer, PIX_FMT_RGB24, tn.shot_width, tn.shot_height); pSwsCtx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, tn.shot_width, tn.shot_height, PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); if (NULL == pSwsCtx) { // sws_getContext is not documented av_log(NULL, AV_LOG_ERROR, " sws_getContext failed\n"); goto cleanup; } /* create the output image */ tn.out_ip = gdImageCreateTrueColor(tn.width, tn.height); if (NULL == tn.out_ip) { av_log(NULL, AV_LOG_ERROR, " gdImageCreateTrueColor failed: width %d, height %d\n", tn.width, tn.height); goto cleanup; } int background = gdImageColorResolve(tn.out_ip, gb_k_bcolor.r, gb_k_bcolor.g, gb_k_bcolor.b); // set backgroud gdImageFilledRectangle(tn.out_ip, 0, 0, tn.width, tn.height, background); /* add info & text */ // do this early so when font is not found we'll quit early if (NULL != all_text && strlen(all_text) > 0) { char *str_ret = image_string(tn.out_ip, gb_f_fontname, gb_F_info_color, gb_F_info_font_size, gb_L_info_location, gb_g_gap, all_text, 0, COLOR_WHITE); if (NULL != str_ret) { av_log(NULL, AV_LOG_ERROR, " %s; font problem? see -f option\n", str_ret); goto cleanup; } } /* alloc dynamic thumb data */ if (-1 == thumb_alloc_dynamic(&tn)) { av_log(NULL, AV_LOG_ERROR, " thumb_alloc_dynamic failed\n"); goto cleanup; } if (1 == gb_z_seek) { seek_mode = 1; } if (1 == gb_Z_nonseek) { seek_mode = 0; av_log(NULL, LOG_INFO, " *** using non-seek mode -- slower but more accurate timing.\n"); } /* decode & fill in the shots */ restart: seek_mode = seek_mode; // target for restart if (0 == seek_mode && gb_B_begin > 10) { av_log(NULL, LOG_INFO, " -B %.2f with non-seek mode will take some time.\n", gb_B_begin); } int64_t seek_target, seek_evade = 0; // in time_base unit int evade_try = 0; // blank screen evasion index double avg_evade_try = 0; // average int direction = 0; // seek direction (seek flags) seek_target = (tn.step + start_time + gb_B_begin) / av_q2d(pStream->time_base); int idx = 0; // idx = thumb_idx int thumb_nb = tn.row * tn.column; // thumb_nb = # of shots we need int64_t prevshot_pts = -1; // pts of previous good shot int64_t prevfound_pts = -1; // pts of previous decoding gdImagePtr edge_ip = NULL; // edge image for (idx = 0; idx < thumb_nb; idx++) { int64_t eff_target = seek_target + seek_evade; // effective target eff_target = MAX(eff_target, start_time_tb); // make sure eff_target > start_time char time_tmp[15]; // FIXME format_time(calc_time(eff_target, pStream->time_base, start_time), time_tmp, ':'); /* for some formats, previous seek might over shoot pass this seek_target; is this a bug in libavcodec? */ if (prevshot_pts > eff_target && 0 == evade_try) { av_log(NULL, LOG_INFO, " skipping shot at %s because of previous seek or evasions\n", time_tmp); idx--; thumb_nb--; goto skip_shot; } // make sure eff_target > previous found eff_target = MAX(eff_target, prevfound_pts+1); if (eff_target > duration_tb) { // end of file goto eof; } format_time(calc_time(eff_target, pStream->time_base, start_time), time_tmp, ':'); av_log(NULL, AV_LOG_VERBOSE, "\n***eff_target tb: %"PRId64", eff_target s:%.2f (%s), prevshot_pts: %"PRId64"\n", eff_target, calc_time(eff_target, pStream->time_base, start_time), time_tmp, prevshot_pts); /* jump to next shot */ //struct timeval dstart; // DEBUG //gettimeofday(&dstart, NULL); // calendar time; effected by load & io & etc. DEBUG if (1 == seek_mode) { // seek mode ret = really_seek(pFormatCtx, video_index, eff_target, direction, duration); if (ret < 0) { av_log(NULL, AV_LOG_ERROR, " seeking to to %.2f s failed\n", calc_time(eff_target, pStream->time_base, start_time)); goto cleanup; } avcodec_flush_buffers(pCodecCtx); ret = read_and_decode(pFormatCtx, video_index, pCodecCtx, pFrame, &found_pts, 1, 0); if (0 == ret) { // end of file goto eof; } else if (ret < 0) { // error av_log(NULL, AV_LOG_ERROR, " read_and_decode failed!\n"); goto cleanup; } //av_log(NULL, LOG_INFO, " found_pts: %"PRId64", eff_target: %"PRId64"\n", found_pts, eff_target); // DEBUG } else { // non-seek mode -- we keep decoding until we get to the next shot found_pts = 0; while (found_pts < eff_target) { // we should check if it's taking too long for this loop. FIXME ret = read_and_decode(pFormatCtx, video_index, pCodecCtx, pFrame, &found_pts, 0, 0); if (0 == ret) { // end of file goto eof; } else if (ret < 0) { // error av_log(NULL, AV_LOG_ERROR, " read_and_decode failed!\n"); goto cleanup; } //av_log(NULL, LOG_INFO, " found_pts: %"PRId64", eff_target: %"PRId64"\n", found_pts, eff_target); // DEBUG } } //struct timeval dfinish; // DEBUG //gettimeofday(&dfinish, NULL); // calendar time; effected by load & io & etc. DEBUG //double decode_time = (dfinish.tv_sec + dfinish.tv_usec/1000000.0) - (dstart.tv_sec + dstart.tv_usec/1000000.0); double decode_time = 0; double found_diff = (found_pts - eff_target) * av_q2d(pStream->time_base); //av_log(NULL, LOG_INFO, " found_diff: %.2f\n", found_diff); // DEBUG // if found frame is too far off from target, we'll disable seeking and start over if (idx < 5 && 1 == seek_mode && 0 == gb_z_seek // usually movies have key frames every 10 s && (tn.step < 15 || found_diff > 15) && (found_diff <= -tn.step || found_diff >= tn.step)) { // compute the approx. time it take for the non-seek mode, if too long print a msg instead double shot_dtime; if (scaled_src_width > 576*4/3.0) { // HD shot_dtime = tn.step * 30 / 30.0; } else if (scaled_src_width > 288*4/3.0) { // ~DVD shot_dtime = tn.step * 30 / 80.0; } else { // small shot_dtime = tn.step * 30 / 500.0; } if (shot_dtime > 2 || shot_dtime * tn.column * tn.row > 120) { av_log(NULL, LOG_INFO, " *** seeking off target %.2f s, increase time step or use non-seek mode.\n", found_diff); goto non_seek_too_long; } // disable seeking and start over av_seek_frame(pFormatCtx, video_index, 0, 0); avcodec_flush_buffers(pCodecCtx); seek_mode = 0; av_log(NULL, LOG_INFO, " *** switching to non-seek mode because seeking was off target by %.2f s.\n", found_diff); av_log(NULL, LOG_INFO, " non-seek mode is slower. increase time step or use -z if you dont want this.\n"); goto restart; } non_seek_too_long: nb_shots++; av_log(NULL, AV_LOG_VERBOSE, "shot %d: found_: %"PRId64" (%.2fs), eff_: %"PRId64" (%.2fs), dtime: %.3f\n", idx, found_pts, calc_time(found_pts, pStream->time_base, start_time), eff_target, calc_time(eff_target, pStream->time_base, start_time), decode_time); av_log(NULL, AV_LOG_VERBOSE, "approx. decoded frames/s %.2f\n", tn.step * 30 / decode_time); // DEBUG /* char debug_filename[2048]; // DEBUG sprintf(debug_filename, "%s_decoded%05d.jpg", tn.out_filename, nb_shots - 1); save_AVFrame(pFrame, pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, debug_filename, pCodecCtx->width, pCodecCtx->height); */ /* doesn't work very well -- tends to make evade_step too large if (evade_step > 0) { // this shot is the same as previous seek -- not good. if (found_pts == prevfound_pts) { // maybe evade_step is too small if (evade_step < 20) { // FIXME evade_step *= 1.2; // FIXME } } // found diffs from target > evade_step if (found_diff <= -evade_step || found_diff >= evade_step) { // maybe evade_step is too small if (evade_step < 20 && found_diff < 20) { // FIXME //evade_step = 1 + ceiling(found_diff); // FIXME } } } */ // got same picture as previous shot, we'll skip it if (prevshot_pts == found_pts && 0 == evade_try) { av_log(NULL, LOG_INFO, " skipping shot at %s because got previous shot\n", time_tmp); idx--; thumb_nb--; goto skip_shot; } /* convert to PIX_FMT_RGB24 & resize */ sws_scale(pSwsCtx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); /* sprintf(debug_filename, "%s_resized%05d.jpg", tn.out_filename, nb_shots - 1); // DEBUG save_AVFrame(pFrameRGB, tn.shot_width, tn.shot_height, PIX_FMT_RGB24, debug_filename, tn.shot_width, tn.shot_height); */ /* if blank screen, try again */ // FIXME: make sure this'll work when step is small // FIXME: make sure each shot wont get repeated double blank = blank_frame(pFrameRGB, tn.shot_width, tn.shot_height); // only do edge when blank detection doesn't work float edge[EDGE_PARTS] = {1,1,1,1,1,1}; // FIXME: change this if EDGE_PARTS is changed if (evade_step > 0 && blank <= gb_b_blank && gb_D_edge > 0) { edge_ip = detect_edge(pFrameRGB, tn.shot_width, tn.shot_height, edge, EDGE_FOUND); } //av_log(NULL, AV_LOG_VERBOSE, " idx: %d, evade_try: %d, blank: %.2f%s edge: %.3f %.3f %.3f %.3f %.3f %.3f%s\n", // idx, evade_try, blank, (blank > gb_b_blank) ? "**b**" : "", // edge[0], edge[1], edge[2], edge[3], edge[4], edge[5], is_edge(edge, EDGE_FOUND) ? "" : "**e**"); // DEBUG if (evade_step > 0 && (blank > gb_b_blank || !is_edge(edge, EDGE_FOUND))) { idx--; evade_try++; // we'll always search forward to support non-seek mode, which cant go backward // keep trying until getting close to next step seek_evade = evade_step * evade_try / av_q2d(pStream->time_base); if (seek_evade < (tn.step - evade_step) / av_q2d(pStream->time_base)) { // FIXME av_log(NULL, AV_LOG_VERBOSE, " * blank or no edge * try #%d: seeking forward seek_evade: %"PRId64" (%.2f s)\n", evade_try, seek_evade, seek_evade * av_q2d(pStream->time_base)); goto continue_cleanup; } // not found -- skip shot char time_tmp[15]; // FIXME format_time(calc_time(seek_target, pStream->time_base, start_time), time_tmp, ':'); av_log(NULL, LOG_INFO, " * blank %.2f or no edge * skipping shot at %s after %d tries\n", blank, time_tmp, evade_try); thumb_nb--; // reduce # shots goto skip_shot; } // avg_evade_try = (avg_evade_try * idx + evade_try ) / (idx+1); // DEBUG //av_log(NULL, AV_LOG_VERBOSE, " *** avg_evade_try: %.2f\n", avg_evade_try); // DEBUG /* convert to GD image */ ip = gdImageCreateTrueColor(tn.shot_width, tn.shot_height); if (NULL == ip) { av_log(NULL, AV_LOG_ERROR, " gdImageCreateTrueColor failed: width %d, height %d\n", tn.shot_width, tn.shot_height); goto cleanup; } FrameRGB_2_gdImage(pFrameRGB, ip, tn.shot_width, tn.shot_height); /* if debugging, save the edge instead */ if (gb_v_verbose > 0 && NULL != edge_ip) { gdImageDestroy(ip); ip = edge_ip; edge_ip = NULL; } /* timestamping */ // FIXME: this frame might not actually be at the requested position. is pts correct? if (1 == t_timestamp) { // on char time_str[15]; // FIXME format_time(calc_time(found_pts, pStream->time_base, start_time), time_str, ':'); char *str_ret = image_string(ip, gb_F_ts_fontname, gb_F_ts_color, gb_F_ts_font_size, gb_L_time_location, 0, time_str, 1, gb_F_ts_shadow); if (NULL != str_ret) { av_log(NULL, AV_LOG_ERROR, " %s; font problem? see -f option or -F option\n", str_ret); goto cleanup; // LEAK: ip, edge_ip } /* stamp idx & blank & edge for debugging */ if (gb_v_verbose > 0) { char idx_str[10]; // FIXME sprintf(idx_str, "idx: %d, blank: %.2f\n%.6f %.6f\n%.6f %.6f\n%.6f %.6f", idx, blank, edge[0], edge[1], edge[2], edge[3], edge[4], edge[5]); image_string(ip, gb_f_fontname, COLOR_WHITE, gb_F_ts_font_size, 2, 0, idx_str, 1, COLOR_BLACK); } } /* save individual shots */ if (1 == gb_I_individual) { char time_str[15]; // FIXME format_time(calc_time(found_pts, pStream->time_base, start_time), time_str, '_'); char individual_filename[UTF8_FILENAME_SIZE]; // FIXME strcpy(individual_filename, tn.out_filename); char *suffix = strstr(individual_filename, gb_o_suffix); assert(NULL != suffix); //sprintf(suffix, "_shot%05d.jpg", idx); // FIXME: hopefully 5 digits will be enough sprintf(suffix, "_%s_%05d.jpg", time_str, idx); // FIXME: hopefully 5 digits will be enough ret = save_jpg(ip, individual_filename); if (0 != ret) { // error av_log(NULL, AV_LOG_ERROR, " saving individual shot #%05d to %s failed\n", idx, individual_filename); } } /* add picture to output image */ thumb_add_shot(&tn, ip, idx, found_pts); gdImageDestroy(ip); ip = NULL; skip_shot: /* step */ seek_target += tn.step / av_q2d(pStream->time_base); seek_evade = 0; direction = 0; evade_try = 0; prevshot_pts = found_pts; av_log(NULL, AV_LOG_VERBOSE, "found_pts bottom: %"PRId64"\n", found_pts); continue_cleanup: // cleaning up before continuing the loop prevfound_pts = found_pts; if (NULL != edge_ip) { gdImageDestroy(edge_ip); edge_ip = NULL; } } av_log(NULL, AV_LOG_VERBOSE, " *** avg_evade_try: %.2f\n", avg_evade_try); // DEBUG eof: idx = idx; // target for eof: /* crop if we dont get enough shots */ int skipped_rows = tn.row - ceil((double)idx / tn.column); if (skipped_rows == tn.row) { av_log(NULL, AV_LOG_ERROR, " all rows're skipped?\n"); goto cleanup; } if (0 != skipped_rows) { int cropped_height = tn.height - skipped_rows*tn.shot_height; gdImagePtr new_out_ip = crop_image(tn.out_ip, tn.width, cropped_height); if (new_out_ip != tn.out_ip) { tn.out_ip = new_out_ip; av_log(NULL, LOG_INFO, " changing # of tiles to %dx%d because of skipped shots; total size: %dx%d\n", tn.column, tn.row - skipped_rows, tn.width, cropped_height); } } /* fill in the last row if some shots were skipped */ /* save output image */ errno = 0; gdImageJpeg(tn.out_ip, out_fp, gb_j_quality); /* FIXME: how to check if write was successful? */ if (0 != errno) { // FIXME: this should work? av_log(NULL, AV_LOG_ERROR, " saving output image failed: %s\n", strerror(errno)); goto cleanup; } tn.out_saved = 1; struct timeval tfinish; gettimeofday(&tfinish, NULL); // calendar time; effected by load & io & etc. double diff_time = (tfinish.tv_sec + tfinish.tv_usec/1000000.0) - (tstart.tv_sec + tstart.tv_usec/1000000.0); // previous version reported # of decoded shots/s; now we report the # of final shots/s //av_log(NULL, LOG_INFO, " avg. %.2f shots/s; output file: %s\n", nb_shots / diff_time, tn.out_filename); av_log(NULL, LOG_INFO, " %.2f s, %.2f shots/s; output: %s\n", diff_time, (tn.idx + 1) / diff_time, tn.out_filename); cleanup: if (NULL != ip) gdImageDestroy(ip); if (NULL != tn.out_ip) gdImageDestroy(tn.out_ip); if (NULL != out_fp) { fclose(out_fp); if (1 != tn.out_saved) { _tunlink(out_filename_w); } } if (NULL != info_fp) { fclose(info_fp); if (1 != tn.out_saved) { _tunlink(info_filename_w); } } if (NULL != pSwsCtx) sws_freeContext(pSwsCtx); // do we need to do this? // Free the video frame if (NULL != rgb_buffer) av_free(rgb_buffer); if (NULL != pFrameRGB) av_free(pFrameRGB); if (NULL != pFrame) av_free(pFrame); // Close the codec if (NULL != pCodecCtx && NULL != pCodecCtx->codec) { avcodec_close(pCodecCtx); } // Close the video file if (NULL != pFormatCtx) av_close_input_file(pFormatCtx); thumb_cleanup_dynamic(&tn); av_log(NULL, AV_LOG_VERBOSE, "make_thumbnail: done\n"); } /* modified from glibc */ int alphasort(const void *a, const void *b) { return strcoll(*(const char **) a, *(const char **) b); //return strcasecmp(*(const char **) a, *(const char **) b); } /* modified from glibc */ int alphacasesort(const void *a, const void *b) { //return strcoll(*(const char **) a, *(const char **) b); return strcasecmp(*(const char **) a, *(const char **) b); } /* return 1 if filename has one of the predefined extensions */ int check_extension(char *filename) { static char *movie_ext[] = { "3gp", "3g2", "asf", "avi", "avs", "dat", "divx", "dsm", "evo", "flv", "m1v", "m2ts", "m2v", "m4a", "mj2", "mjpg", "mjpeg", "mkv", "mov", "moov", "mp4", "mpg", "mpeg", "mpv", "nut", "ogg", "ogm", "qt", "rm", "rmvb", "swf", "ts", "vob", "wmv", "xvid" }; // FIXME: static static int sorted = 0; // 1 = sorted static const int nb_ext = sizeof(movie_ext) / sizeof(*movie_ext); if (0 == sorted) { qsort(movie_ext, nb_ext, sizeof(*movie_ext), alphacasesort); sorted = 1; } char *ext = strrchr(filename, '.'); if (NULL == ext) { return 0; } ext += 1; if (NULL == bsearch(&ext, movie_ext, nb_ext, sizeof(*movie_ext), alphacasesort)) { return 0; } if (NULL != strstr(filename, "uTorrentPartFile")) { return 0; } return 1; } void process_loop(int n, char **files); /* modified from glibc's scandir -- mingw doesn't have scandir */ void process_dir(char *dir) { #if defined(WIN32) && defined(_UNICODE) wchar_t dir_w[FILENAME_MAX]; UTF8_2_WC(dir_w, dir, FILENAME_MAX); #else char *dir_w = dir; #endif _TDIR *dp = _topendir(dir_w); if (NULL == dp) { av_log(NULL, AV_LOG_ERROR, "\n%s: opendir failed: %s\n", dir, strerror(errno)); return; } /* read directory & sort */ struct _tdirent *d; char **v = NULL; size_t cnt = 0, vsize = 0; while (1) { errno = 0; d = _treaddir(dp); if (NULL == d) { if (0 != errno) { // is this check good? av_log(NULL, AV_LOG_ERROR, "\n%s: readdir failed: %s\n", dir, strerror(errno)); goto cleanup; } break; } if (_tcscmp(d->d_name, _TEXT(".")) == 0 || _tcscmp(d->d_name, _TEXT("..")) == 0) { continue; } #if defined(WIN32) && defined(_UNICODE) char d_name_utf8[UTF8_FILENAME_SIZE]; WC_2_UTF8(d_name_utf8, d->d_name, UTF8_FILENAME_SIZE); #else char *d_name_utf8 = d->d_name; #endif char child_utf8[UTF8_FILENAME_SIZE]; strcpy_va(child_utf8, 3, dir, "/", d_name_utf8); if (1 != is_dir(child_utf8) && 1 != check_extension(child_utf8)) { continue; } if (cnt == vsize) { char **new; if (vsize == 0) vsize = 50; else vsize *= 2; new = realloc(v, vsize * sizeof(*v)); if (new == NULL) { // mingw doesn't seem to set errno for memory functions av_log(NULL, AV_LOG_ERROR, "\n%s: realloc failed: %s\n", dir, strerror(errno)); goto cleanup; } v = new; } char *vnew = malloc(strlen(child_utf8) + 1); // for '\0' if (vnew == NULL) { av_log(NULL, AV_LOG_ERROR, "\n%s: malloc failed: %s\n", dir, strerror(errno)); goto cleanup; } strcpy(vnew, child_utf8); v[cnt++] = vnew; //av_log(NULL, LOG_INFO, "process_dir added: %s\n", v[cnt-1]); // DEBUG } qsort(v, cnt, sizeof(*v), alphasort); /* process dirs & files */ process_loop(cnt, v); cleanup: while (cnt > 0) free(v[--cnt]); free(v); _tclosedir(dp); } /* */ void process_loop(int n, char **files) { int i; for (i = 0; i < n; i++) { av_log(NULL, AV_LOG_VERBOSE, "process_loop: %s\n", files[i]); rem_trailing_slash(files[i]); // if (is_dir(files[i])) { // directory //av_log(NULL, LOG_INFO, "process_loop: %s is a DIR\n", files[i]); // DEBUG process_dir(files[i]); } else { // not a directory //av_log(NULL, LOG_INFO, "process_loop: %s is not a DIR\n", files[i]); // DEBUG make_thumbnail(files[i]); } } } // copied & modified from mingw-runtime-3.13's init.c typedef struct { int newmode; } _startupinfo; extern void __wgetmainargs (int *, wchar_t ***, wchar_t ***, int, _startupinfo *); char *gb_argv[10240]; // FIXME: global & hopefully noone will use more than this /* get command line arguments and expand wildcards in utf-8 in windows caller needs to free argv[i] return 0 if ok */ int get_windows_argv(int *pargc, char ***pargv) { #if defined(WIN32) && defined(_UNICODE) // copied & modified from mingw-runtime-3.13's init.c int _argc = 0; wchar_t **_argv = 0; wchar_t **dummy_environ = 0; _startupinfo start_info; start_info.newmode = 0; __wgetmainargs(&_argc, &_argv, &dummy_environ, -1, &start_info); //printf("\nafter __wgetmainargs; _argc: %d\n", _argc); // DEBUG int i; for (i = 0; i < _argc; i++) { //wprintf(L"_argv[%d] wc: %s\n", i, _argv[i]); // DEBUG char utf8_buf[UTF8_FILENAME_SIZE] = ""; WC_2_UTF8(utf8_buf, _argv[i], UTF8_FILENAME_SIZE); //printf("_argv[%d] utf8: %s\n", i, utf8_buf); // DEBUG char *dup = strdup(utf8_buf); if (NULL == dup) { goto error; } gb_argv[i] = dup; } *pargc = _argc; *pargv = gb_argv; return 0; error: while (--_argc >= 0) { free(gb_argv[_argc]); } return -1; #endif return 0; } /* */ int get_location_opt(char c, char *optarg) { int ret = 1; char *bak = strdup(optarg); // backup for displaying error if (NULL == bak) { av_log(NULL, AV_LOG_ERROR, "%s: strdup failed\n", gb_argv0); return ret; } const char *delim = ":"; char *tailptr; // info text location char *token = strtok(optarg, delim); if (NULL == token) { goto cleanup; } gb_L_info_location = strtod(token, &tailptr); if ('\0' != *tailptr) { // error goto cleanup; } // time stamp location token = strtok (NULL, delim); if (NULL == token) { ret = 0; // time stamp format is optional goto cleanup; } gb_L_time_location = strtod(token, &tailptr); if ('\0' != *tailptr) { // error goto cleanup; } ret = 0; cleanup: if (0 != ret) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c is invalid at '%s'\n", gb_argv0, c, bak); } free(bak); return ret; } static const int hex2int[] = {0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,0,10,11,12,13,14,15}; #define CHAR2INT(p) (hex2int[(p)-'0']) // A-F must be in uppercase /* col must be in the correct format RRGGBB (in hex) */ rgb_color color_str2rgb_color(color_str col) { return (rgb_color) {CHAR2INT(col[0])*16 + CHAR2INT(col[1]), CHAR2INT(col[2])*16 + CHAR2INT(col[3]), CHAR2INT(col[4])*16 + CHAR2INT(col[5]) }; } /* check and convert color_str to rgb_color return -1 if error */ int parse_color(rgb_color *rgb, color_str str) { if (NULL == str || strlen(str) < 6) { return -1; } int i; for (i = 0; i < 6; i++) { char upper = toupper(str[i]); if (upper < '0' || upper > 'F') return -1; if (upper < 'A' && upper > '9') return -1; str[i] = upper; } *rgb = color_str2rgb_color(str); //av_log(NULL, LOG_INFO, "parse_color: %s=>%d,%d,%d\n", str, rgb->r, rgb->g, rgb->b); //DEBUG return 0; } /* */ int get_color_opt(char c, rgb_color *color, char *optarg) { if (-1 == parse_color(color, optarg)) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c is invalid at '%s' -- must be RRGGBB in hex\n", gb_argv0, c, optarg); return 1; } return 0; } /* */ int get_format_opt(char c, char *optarg) { int ret = 1; char *bak = strdup(optarg); // backup for displaying error if (NULL == bak) { av_log(NULL, AV_LOG_ERROR, "%s: strdup failed\n", gb_argv0); return ret; } const char *delim = ":"; // info text font color char *token = strtok(optarg, delim); if (NULL == token || -1 == parse_color(&gb_F_info_color, token)) { goto cleanup; } // info text font size token = strtok (NULL, delim); if (NULL == token) { goto cleanup; } char *tailptr; gb_F_info_font_size = strtod(token, &tailptr); if ('\0' != *tailptr) { // error goto cleanup; } // time stamp font token = strtok (NULL, delim); if (NULL == token) { ret = 0; // time stamp format is optional gb_F_ts_fontname = gb_f_fontname; gb_F_ts_font_size = gb_F_info_font_size - 1; goto cleanup; } gb_F_ts_fontname = token; // time stamp font color token = strtok (NULL, delim); if (NULL == token || -1 == parse_color(&gb_F_ts_color , token)) { goto cleanup; } // time stamp shadow color token = strtok (NULL, delim); if (NULL == token || -1 == parse_color(&gb_F_ts_shadow , token)) { goto cleanup; } // time stamp font size token = strtok (NULL, delim); if (NULL == token) { goto cleanup; } gb_F_ts_font_size = strtod(token, &tailptr); if ('\0' != *tailptr) { // error goto cleanup; } ret = 0; cleanup: //av_log(NULL, LOG_INFO, "%s:%.1f:", format_color(gb_F_info_color), gb_F_info_font_size); // DEBUG //av_log(NULL, LOG_INFO, "%s:%s:", gb_F_ts_fontname, format_color(gb_F_ts_color)); // DEBUG //av_log(NULL, LOG_INFO, "%s:%.1f\n", format_color(gb_F_ts_shadow), gb_F_ts_font_size); // DEBUG if (0 != ret) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c is invalid at '%s'\n", gb_argv0, c, bak); av_log(NULL, AV_LOG_ERROR, "examples:\n"); av_log(NULL, AV_LOG_ERROR, "info text blue color size 10:\n -%c 0000FF:10\n", c); av_log(NULL, AV_LOG_ERROR, "info text green color size 12; time stamp font comicbd.ttf yellow color black shadow size 8 :\n -%c 00FF00:12:comicbd.ttf:ffff00:000000:8\n", c); } free(bak); return ret; } /* */ int get_int_opt(char c, int *opt, char *optarg, int sign) { char *tailptr; int ret = strtol(optarg, &tailptr, 10); if ('\0' != *tailptr) { // error av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c is invalid at '%s'\n", gb_argv0, c, tailptr); return 1; } if (sign > 0 && ret <= 0) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c must be > 0\n", gb_argv0, c); return 1; } else if (sign == 0 && ret < 0) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c must be >= 0\n", gb_argv0, c); return 1; } *opt = ret; return 0; } int get_double_opt(char c, double *opt, char *optarg, double sign) { char *tailptr; double ret = strtod(optarg, &tailptr); if ('\0' != *tailptr) { // error av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c is invalid at '%s'\n", gb_argv0, c, tailptr); return 1; } if (sign > 0 && ret <= 0) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c must be > 0\n", gb_argv0, c); return 1; } else if (sign == 0 && ret < 0) { av_log(NULL, AV_LOG_ERROR, "%s: argument for option -%c must be >= 0\n", gb_argv0, c); return 1; } *opt = ret; return 0; } void usage() { av_log(NULL, AV_LOG_ERROR, "\nmovie thumbnailer (mtn) %s\n", gb_version); //wrong version; compile time not runtime //av_log(NULL, AV_LOG_ERROR, " with: %s %s %s %s GD%s\n", LIBAVCODEC_IDENT, LIBAVFORMAT_IDENT, LIBAVUTIL_IDENT, LIBSWSCALE_IDENT, GD_VERSION_STRING); av_log(NULL, AV_LOG_ERROR, "mtn saves thumbnails of specified movie files or directories to jpeg files.\n"); av_log(NULL, AV_LOG_ERROR, "for directories, it will recursively search inside for movie files.\n"); av_log(NULL, AV_LOG_ERROR, "usage:\n %s [options] file_or_dir1 [file_or_dir2] ... [file_or_dirn]\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, "options: (and default values)\n"); av_log(NULL, AV_LOG_ERROR, " -a aspect_ratio : override input file's display aspect ratio\n"); av_log(NULL, AV_LOG_ERROR, " -b %.2f : skip if %% blank is higher; 0:skip all 1:skip really blank >1:off\n", GB_B_BLANK); av_log(NULL, AV_LOG_ERROR, " -B %.1f : omit this seconds from the beginning\n", GB_B_BEGIN); av_log(NULL, AV_LOG_ERROR, " -c %d : # of column\n", GB_C_COLUMN); av_log(NULL, AV_LOG_ERROR, " -C %d : cut movie and thumbnails not more than the specified seconds; <=0:off\n", GB_C_CUT); //av_log(NULL, AV_LOG_ERROR, " -d : this option shouldn't be needed anymore\n"); av_log(NULL, AV_LOG_ERROR, " -D %d : edge detection; 0:off >0:on; higher detects more; try -D4 -D6 or -D8\n", gb_D_edge); //av_log(NULL, AV_LOG_ERROR, " -e : to be done\n"); // extension of movie files av_log(NULL, AV_LOG_ERROR, " -E %.1f : omit this seconds at the end\n", GB_E_END); av_log(NULL, AV_LOG_ERROR, " -f %s : font file; use absolute path if not in usual places\n", GB_F_FONTNAME); av_log(NULL, AV_LOG_ERROR, " -F RRGGBB:size[:font:RRGGBB:RRGGBB:size] : font format [time is optional]\n info_color:info_size[:time_font:time_color:time_shadow:time_size]\n"); av_log(NULL, AV_LOG_ERROR, " -g %d : gap between each shot\n", GB_G_GAP); av_log(NULL, AV_LOG_ERROR, " -h %d : minimum height of each shot; will reduce # of column to fit\n", GB_H_HEIGHT); av_log(NULL, AV_LOG_ERROR, " -i : info text off\n"); av_log(NULL, AV_LOG_ERROR, " -I : save individual shots too\n"); av_log(NULL, AV_LOG_ERROR, " -j %d : jpeg quality\n", GB_J_QUALITY); av_log(NULL, AV_LOG_ERROR, " -k RRGGBB : background color (in hex)\n"); // backgroud color av_log(NULL, AV_LOG_ERROR, " -L info_location[:time_location] : location of text\n 1=lower left, 2=lower right, 3=upper right, 4=upper left\n"); av_log(NULL, AV_LOG_ERROR, " -n : run at normal priority\n"); av_log(NULL, AV_LOG_ERROR, " -N info_suffix : save info text to a file with suffix\n"); av_log(NULL, AV_LOG_ERROR, " -o %s : output suffix\n", GB_O_SUFFIX); av_log(NULL, AV_LOG_ERROR, " -O directory : save output files in the specified directory\n"); av_log(NULL, AV_LOG_ERROR, " -p : pause before exiting; default on in win32\n"); av_log(NULL, AV_LOG_ERROR, " -P : dont pause before exiting; override -p\n"); //av_log(NULL, AV_LOG_ERROR, " -q : to be done\n"); // quiet mode av_log(NULL, AV_LOG_ERROR, " -r %d : # of rows; >0:override -s\n", GB_R_ROW); av_log(NULL, AV_LOG_ERROR, " -s %d : time step between each shot\n", GB_S_STEP); av_log(NULL, AV_LOG_ERROR, " -t : time stamp off\n"); av_log(NULL, AV_LOG_ERROR, " -T text : add text above output image\n"); av_log(NULL, AV_LOG_ERROR, " -v : verbose mode (debug)\n"); av_log(NULL, AV_LOG_ERROR, " -w %d : width of output image; 0:column * movie width\n", GB_W_WIDTH); av_log(NULL, AV_LOG_ERROR, " -W : dont overwrite existing files, i.e. update mode\n"); av_log(NULL, AV_LOG_ERROR, " -z : always use seek mode\n"); av_log(NULL, AV_LOG_ERROR, " -Z : always use non-seek mode -- slower but more accurate timing\n"); av_log(NULL, AV_LOG_ERROR, "examples:\n"); av_log(NULL, AV_LOG_ERROR, " to save thumbnails to file infile%s with default options:\n %s infile.avi\n", GB_O_SUFFIX, gb_argv0); av_log(NULL, AV_LOG_ERROR, " to change time step to 65 seconds & change total width to 900:\n %s -s 65 -w 900 infile.avi\n", gb_argv0); // as of version 0.60, -s 0 is not needed av_log(NULL, AV_LOG_ERROR, " to step evenly to get 3 columns x 10 rows:\n %s -c 3 -r 10 infile.avi\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, " to save output files to writeable directory:\n %s -O writeable /read/only/dir/infile.avi\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, " to get 2 columns in original movie size:\n %s -c 2 -w 0 infile.avi\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, " to skip uninteresting shots, try:\n %s -D 6 infile.avi\n", gb_argv0); #ifdef WIN32 av_log(NULL, AV_LOG_ERROR, "\nin windows, you can run %s from command prompt or drag files/dirs from\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, "windows explorer and drop them on %s. you can change the default options\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, "by creating a shortcut to %s and add options there (right click the\n", gb_argv0); av_log(NULL, AV_LOG_ERROR, "shortcut -> Properties -> Target); then drop files/dirs on the shortcut\n"); av_log(NULL, AV_LOG_ERROR, "instead.\n"); #else av_log(NULL, AV_LOG_ERROR, "\nyou'll probably need to change the truetype font path (-f fontfile).\n"); av_log(NULL, AV_LOG_ERROR, "the default is set to %s which might not exist in non-windows\n", GB_F_FONTNAME); av_log(NULL, AV_LOG_ERROR, "systems. if you dont have a truetype font, you can turn the text off by\n"); av_log(NULL, AV_LOG_ERROR, "using -i -t.\n"); #endif av_log(NULL, AV_LOG_ERROR, "\nmtn comes with ABSOLUTELY NO WARRANTY. this is free software, and you are\n"); av_log(NULL, AV_LOG_ERROR, "welcome to redistribute it under certain conditions; for details see file\n"); av_log(NULL, AV_LOG_ERROR, "gpl-2.0.txt.\n"); } int main(int argc, char *argv[]) { gb_argv0 = path_2_file(argv[0]); setvbuf(stderr, NULL, _IONBF, 0); // turn off buffering in mingw gb_st_start = time(NULL); // program start time srand(gb_st_start); // get utf-8 argv in windows if (0 != get_windows_argv(&argc, &argv)) { av_log(NULL, AV_LOG_ERROR, "%s: cannot get command line arguments\n", gb_argv0); return -1; } // set locale __attribute__((unused)) char *locale = setlocale(LC_ALL, ""); //av_log(NULL, AV_LOG_VERBOSE, "locale: %s\n", locale); /* get & check options */ int parse_error = 0; int c; while (-1 != (c = getopt(argc, argv, "a:b:B:c:C:D:e:E:f:F:g:h:iIj:k:L:nN:o:O:pPqr:s:tT:vVw:WzZ"))) { switch (c) { double tmp_a_ratio = 0; case 'a': if (0 == get_double_opt('a', &tmp_a_ratio, optarg, 1)) { // success gb_a_ratio.num = tmp_a_ratio * 10000; gb_a_ratio.den = 10000; } else { parse_error++; } break; case 'b': parse_error += get_double_opt('b', &gb_b_blank, optarg, 0); if (gb_b_blank < .2) { av_log(NULL, LOG_INFO, "%s: -b %.2f might be too extreme; try -b .5\n", gb_argv0, gb_b_blank); } if (gb_b_blank > 1) { // turn edge detection off cuz it requires blank detection gb_D_edge = 0; } break; case 'B': parse_error += get_double_opt('B', &gb_B_begin, optarg, 0); break; case 'c': parse_error += get_int_opt('c', &gb_c_column, optarg, 1); break; case 'C': parse_error += get_double_opt('C', &gb_C_cut, optarg, 1); break; case 'D': parse_error += get_int_opt('D', &gb_D_edge, optarg, 0); if (gb_D_edge > 0 && (gb_D_edge < 4 || gb_D_edge > 12)) { av_log(NULL, LOG_INFO, "%s: -D%d might be too extreme; try -D4, -D6, or -D8\n", gb_argv0, gb_D_edge); } break; case 'e': gb_e_ext = optarg; break; case 'E': parse_error += get_double_opt('E', &gb_E_end, optarg, 0); break; case 'f': gb_f_fontname = optarg; if (0 == strcmp(gb_F_ts_fontname, GB_F_FONTNAME)) { gb_F_ts_fontname = gb_f_fontname; } break; case 'F': parse_error += get_format_opt('F', optarg); break; case 'g': parse_error += get_int_opt('g', &gb_g_gap, optarg, 0); break; case 'h': parse_error += get_int_opt('h', &gb_h_height, optarg, 0); break; case 'i': gb_i_info = 0; break; case 'I': gb_I_individual = 1; break; case 'j': parse_error += get_int_opt('j', &gb_j_quality, optarg, 1); break; case 'k': // background color parse_error += get_color_opt('k', &gb_k_bcolor, optarg); break; case 'L': parse_error += get_location_opt('L', optarg); break; case 'n': gb_n_normal = 1; // normal priority break; case 'N': gb_N_suffix = optarg; break; case 'o': gb_o_suffix = optarg; break; case 'O': gb_O_outdir = optarg; rem_trailing_slash(gb_O_outdir); break; case 'p': gb_p_pause = 1; // pause before exiting break; case 'P': gb_P_dontpause = 1; // dont pause break; case 'q': gb_q_quiet = 1; //quiet break; case 'r': parse_error += get_int_opt('r', &gb_r_row, optarg, 0); break; case 's': parse_error += get_int_opt('s', &gb_s_step, optarg, 0); break; case 't': gb_t_timestamp = 0; // off break; case 'T': gb_T_text = optarg; break; case 'v': gb_v_verbose = 1; // verbose break; case 'V': gb_V = 1; // DEBUG av_log(NULL, LOG_INFO, "%s: -V is only used for debugging\n", gb_argv0); break; case 'w': parse_error += get_int_opt('w', &gb_w_width, optarg, 0); break; case 'W': gb_W_overwrite = 0; break; case 'z': gb_z_seek = 1; // always seek mode break; case 'Z': gb_Z_nonseek = 1; // always non-seek mode break; default: parse_error += 1; break; } } if (optind == argc) { av_log(NULL, AV_LOG_ERROR, "%s: no input files or directories specified\n", gb_argv0); parse_error += 1; } /* check arguments */ if (gb_r_row == 0 && gb_s_step == 0) { av_log(NULL, AV_LOG_ERROR, "%s: option -r and -s cant be 0 at the same time\n", gb_argv0); parse_error += 1; } if (gb_b_blank > 1 && gb_D_edge > 0) { av_log(NULL, AV_LOG_ERROR, "%s: -D requires -b arg to be less than 1\n", gb_argv0); parse_error += 1; } if (gb_z_seek == 1 && gb_Z_nonseek == 1) { av_log(NULL, AV_LOG_ERROR, "%s: option -z and -Z cant be used together\n", gb_argv0); parse_error += 1; } if (gb_E_end > 0 && gb_C_cut > 0) { av_log(NULL, AV_LOG_ERROR, "%s: option -C and -E cant be used together\n", gb_argv0); parse_error += 1; } if (0 != parse_error) { usage(); goto exit; } /* lower priority */ if (1 != gb_n_normal) { // lower priority #ifdef WIN32 SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS); #else nice(10); // mingw doesn't have nice?? //setpriority (PRIO_PROCESS, 0, PRIO_MAX/2); #endif } /* create output directory */ if (NULL != gb_O_outdir && !is_dir(gb_O_outdir)) { #ifdef WIN32 int ret = mkdir(gb_O_outdir); #else int ret = mkdir(gb_O_outdir, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); #endif if (0 != ret) { av_log(NULL, AV_LOG_ERROR, "\n%s: creating output directory '%s' failed: %s\n", gb_argv0, gb_O_outdir, strerror(errno)); goto exit; } } /* init */ av_register_all(); // Register all formats and codecs if (gb_v_verbose > 0) { av_log_set_level(AV_LOG_VERBOSE); } else { av_log_set_level(LOG_INFO); } //gdUseFontConfig(1); // set GD to use fontconfig patterns /* process movie files */ process_loop(argc - optind, argv + optind); exit: // clean up #if defined(WIN32) && defined(_UNICODE) while (--argc >= 0) { free(argv[argc]); } #endif //av_log(NULL, AV_LOG_VERBOSE, "\n%s: total run time: %.2f s.\n", gb_argv0, difftime(time(NULL), gb_st_start)); if (1 == gb_p_pause && 0 == gb_P_dontpause) { av_log(NULL, AV_LOG_ERROR, "\npausing... press Enter key to exit (see -P option)\n"); fflush(stdout); fflush(stderr); getchar(); } return 0; }