/** * \file logo.c * Composite an alpha-channel logo with optional drop-shadow onto video. * * Sponsored by Fabrik Inc. - bfree(at)fabrikinc.com * \author Bob (grafman) Free - bfree(at)graphcomp.com * * This vhook demonstrates the use of av_read_image to load still * images with alpha/transparency and composite them on video. * * Note: PNG support requires that FFMPEG be built with the PNG * codec registered/enabled. * ****************************************************************************** * EXAMPLE USAGE: * * ffmpeg -i INFILE -vhook 'PATH/logo.so -f logo.gif' OUTFILE * * * Note: the entire vhook argument must be single-quoted. * * * REQUIRED ARGS: * * -f * * Specifies the image to use for the logo. GIF is supported * in normal FFMPEG builds; PNG is supported if enabled in libavcodec * and libavformat. * * * OPTIONAL ARGS: * * -x * * Defines a logo offset from the left side of the frame. * A negative value (including -0) offsets from the right side. * * -y * * Defines a logo offset from the top of the frame. * A negative value (including -0) offsets from the bottom. * * -w * * Defines a drop shadow to the right of the logo. * A negative value shifts the shadow to the left. * * -h * * Defines a drop shadow to the bottom of the logo. * A negative value shifts the shadow upward. * * -d * * Defines the percent opacity of the drop shadow (0 - 100); * 100 is opaque. Defaults to 75. * * * Sample logos and additional notes available at * http://graphcomp.com/ffmpeg#plugins * ****************************************************************************** * LICENSE: * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ /* Includes */ #include #include #include #include #include #include #include "common.h" #include "avformat.h" #include "avcodec.h" #include "allformats.h" #include "framehook.h" #include "cmdutils.h" /* This stuff belongs in a header file */ /** * RGBA pixel. */ typedef struct { uint8_t R; ///< Red. uint8_t G; ///< Green. uint8_t B; ///< Blue. uint8_t A; ///< Alpha. } RGBA; /** * RECT - bounding rectangle. */ typedef struct { int left; int top; int right; int bottom; } RECT; /* * Constants. */ #define MAX_FILEPATH 2048 ///< Max filepath length. /** * ContextInfo - shares data between vhook functions. */ typedef struct { // User parameters - populated by Configure. char filename[MAX_FILEPATH]; ///< Logo filepath. int xDir; ///< Horizontal offset direction. int yDir; ///< Vertical offset direction. int xOff; ///< Horizontal offset in pixels. int yOff; ///< Vertical offset in pixels. int xDrop; ///< Horizontal drop-shadow offset. int yDrop; ///< Vertical drop-shadow offset. int dOpacity; ///< Drop-shadow opacity (0-100). // Image cache buffers - populated by load_image. int image_loaded; ///< Image cache loaded flag. int width; ///< Logo width in pixels. int height; ///< Logo height in pixels. int inp_fmt; ///< Logo format constant. int inp_size; ///< Logo buffer size in bytes. uint8_t* inp_buf; ///< Pointer to logo buffer. int rgba_size; ///< RGBA buffer size in bytes. uint8_t* rgba_buf; ///< Pointer to RGBA buffer. AVFrame* rgbaFrame; ///< Pointer to RGBA frame. // Bounds data - populated by calc_bounds. int vidWidth; ///< Video frame width in pixels. int vidHeight; ///< Video frame height in pixels. int vid_row; ///< Video row size in bytes. int logo_row; ///< Logo row size in bytes. RECT rLogo; ///< Logo placement on video frame. RECT rDrop; ///< Drop-shadow placement on video frame. RECT rBounds; ///< Affected pixels on video frame. } ContextInfo; /* * Macros. */ #define min(a,b) ((a < b) ? a : b) ///< Return the smaller of 2 values. #define max(a,b) ((a > b) ? a : b) ///< Return the larger of 2 values. /** * Allocate context block and capture user parameters. * Called by FFMPEG pipeline. * @param ctxp A handle to receive alloacted context pointer. * @param argc vhook's argument count. * @param argv vhook's argument pointers. * @return 0 for success; otherwise failure. */ int Configure(void **ctxp, int argc, char *argv[]); /** * Release context block. * Called by FFMPEG pipeline. * @param ctx Context pointer. */ void Release(void *ctx); /** * Main video frame proc. * Called by FFMPEG pipeline. * @param ctx Context pointer. * @param picture Pointer to video frame. * @param pix_fmt Video frame format constant. * @param src_width Video frame width in pixels. * @param src_heigth Video frame height in pixels. * @param pts Presentation timestamp. */ void Process ( void *ctx, AVPicture *picture, enum PixelFormat pix_fmt, int src_width, int src_height, int64_t pts ); /** * Load and cache an image. * @param ci Context pointer. * @return 0 for success; otherwise failure. */ int load_image(ContextInfo *ci); /** * Release an image cache. * @param ci Context pointer. */ void release_image(ContextInfo *ci); /** * Initialization callback for av_read_image. * @param opaque Context pointer. * @param info Image info from codec. * @return 0 for success; otherwise failure. */ static int read_image_alloc_cb(void *opaque, AVImageInfo *info); /** * Calculates bounding info for video frame, logo and drop-shadow. * @param ci Context pointer. * @param vid_width Video frame width in pixels. * @param vid_height Video frame height in pixels. */ void calc_bounds(ContextInfo *ci, int vid_width, int vid_height); /** * Merges a pixel component onto another using and alpha-channel value. * @param back Background pixel value. * @param fore Foreground pixel value. * @param alpha Alpha-channel value - from 0.0 (transparent) to 1.0 (opaque). * @return Merged component value. */ int alpha_merge(int back, int fore, double alpha); /* * Case-insensitive string compare. */ int stricmp (const char *s, const char *t); /* Finally - the code */ /****************************************************************************** * Allocate Context block and capture user parameters. ******************************************************************************/ int Configure(void **ctxp, int argc, char *argv[]) { ContextInfo *ci; int c; // Allocate context block if (0 == (*ctxp = av_mallocz(sizeof(ContextInfo)))) return -1; ci = (ContextInfo *)*ctxp; // Set drop shadow default to 75% ci->dOpacity = 75; // Parse user parameters opterr = 0; optind = 1; while ((c = getopt(argc, argv, "f:x::y::w::h::d::")) > 0) { switch (c) { // Logo filepath case 'f': { strncpy(ci->filename, optarg, MAX_FILEPATH-1); ci->filename[MAX_FILEPATH-1] = 0; break; } // Logo offset case 'x': { ci->xDir = strchr(argv[optind],'-') ? -1 : 1; ci->xOff = abs(atoi(argv[optind])); break; } case 'y': { ci->yDir = strchr(argv[optind],'-') ? -1 : 1; ci->yOff = abs(atoi(argv[optind])); break; } // Drop shadow offset case 'w': { ci->xDrop = atoi(argv[optind]); break; } case 'h': { ci->yDrop = atoi(argv[optind]); break; } // Drop shadow opacity case 'd': { ci->dOpacity = atoi(argv[optind]); if (ci->dOpacity < 0) ci->dOpacity = 0; if (ci->dOpacity > 100) ci->dOpacity = 100; break; } // Ignore unsupported args default: { av_log(NULL, AV_LOG_DEBUG, "logo: Unrecognized argument '-%c %s' - ignored\n", c,argv[optind]); } } } // Check that a filepath was provided if (0 == ci->filename[0]) { av_log(NULL, AV_LOG_ERROR, "logo: No filepath specified.\n"); return -1; } // Register codecs av_register_all(); // Load and cache logo return(load_image(ci)); } /**************************************************************************** * Free Context block. ****************************************************************************/ void Release(void *ctx) { ContextInfo *ci = (ContextInfo *)ctx; if (ci) release_image(ci); if (ctx) av_free(ctx); } /**************************************************************************** * Main filter proc. ****************************************************************************/ void Process(void *ctx, AVPicture *picture, enum PixelFormat pix_fmt, int src_width, int src_height, int64_t pts) { ContextInfo *ci; uint8_t *buf = 0; AVPicture *pict = picture; AVPicture rgbaPict; int x, y, xLogo, yLogo, xDrop, yDrop; int vid_offs, logo_offs, drop_offs; RGBA *pVid; RGBA *pLogo; RGBA *pDrop; double alpha; double opacity; // Skip if no frame dimensions if (!src_width || !src_height) return; // Initialize context ci = (ContextInfo *) ctx; calc_bounds(ci, src_width, src_height); // Convert video frame to RGBA32 (easier to process for palette-based videos) // Could optimize for non-palette based videos if (pix_fmt != PIX_FMT_RGBA32) { int size = avpicture_get_size(PIX_FMT_RGBA32, src_width, src_height); buf = av_malloc(size); avpicture_fill(&rgbaPict, buf, PIX_FMT_RGBA32, src_width, src_height); if (img_convert(&rgbaPict, PIX_FMT_RGBA32, picture, pix_fmt, src_width, src_height) < 0) { av_free(buf); return; } pict = &rgbaPict; } /* Insert filter code here, if any */ // Frame's row loop for (y=ci->rBounds.top; yrBounds.bottom; y++) { // Get video frame pointer offset vid_offs = y * ci->vid_row; // Get logo pointer offset yLogo = y - ci->rLogo.top; logo_offs = yLogo * ci->logo_row; // Get drop-shadow pointer offset yDrop = y - ci->rDrop.top; drop_offs = yDrop * ci->logo_row; // Row's pixel loop for (x=ci->rBounds.left; xrBounds.right; x++) { // Get pointer to video frame pixel pVid = (RGBA *)(pict->data[0]+vid_offs+(x<<2)); // Handle drop-shadow first - skip if no drop-shadow offsets if (ci->xDrop || ci->yDrop) { xDrop = x - ci->rDrop.left; if (xDrop > 0 && xDrop < ci->width && yDrop > 0 && yDrop < ci->height) { // Get pointer to drop-shadow pixel pDrop = (RGBA *)(ci->rgbaFrame->data[0]+drop_offs+(xDrop<<2)); // Lame drop shadow - gaussian distribution would be much better opacity = ci->dOpacity * pDrop->A / 25500.0; // Composite drop-shadow if (opacity != 0.0) { pVid->R = alpha_merge(pVid->R,0,opacity); pVid->G = alpha_merge(pVid->G,0,opacity); pVid->B = alpha_merge(pVid->B,0,opacity); } } } // Handle logo next xLogo = x - ci->rLogo.left; if (yLogo > 0 && yLogo < ci->height && xLogo > 0 && xLogo < ci->width) { // Get pointer to logo pixel pLogo = (RGBA *)(ci->rgbaFrame->data[0]+logo_offs+(xLogo<<2)); // If opaque, just copy if (pLogo->A == 255) { *pVid = *pLogo; } // Skip if transparent - otherwise merge else if (pLogo->A) { alpha = pLogo->A / 255.0; pVid->R = alpha_merge(pVid->R,pLogo->R,alpha); pVid->G = alpha_merge(pVid->G,pLogo->G,alpha); pVid->B = alpha_merge(pVid->B,pLogo->B,alpha); } } } // foreach X } // foreach Y // Convert modified frame back to video format if (pix_fmt != PIX_FMT_RGBA32) { if (img_convert(picture, pix_fmt, &rgbaPict, PIX_FMT_RGBA32, src_width, src_height) < 0) { // Error handling here } av_free(buf); buf = 0; } } /**************************************************************************** * Load and cache image buffers. ****************************************************************************/ int load_image(ContextInfo *ci) { AVImageFormat *pFormat; ByteIOContext bctx,*pb=&bctx; AVFrame *pFrameInp; int err; // Just return if logo has already been fetched/converted if (ci->image_loaded) return 0; // Guess image format pFormat = guess_image_format(ci->filename); // JPEG image decoder is broken - bail #if !defined(JPEG_FIXED) if (!strcmp(pFormat->name,"jpeg")) { av_log(NULL, AV_LOG_ERROR,"logo: JPEG image format not supported\n"); return -1; } #endif // Unable to guess format if (!pFormat) { av_log(NULL, AV_LOG_ERROR,"logo: Unsupported image format\n"); return -1; } // Open file if (url_fopen(pb, ci->filename, URL_RDONLY) < 0) { av_log(NULL, AV_LOG_ERROR,"logo: Unable to open image\n"); return -1; } // Read file err = av_read_image(pb, ci->filename, pFormat, read_image_alloc_cb, ci); url_fclose(pb); // Handle errors if (!ci->inp_buf) { av_log(NULL, AV_LOG_ERROR,"logo: Unable to allocate read buffer\n"); return -1; } if (!ci->rgba_buf || !ci->rgbaFrame) { av_log(NULL, AV_LOG_ERROR,"logo: Unable to allocate rgba buffer\n"); return -1; } if (err) { av_log(NULL, AV_LOG_ERROR,"logo: av_read_image error: %d\n",err); return -1; } // Convert to RGBA32 if necessary if (ci->inp_fmt != PIX_FMT_RGBA32) { // Allocate input frame pFrameInp = avcodec_alloc_frame(); avpicture_fill((AVPicture*)pFrameInp, ci->inp_buf,ci->inp_fmt,ci->width,ci->height); img_convert((AVPicture*)ci->rgbaFrame, PIX_FMT_RGBA32, (AVPicture*)pFrameInp, ci->inp_fmt, ci->width, ci->height); av_free(pFrameInp); } // Done ci->image_loaded = 1; return(0); } /**************************************************************************** * Release image buffers. ****************************************************************************/ void release_image(ContextInfo *ci) { // Free RGBA format buffer if (ci->rgbaFrame) av_free(ci->rgbaFrame); ci->rgbaFrame = 0; if (ci->inp_fmt != PIX_FMT_RGBA32) { if (ci->rgba_buf) av_free(ci->rgba_buf); ci->rgba_buf = 0; } // Free input buffer if (ci->inp_buf) av_free(ci->inp_buf); ci->inp_buf = 0; ci->image_loaded = 0; } /**************************************************************************** * Alloc callback for av_read_image. ****************************************************************************/ static int read_image_alloc_cb(void *opaque, AVImageInfo *info) { ContextInfo *ci = opaque; // Capture image dimensions and pixel format if (!info->width || !info->height) return -1; ci->width = info->width; ci->height = info->height; ci->inp_fmt = info->pix_fmt; // Allocate input image buffer ci->inp_size = avpicture_get_size(info->pix_fmt,info->width,info->height); ci->inp_buf = av_malloc(ci->inp_size); // Map input frame to buffer avpicture_fill(&info->pict,ci->inp_buf,info->pix_fmt,info->width,info->height); // Input format is already PIX_FMT_RGBA32 if (ci->inp_fmt == PIX_FMT_RGBA32) { ci->rgba_size = ci->inp_size; ci->rgba_buf = ci->inp_buf; } // Otherwise allocate rgba buffer else { ci->rgba_size = avpicture_get_size(PIX_FMT_RGBA32,info->width,info->height); ci->rgba_buf = av_malloc(ci->rgba_size); } // Map RGBA frame to buffer ci->rgbaFrame = avcodec_alloc_frame(); avpicture_fill((AVPicture*)ci->rgbaFrame, ci->rgba_buf,PIX_FMT_RGBA32,info->width,info->height); return(0); } /**************************************************************************** * Calculate and cache bounds. ****************************************************************************/ void calc_bounds(ContextInfo *ci, int vid_width, int vid_height) { // skip if already cached if (ci->vidWidth && ci->vidHeight) return; // Cache video frame dimensions ci->vidWidth = vid_width; ci->vidHeight = vid_height; // Calculate row sizes - assume 4 bytes/pixel for RGBA ci->vid_row = vid_width << 2; ci->logo_row = ci->width << 2; // Calculate logo position on frame if (ci->xDir < 0) { ci->rLogo.right = ci->vidWidth - ci->xOff; ci->rLogo.left = ci->rLogo.right - ci->width; } else { ci->rLogo.left = ci->xOff; ci->rLogo.right = ci->rLogo.left + ci->width; } if (ci->yDir < 0) { ci->rLogo.bottom = ci->vidHeight - ci->yOff; ci->rLogo.top = ci->rLogo.bottom - ci->height; } else { ci->rLogo.top = ci->yOff; ci->rLogo.bottom = ci->rLogo.top + ci->height; } // Calculate drop shadow position on frame ci->rDrop.left = ci->rLogo.left + ci->xDrop; ci->rDrop.right = ci->rLogo.right + ci->xDrop; ci->rDrop.top = ci->rLogo.top + ci->yDrop; ci->rDrop.bottom = ci->rLogo.bottom + ci->yDrop; // Calculate combined logo/drop-shadow bounds ci->rBounds.left = max(0,min(vid_width,min(ci->rLogo.left,ci->rDrop.left))); ci->rBounds.top = max(0,min(vid_height,min(ci->rLogo.top,ci->rDrop.top))); ci->rBounds.right = min(vid_width,max(0,max(ci->rLogo.right,ci->rDrop.right))); ci->rBounds.bottom = min(vid_height,max(0,max(ci->rLogo.bottom,ci->rDrop.bottom))); } /**************************************************************************** * Very simple alpha merge. * * alpha is 0.0 (transparent) to 1.0 (opaque). ****************************************************************************/ int alpha_merge(int back, int fore, double alpha) { int val = (back * (1.0 - alpha)) + (fore * alpha); return(min(255,(max(0,val)))); } /**************************************************************************** * Is there a portable version of this? * * 0 = string found; otherwise not ****************************************************************************/ int stricmp (const char *s, const char *t) { int d = 0; do { d = toupper(*s) - toupper(*t); } while (*s++ && *t++ && !d); return(d); }