// --------------------------------------------------------------------------------------------------------------------
//
// This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
//
//
// A wrapper for a HandBrake instance.
//
// --------------------------------------------------------------------------------------------------------------------
namespace HandBrake.Interop
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows.Media.Imaging;
using HandBrake.Interop.EventArgs;
using HandBrake.Interop.HbLib;
using HandBrake.Interop.Helpers;
using HandBrake.Interop.Interfaces;
using HandBrake.Interop.Json.Encode;
using HandBrake.Interop.Json.Factories;
using HandBrake.Interop.Json.Scan;
using HandBrake.Interop.Json.State;
using HandBrake.Interop.Model;
using HandBrake.Interop.Model.Encoding;
using HandBrake.Interop.Model.Scan;
using Newtonsoft.Json;
using Geometry = HandBrake.Interop.Json.Anamorphic.Geometry;
///
/// A wrapper for a HandBrake instance.
///
public class HandBrakeInstance : IHandBrakeInstance, IDisposable
{
///
/// The number of MS between status polls when scanning.
///
private const double ScanPollIntervalMs = 200;
///
/// The number of MS between status polls when encoding.
///
private const double EncodePollIntervalMs = 200;
///
/// The native handle to the HandBrake instance.
///
private IntPtr hbHandle;
///
/// The timer to poll for scan status.
///
private Timer scanPollTimer;
///
/// The timer to poll for encode status.
///
private Timer encodePollTimer;
///
/// The list of titles on this instance.
///
private List
titles;
///
/// The index of the default title.
///
private int featureTitle;
///
/// A value indicating whether this object has been disposed or not.
///
private bool disposed;
///
/// Finalizes an instance of the HandBrakeInstance class.
///
~HandBrakeInstance()
{
this.Dispose(false);
}
///
/// Fires for progress updates when scanning.
///
public event EventHandler ScanProgress;
///
/// Fires when a scan has completed.
///
public event EventHandler ScanCompleted;
///
/// Fires for progress updates when encoding.
///
public event EventHandler EncodeProgress;
///
/// Fires when an encode has completed.
///
public event EventHandler EncodeCompleted;
///
/// Gets the handle.
///
internal IntPtr Handle
{
get
{
return this.Handle;
}
}
///
/// Gets the list of titles on this instance.
///
public List Titles
{
get
{
return this.titles;
}
}
///
/// Gets the index of the default title.
///
public int FeatureTitle
{
get
{
return this.featureTitle;
}
}
///
/// Gets the HandBrake version string.
///
public string Version
{
get
{
var versionPtr = HBFunctions.hb_get_version(this.hbHandle);
return Marshal.PtrToStringAnsi(versionPtr);
}
}
///
/// Gets the HandBrake build number.
///
public int Build
{
get
{
return HBFunctions.hb_get_build(this.hbHandle);
}
}
///
/// Initializes this instance.
///
///
/// The code for the logging verbosity to use.
///
public void Initialize(int verbosity)
{
HandBrakeUtils.EnsureGlobalInit();
HandBrakeUtils.RegisterLogger();
this.hbHandle = HBFunctions.hb_init(verbosity, update_check: 0);
}
///
/// Starts scanning the given path.
///
///
/// The path to the video to scan.
///
///
/// The number of preview images to make.
///
///
/// The minimum duration of a title to show up on the scan.
///
public void StartScan(string path, int previewCount, TimeSpan minDuration)
{
this.StartScan(path, previewCount, minDuration, 0);
}
///
/// Starts a scan for the given input path.
///
///
/// The path of the video to scan.
///
///
/// The number of preview images to generate for each title while scanning.
///
public void StartScan(string path, int previewCount)
{
this.StartScan(path, previewCount, TimeSpan.FromSeconds(10), 0);
}
///
/// Starts a scan of the given path.
///
///
/// The path of the video to scan.
///
///
/// The number of preview images to generate for each title while scanning.
///
///
/// The title index to scan (1-based, 0 for all titles).
///
public void StartScan(string path, int previewCount, int titleIndex)
{
this.StartScan(path, previewCount, TimeSpan.Zero, titleIndex);
}
///
/// Starts a scan of the given path.
///
///
/// The path of the video to scan.
///
///
/// The number of previews to make on each title.
///
///
/// The minimum duration of a title to show up on the scan.
///
///
/// The title index to scan (1-based, 0 for all titles).
///
public void StartScan(string path, int previewCount, TimeSpan minDuration, int titleIndex)
{
IntPtr pathPtr = InteropUtilities.ToUtf8PtrFromString(path);
HBFunctions.hb_scan(this.hbHandle, pathPtr, titleIndex, previewCount, 1, (ulong)(minDuration.TotalSeconds * 90000));
Marshal.FreeHGlobal(pathPtr);
this.scanPollTimer = new Timer();
this.scanPollTimer.Interval = ScanPollIntervalMs;
// Lambda notation used to make sure we can view any JIT exceptions the method throws
this.scanPollTimer.Elapsed += (o, e) =>
{
this.PollScanProgress();
};
this.scanPollTimer.Start();
}
///
/// Stops an ongoing scan.
///
public void StopScan()
{
HBFunctions.hb_scan_stop(this.hbHandle);
}
///
/// Gets an image for the given job and preview
///
///
/// Only incorporates sizing and aspect ratio into preview image.
///
///
/// The encode job to preview.
///
///
/// The index of the preview to get (0-based).
///
///
/// An image with the requested preview.
///
[HandleProcessCorruptedStateExceptions]
public BitmapImage GetPreview(EncodeJob job, int previewNumber)
{
Title title = this.Titles.FirstOrDefault(t => t.TitleNumber == job.Title);
Validate.NotNull(title, "GetPreview: Title should not have been null. This is probably a bug.");
// Creat the Expected Output Geometry details for libhb.
hb_geometry_settings_s uiGeometry = new hb_geometry_settings_s
{
crop = new[] { job.Cropping.Top, job.Cropping.Bottom, job.Cropping.Left, job.Cropping.Right },
itu_par = 0,
keep = (int)AnamorphicFactory.KeepSetting.HB_KEEP_WIDTH + (job.KeepDisplayAspect ? 0x04 : 0), // TODO Keep Width?
maxWidth = job.MaxWidth,
maxHeight = job.MaxHeight,
mode = (int)(hb_anamorphic_mode_t)job.Anamorphic,
modulus = job.Modulus,
geometry = new hb_geometry_s
{
height = job.Height,
width = job.Width,
par = job.Anamorphic != Anamorphic.Custom
? new hb_rational_t { den = title.ParVal.Height, num = title.ParVal.Width }
: new hb_rational_t { den = job.PixelAspectY, num = job.PixelAspectX }
}
};
// Sanatise the input.
Geometry resultGeometry = AnamorphicFactory.CreateGeometry(job, title, AnamorphicFactory.KeepSetting.HB_KEEP_WIDTH); // TODO this keep isn't right.
int width = resultGeometry.Width * resultGeometry.PAR.Num / resultGeometry.PAR.Den;
int height = resultGeometry.Height;
uiGeometry.geometry.height = resultGeometry.Height; // Prased the height now.
int outputWidth = width;
int outputHeight = height;
// Fetch the image data from LibHb
IntPtr resultingImageStuct = HBFunctions.hb_get_preview2(this.hbHandle, job.Title, previewNumber, ref uiGeometry, 0);
hb_image_s image = InteropUtilities.ToStructureFromPtr(resultingImageStuct);
// Copy the filled image buffer to a managed array.
int stride_width = image.plane[0].stride;
int stride_height = image.plane[0].height_stride;
int imageBufferSize = stride_width * stride_height; // int imageBufferSize = outputWidth * outputHeight * 4;
byte[] managedBuffer = new byte[imageBufferSize];
Marshal.Copy(image.plane[0].data, managedBuffer, 0, imageBufferSize);
var bitmap = new Bitmap(outputWidth, outputHeight);
BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, outputWidth, outputHeight), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
IntPtr ptr = bitmapData.Scan0; // Pointer to the first pixel.
for (int i = 0; i < image.height; i++)
{
try
{
Marshal.Copy(managedBuffer, i * stride_width, ptr, stride_width);
ptr = IntPtr.Add(ptr, outputWidth * 4);
}
catch (Exception exc)
{
Debug.WriteLine(exc); // In theory, this will allow a partial image display if this happens. TODO add better logging of this.
}
}
bitmap.UnlockBits(bitmapData);
// Close the image so we don't leak memory.
IntPtr nativeJobPtrPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
Marshal.WriteIntPtr(nativeJobPtrPtr, resultingImageStuct);
HBFunctions.hb_image_close(nativeJobPtrPtr);
Marshal.FreeHGlobal(nativeJobPtrPtr);
// Create a Bitmap Image for display.
using (var memoryStream = new MemoryStream())
{
try
{
bitmap.Save(memoryStream, ImageFormat.Bmp);
}
finally
{
bitmap.Dispose();
}
var wpfBitmap = new BitmapImage();
wpfBitmap.BeginInit();
wpfBitmap.CacheOption = BitmapCacheOption.OnLoad;
wpfBitmap.StreamSource = memoryStream;
wpfBitmap.EndInit();
wpfBitmap.Freeze();
return wpfBitmap;
}
}
///
/// Starts an encode with the given job.
///
///
/// The job to start.
///
///
/// The title.
///
///
/// The scan Preview Count.
///
[HandleProcessCorruptedStateExceptions]
public void StartEncode(EncodeJob jobToStart, Title title, int scanPreviewCount)
{
this.StartEncode(jobToStart, title, false, 0, 0, 0, scanPreviewCount);
}
///
/// Starts an encode with the given job.
///
///
/// The job to start.
///
///
/// The title.
///
///
/// The scan Preview Count.
///
///
/// Preview Feature: Preview to encode
///
///
/// Number of seconds to encode for the preview
///
///
/// The overall Selected Length Seconds.
///
///
/// Number of previews
///
[HandleProcessCorruptedStateExceptions]
public void StartEncode(EncodeJob job, Title title, bool preview, int previewNumber, int previewSeconds, double overallSelectedLengthSeconds, int scanPreviewCount)
{
JsonEncodeObject encodeObject = EncodeFactory.Create(job, title);
JsonSerializerSettings settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
};
string encode = JsonConvert.SerializeObject(encodeObject, Formatting.Indented, settings);
HBFunctions.hb_add_json(this.hbHandle, InteropUtilities.ToUtf8PtrFromString(encode));
HBFunctions.hb_start(this.hbHandle);
this.encodePollTimer = new Timer();
this.encodePollTimer.Interval = EncodePollIntervalMs;
this.encodePollTimer.Elapsed += (o, e) =>
{
this.PollEncodeProgress();
};
this.encodePollTimer.Start();
}
///
/// Pauses the current encode.
///
[HandleProcessCorruptedStateExceptions]
public void PauseEncode()
{
HBFunctions.hb_pause(this.hbHandle);
}
///
/// Resumes a paused encode.
///
[HandleProcessCorruptedStateExceptions]
public void ResumeEncode()
{
HBFunctions.hb_resume(this.hbHandle);
}
///
/// Stops the current encode.
///
[HandleProcessCorruptedStateExceptions]
public void StopEncode()
{
HBFunctions.hb_stop(this.hbHandle);
// Also remove all jobs from the queue (in case we stopped a 2-pass encode)
var currentJobs = new List();
int jobs = HBFunctions.hb_count(this.hbHandle);
for (int i = 0; i < jobs; i++)
{
currentJobs.Add(HBFunctions.hb_job(this.hbHandle, 0));
}
foreach (IntPtr job in currentJobs)
{
HBFunctions.hb_rem(this.hbHandle, job);
}
}
///
/// Frees any resources associated with this object.
///
public void Dispose()
{
if (this.disposed)
{
return;
}
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Frees any resources associated with this object.
///
///
/// True if managed objects as well as unmanaged should be disposed.
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Free other state (managed objects).
}
// Free unmanaged objects.
IntPtr handlePtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
Marshal.WriteIntPtr(handlePtr, this.hbHandle);
HBFunctions.hb_close(handlePtr);
Marshal.FreeHGlobal(handlePtr);
this.disposed = true;
}
///
/// Checks the status of the ongoing scan.
///
private void PollScanProgress()
{
IntPtr json = HBFunctions.hb_get_state_json(this.hbHandle);
string statusJson = Marshal.PtrToStringAnsi(json);
JsonState state = JsonConvert.DeserializeObject(statusJson);
if (state.State == NativeConstants.HB_STATE_SCANNING)
{
if (this.ScanProgress != null)
{
this.ScanProgress(this, new ScanProgressEventArgs
{
Progress = state.Scanning.Progress,
CurrentPreview = state.Scanning.Preview,
Previews = state.Scanning.PreviewCount,
CurrentTitle = state.Scanning.Title,
Titles = state.Scanning.TitleCount
});
}
}
else if (state.State == NativeConstants.HB_STATE_SCANDONE)
{
this.titles = new List();
var jsonMsg = HBFunctions.hb_get_title_set_json(this.hbHandle);
string scanJson = InteropUtilities.ToStringFromUtf8Ptr(jsonMsg);
JsonScanObject scanObject = JsonConvert.DeserializeObject(scanJson);
foreach (Title title in ScanFactory.CreateTitleSet(scanObject))
{
// Set the Main Title.
this.featureTitle = title.IsMainFeature ? title.TitleNumber : 0;
this.titles.Add(title);
}
this.scanPollTimer.Stop();
if (this.ScanCompleted != null)
{
this.ScanCompleted(this, new System.EventArgs());
}
}
}
///
/// Checks the status of the ongoing encode.
///
///
/// Checks the status of the ongoing encode.
///
private void PollEncodeProgress()
{
IntPtr json = HBFunctions.hb_get_state_json(this.hbHandle);
string statusJson = Marshal.PtrToStringAnsi(json);
JsonState state = JsonConvert.DeserializeObject(statusJson);
if (state.State == NativeConstants.HB_STATE_WORKING)
{
if (this.EncodeProgress != null)
{
var progressEventArgs = new EncodeProgressEventArgs
{
FractionComplete = state.Working.Progress,
CurrentFrameRate = state.Working.Rate,
AverageFrameRate = state.Working.RateAvg,
EstimatedTimeLeft = new TimeSpan(state.Working.Hours, state.Working.Minutes, state.Working.Seconds),
Pass = 1, // TODO
};
this.EncodeProgress(this, progressEventArgs);
}
}
else if (state.State == NativeConstants.HB_STATE_WORKDONE)
{
this.encodePollTimer.Stop();
if (this.EncodeCompleted != null)
{
this.EncodeCompleted(this, new EncodeCompletedEventArgs
{
Error = state.WorkDone.Error != (int)hb_error_code.HB_ERROR_NONE
});
}
}
}
}
}