291 lines
No EOL
11 KiB
C#
291 lines
No EOL
11 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Keeps all the video images in memory as a sprite sheet so that it is really fast to display video with different effects.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Generates an atlas for the requested pngFolder
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Paints the requested image on the graphics.
|
|
/// </summary>
|
|
public void Paint(Graphics graphics, BeatUnit beat, Rectangle destinationBounds)
|
|
{
|
|
Paint(graphics, ToVideoFrame(beat), destinationBounds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes which frame in the video matches the specified frame in beat.
|
|
/// </summary>
|
|
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<string>(_infoWebmMd5) != GetHash(webmFilePath)
|
|
|| metadata.Value<string>(_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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} |