using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Windows.Forms;
using FFmpegNet;
using JD.Application.Utils;
using JD.Asset.Types.Map;
using JD.Audio.Annotations;
using JD.Kernel.Classes;
using JD.Serialization;
using Newtonsoft.Json.Linq;
using TeaBox.ObjectModel;
using FrameConverter = JD.Core.FrameConverter;
namespace JD.VideoPlayer.Project
{
///
/// Keeps all the video images in memory as a sprite sheet so that it is really fast to display video with different effects.
///
public class VideoSpriteSheet
{
private const int MaxColumnCount = 100;
private readonly Tape _tape;
private readonly Scope _isGenerating = new Scope();
private Image _spriteSheet;
public VideoSpriteSheet(Tape tape, Size singleImageSize)
{
_tape = tape;
SingleImageSize = singleImageSize;
}
public Size SingleImageSize { get; }
#region Generation
///
/// Generates an atlas for the requested pngFolder
///
public void GenerateSpriteSheet(string folder, string targetFile, IProgressMonitor progressMonitor)
{
if (_isGenerating.InScope)
{
return;
}
using (_isGenerating.Begin())
{
var images = Directory.GetFiles(folder, "*.bmp").OrderBy(x => x).Select((imagePath, index) => new { imagePath, FrameIndex = index }).ToArray();
progressMonitor.TotalWork = images.Length;
int rowCount = (int)Math.Ceiling(images.Length / (double)MaxColumnCount);
using (var destinationImage = new Bitmap(MaxColumnCount * SingleImageSize.Width, rowCount * SingleImageSize.Height))
using (var graphics = Graphics.FromImage(destinationImage))
{
Parallel.ForEach(images, imageToLoad =>
{
using (var image = Image.FromFile(imageToLoad.imagePath))
{
lock (destinationImage)
{
graphics.DrawImage(image, GetImageBounds(imageToLoad.FrameIndex));
progressMonitor.WorkDone++;
}
}
});
destinationImage.Save(targetFile, ImageFormat.Png);
}
}
}
private Rectangle GetImageBounds(int frame)
{
var row = frame % MaxColumnCount;
var column = frame / MaxColumnCount;
return new Rectangle(new Point(row * SingleImageSize.Width, column * SingleImageSize.Height), SingleImageSize);
}
#endregion
public void LoadFromSpriteSheet(string imagePath)
{
try
{
_spriteSheet = Image.FromFile(imagePath);
}
catch (Exception) // Typically a GDI exception
{
// When loading an image that is too large, it may fail
// in that case, allow to open the editor but prompt a
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(10), "Deactivating video preview", "Could not load video, preview will not be available", ToolTipIcon.Warning);
}
}
#region Painting
///
/// Paints the requested image on the graphics.
///
public void Paint(Graphics graphics, BeatUnit beat, Rectangle destinationBounds)
{
Paint(graphics, ToVideoFrame(beat), destinationBounds);
}
///
/// Computes which frame in the video matches the specified frame in beat.
///
private int ToVideoFrame(BeatUnit beat)
{
var wave = _tape.TryGetWave();
var time = wave.UnitConverter.TimeFromBeat(beat) - wave.Annotations.VideoStartTime;
return (int)Math.Round(time.TotalSeconds / FrameConverter.VideoFrameDuration.TotalSeconds);
}
private void Paint(Graphics graphics, int frameId, Rectangle destinationBounds)
{
if (_spriteSheet != null)
{
var sourceBounds = GetImageBounds(frameId);
AspectRatio.ScaleRectanglesKeepRatio(ref destinationBounds, ref sourceBounds);
if (sourceBounds.Width > 0 && sourceBounds.Height > 0 &&
destinationBounds.Width > 0 && destinationBounds.Height > 0)
{
graphics.DrawImage(_spriteSheet, destinationBounds, sourceBounds, GraphicsUnit.Pixel);
}
}
}
private static class AspectRatio
{
public static void ScaleRectanglesKeepRatio(ref Rectangle destinationBounds, ref Rectangle sourceBounds)
{
double sourceRatio = sourceBounds.Width / (double)sourceBounds.Height;
double destinationRatio = destinationBounds.Width / (double)destinationBounds.Height;
if (destinationRatio < sourceRatio)
{
// Source image is proportionnaly larger than destination
// use only the center of the source image and apply on target.
int newImageWidth = (int)(sourceBounds.Height * destinationRatio);
SetWidthPreserveCenter(ref sourceBounds, newImageWidth);
}
else
{
// Target image is proportionnaly larger than source
// use all the source image and center it on destination.
int newImageWidth = (int)(destinationBounds.Height * sourceRatio);
SetWidthPreserveCenter(ref destinationBounds, newImageWidth);
}
}
private static void SetWidthPreserveCenter(ref Rectangle bounds, int newWidth)
{
int toRemove = bounds.Width - newWidth;
bounds.X += toRemove / 2;
bounds.Width -= toRemove;
}
}
#endregion
}
public static class VideoSpriteSheetExtensions
{
private const string _infoGeneratedOn = "GeneratedOn";
private const string _infoWebmMd5 = "WebmMd5";
private const string _infoSpriteSheetMd5 = "SpriteSheetMd5";
public static VideoSpriteSheet VideoSpriteSheet(this Tape tape)
{
var map = Map.FromPath(tape.Path);
return tape.VideoSpriteSheet(System.IO.Path.Combine(map.VideoCoachFolder, "edition_video_spritesheet.png"));
}
public static VideoSpriteSheet VideoSpriteSheet(this Tape tape, string spritesheetFilePath)
{
JObject GetMetadata()
{
if (File.Exists(spritesheetFilePath + ".info"))
{
try
{
return JObject.Parse(File.ReadAllText(spritesheetFilePath + ".info"));
}
catch { }
}
return null;
}
VideoSpriteSheet spriteSheet = tape.Extensions.cache;
if (spriteSheet == null)
{
spriteSheet = new VideoSpriteSheet(tape, new Size(200, 150));
tape.Extensions.cache = spriteSheet;
var map = Map.FromPath(tape.Path);
FileInfo spritesheetFile = new FileInfo(spritesheetFilePath);
string webmFilePath = System.IO.Path.Combine(map.VideoCoachFolder, map.Name + ".webm");
JObject metadata = GetMetadata();
if (!spritesheetFile.Exists
|| metadata is null
|| metadata.Value(_infoWebmMd5) != GetHash(webmFilePath)
|| metadata.Value(_infoSpriteSheetMd5) != GetHash(spritesheetFilePath))
{
GenerateSpriteSheet(spriteSheet, webmFilePath, spritesheetFilePath);
}
else
{
spriteSheet.LoadFromSpriteSheet(spritesheetFilePath);
}
}
return spriteSheet;
}
private static string GetHash(string filePath)
{
using (Stream stream = File.OpenRead(filePath))
{
return Convert.ToBase64String(MD5.Create().ComputeHash(stream));
}
}
private static void GenerateSpriteSheet(VideoSpriteSheet spritesheet, string webmFilePath, string spritesheetFilePath)
{
if (!File.Exists(webmFilePath))
{
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(15), "Cannot generate Video Preview", $"file \"{webmFilePath}\" not found", ToolTipIcon.Warning);
return;
}
TeaBox.ObjectModel.Application.Main.Dispatcher.DoLongOperationAsync(() =>
{
using (var progress = TeaBox.ObjectModel.Application.Main.Status.ReportProgress("Generating optimized edition video", ProgressReportingType.ProgressDialog))
{
string tempDirectory = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString());
try
{
Directory.CreateDirectory(tempDirectory);
FFmpegCommand command = new FFmpegCommand();
command.Inputs.Add(new FFmpegInput(webmFilePath));
command.OutputFileName = System.IO.Path.Combine(tempDirectory, "%5d.bmp");
command.Execute();
spritesheet.GenerateSpriteSheet(tempDirectory, spritesheetFilePath, progress);
File.WriteAllText(
spritesheetFilePath + ".info",
new JObject
{
[_infoGeneratedOn] = DateTime.Now.ToString(),
[_infoWebmMd5] = GetHash(webmFilePath),
[_infoSpriteSheetMd5] = GetHash(spritesheetFilePath),
}.ToString());
spritesheet.LoadFromSpriteSheet(spritesheetFilePath);
}
catch (Exception ex)
{
string message = "An error occurred generating the video sprite sheet. Details: " + ex.Message;
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(10), "No Video Preview available", message, ToolTipIcon.Warning);
}
finally
{
Directory.Delete(tempDirectory, true);
}
}
});
}
}
}