/// <summary>Creates a thumbnail</summary> /// <param name="info">Media information to use for header text</param> /// <param name="pFormatContext">The format context to use</param> /// <param name="vidStream">The video stream to use</param> /// <param name="options">Options setting the appearence of the sheet</param> /// <param name="onProgress">Is called after each taken frame and passes (int currentIndex, int positions.Count) - if true is returned, the loop is canceled</param> private unsafe static Bitmap ExtractVideoThumbnailSheet(FFmpegMediaInfo info, AVFormatContext* pFormatContext, AVStream* vidStream, VideoThumbSheetOptions options, Func<int, int, bool> onProgress) { #region Structure diagram /* m := Margin, p := Padding, tw := ThumbWidth, th := ThumbHeight, hh := HeaderHeight, ### := Thumbnail image m ###tw### p ###tw### p ###tw### p ###tw### p ###tw### m +--------------------------------------------------------+ | | m } Margin | +----------------------------------------------------+ | | |Filename: xxxxxxxxxxxxxxxxxxxxxx.avi | | xx \ | |Length: 0:00:00 Resolution: 640x480 @25FPS | | hh } Header height | |Streams: Video[div5/xvid], Audio[mp3a/MP3/eng] | | xx / | +----------------------------------------------------+ | | | 2*p } 2 * Padding | +----------------------------------------------------+ | | |########| |########| |########| |########| |########| | ## \ | |########| |########| |########| |########| |########| | th } Thumbnail height | |########| |########| |########| |########| |########| | ## / | +----------------------------------------------------+ | | | p } 1* Padding | +----------------------------------------------------+ | | |########| |########| |########| |########| |########| | ... ... ... ... ... ... | |########| |########| |########| |########| |########| | + +----------------------------------------------------+ + | | m } Margin +--------------------------------------------------------+ */ #endregion #region Header and image preperations // Get video info from codec int width = vidStream->codec->width; int height = vidStream->codec->height; AVRational framerate = vidStream->codec->framerate; long duration = pFormatContext->duration; // Create header text and calculate its height (using current video stream) string header = String.Format("Filename: {0}\r\nLength: {1} Resolution: {2}x{3} @{4}FPS\r\nStreams: {5}", Path.GetFileName(info.Filename), ToFormattedString(info.Duration), width, height, ToDescString(framerate), String.Join(", ", info.Streams.Select(s => s.ToString()).ToArray()) ); int headerHight = Convert.ToInt32(MeassureString(header, options.HeaderFont).Height); // Calculate image sizes and create image int thumbHeight = height * options.ThumbWidth / width; int imgWidth = 2 * options.Margin + options.ThumbColumns * (options.ThumbWidth + options.Padding) - options.Padding; int imgHeight = 2 * options.Margin + headerHight + options.ThumbRows * (thumbHeight + options.Padding) + options.Padding; int thumbTop = options.Margin + headerHight + 2 * options.Padding; Bitmap bmp = new Bitmap(imgWidth, imgHeight); int count = options.ThumbColumns * options.ThumbRows; List<long> positions = CreateRegularTimePositionList(duration, count); #endregion using (Graphics g = Graphics.FromImage(bmp)) using (Brush headerBrush = new SolidBrush(options.HeaderColor)) using (Pen borderPen = new Pen(options.ThumbBorderColor, 1f)) using (Brush indexShadowBrush = new SolidBrush(options.IndexShadowColor)) using (Brush indexBrush = new SolidBrush(options.IndexColor)) { // Set background color g.Clear(options.BackgroundColor); // Draw header text float marginF = Convert.ToSingle(options.Margin); g.DrawString(header, options.HeaderFont, headerBrush, new PointF(marginF, marginF)); // Loop through images as they are extracted int c, r, x, y; string tsText; SizeF tsSize; float ix, iy; ExtractFrames(pFormatContext, vidStream, positions, options.ForceExactTimePosition, (i, ts, img) => { // Calculate positions and sizes c = i % options.ThumbColumns; // column r = i / options.ThumbColumns; // row x = options.Margin + c * (options.ThumbWidth + options.Padding); // thumb left y = thumbTop + r * (thumbHeight + options.Padding); // thumb top tsText = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds); // timestamp text tsSize = g.MeasureString(tsText, options.IndexFont); // timestamp text size ix = Convert.ToSingle(x + options.ThumbWidth) - tsSize.Width - 3f; // timestamp text left iy = Convert.ToSingle(y + thumbHeight) - tsSize.Height - 3f; // timestamp text top // Insert thumbnail image resized g.DrawImage(img, x, y, options.ThumbWidth, thumbHeight); // Overdraw edges with border if (options.DrawThumbnailBorder) g.DrawRectangle(borderPen, x, y, options.ThumbWidth, thumbHeight); // Draw timestamp shadow g.DrawString(tsText, options.IndexFont, indexShadowBrush, ix - 1f, iy - 1f); g.DrawString(tsText, options.IndexFont, indexShadowBrush, ix + 1f, iy + 1f); // Draw timestamp g.DrawString(tsText, options.IndexFont, indexBrush, ix, iy); // Dispose the thumbnail image because it's not needed anymore img.Dispose(); // Publish progress if (onProgress != null) return onProgress(i, count); else return false; }); } return bmp; }
/// <summary>Creates a thumbnail sheet</summary> /// <param name="streamIndex">The index of the video stream to extract the frames from (-1 selects the first video stream)</param> /// <param name="options">Options setting the thumbnail sheet's appearence</param> /// <param name="onProgress">Is called after each taken frame and passes (int currentIndex, int positions.Count) - if true is returned, the loop is canceled</param> public Bitmap GetThumbnailSheet(int streamIndex, VideoThumbSheetOptions options, Func<int, int, bool> onProgress) { FFmpegStreamInfo stream; if (streamIndex == -1) stream = this.Streams.FirstOrDefault(s => s.StreamType == FFmpegStreamType.Video); else stream = this.Streams[streamIndex]; if (stream == null || stream.StreamType != FFmpegStreamType.Video) throw new Exception("No video stream selected!"); return ExtractVideoThumbnailSheet(this, this.AVFormatContext, stream.AVStream, options, onProgress); }