// --------------------------------------------------------------------------------------------------------------------
//
// 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.Interop
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Timers;
using System.Xml;
using HandBrake.Interop.Interop.HbLib;
using HandBrake.Interop.Interop.Helpers;
using HandBrake.Interop.Interop.Interfaces;
using HandBrake.Interop.Interop.Interfaces.EventArgs;
using HandBrake.Interop.Interop.Interfaces.Model;
using HandBrake.Interop.Interop.Interfaces.Model.Encoders;
using HandBrake.Interop.Interop.Interfaces.Model.Picture;
using HandBrake.Interop.Interop.Interfaces.Model.Preview;
using HandBrake.Interop.Interop.Json.Encode;
using HandBrake.Interop.Interop.Json.Scan;
using HandBrake.Interop.Interop.Json.State;
using HandBrake.Interop.Utilities;
public class HandBrakeInstance : IHandBrakeInstance, IDisposable
{
private const double ScanPollIntervalMs = 250;
private const double EncodePollIntervalMs = 250;
private Timer scanPollTimer;
private Timer encodePollTimer;
private bool disposed;
///
/// Finalizes an instance of the HandBrakeInstance class.
///
~HandBrakeInstance()
{
if (this.Handle != IntPtr.Zero)
{
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 number of previews created during scan.
///
public int PreviewCount { get; private set; }
///
/// Gets the list of titles on this instance.
///
public JsonScanObject Titles { get; private set; }
///
/// Gets the raw JSON for the list of titles on this instance.
///
public string TitlesJson { get; private set; }
///
/// Gets the index of the default title.
///
public int FeatureTitle { get; private set; }
///
/// Gets the HandBrake version string.
///
public string Version => Marshal.PtrToStringAnsi(HBFunctions.hb_get_version(this.Handle));
///
/// Gets the HandBrake build number.
///
public int Build => HBFunctions.hb_get_build(this.Handle);
public bool IsRemoteInstance => false;
///
/// Gets the handle.
///
internal IntPtr Handle { get; private set; }
///
/// Initializes this instance.
///
///
/// The code for the logging verbosity to use.
///
///
/// True disables hardware init.
///
public void Initialize(int verbosity, bool noHardware)
{
HandBrakeUtils.EnsureGlobalInit(noHardware);
HandBrakeUtils.RegisterLogger();
this.Handle = HBFunctions.hb_init(verbosity, update_check: 0);
}
///
/// 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)
{
this.PreviewCount = previewCount;
IntPtr pathPtr = InteropUtilities.ToUtf8PtrFromString(path);
HBFunctions.hb_scan(this.Handle, 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) =>
{
try
{
this.PollScanProgress();
}
catch (Exception exc)
{
Debug.WriteLine(exc);
HandBrakeUtils.SendErrorEvent(exc.ToString());
}
};
this.scanPollTimer.Start();
}
///
/// Stops an ongoing scan.
///
[HandleProcessCorruptedStateExceptions]
public void StopScan()
{
this.scanPollTimer.Stop();
HBFunctions.hb_scan_stop(this.Handle);
}
///
/// 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).
///
///
/// True to enable basic deinterlace of preview images.
///
///
/// An image with the requested preview.
///
[HandleProcessCorruptedStateExceptions]
public RawPreviewData GetPreview(PreviewSettings settings, int previewNumber, bool deinterlace)
{
SourceTitle title = this.Titles.TitleList.FirstOrDefault(t => t.Index == settings.TitleNumber);
// Create the Expected Output Geometry details for libhb.
hb_geometry_settings_s uiGeometry = new hb_geometry_settings_s
{
crop = new[] { settings.Cropping.Top, settings.Cropping.Bottom, settings.Cropping.Left, settings.Cropping.Right },
itu_par = 0,
keep = (int)HandBrakePictureHelpers.KeepSetting.HB_KEEP_WIDTH + (settings.KeepDisplayAspect ? 0x04 : 0), // TODO Keep Width?
maxWidth = settings.MaxWidth,
maxHeight = settings.MaxHeight,
mode = (int)(hb_anamorphic_mode_t)settings.Anamorphic,
modulus = settings.Modulus ?? 16,
geometry = new hb_geometry_s
{
height = settings.Height,
width = settings.Width,
par = new hb_rational_t { den = settings.PixelAspectY, num = settings.PixelAspectX }
}
};
// Fetch the image data from LibHb
IntPtr resultingImageStuct = HBFunctions.hb_get_preview2(this.Handle, settings.TitleNumber, previewNumber, ref uiGeometry, deinterlace ? 1 : 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);
RawPreviewData preview = new RawPreviewData(managedBuffer, stride_width, stride_height, image.width, image.height);
// 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);
return preview;
}
///
/// Determines if DRC can be applied to the given track with the given encoder.
///
/// The track Number.
/// The encoder to use for DRC.
/// The title.
/// True if DRC can be applied to the track with the given encoder.
public bool CanApplyDrc(int trackNumber, HBAudioEncoder encoder, int title)
{
return HBFunctions.hb_audio_can_apply_drc2(this.Handle, title, trackNumber, encoder.Id) > 0;
}
///
/// Starts an encode with the given job.
///
///
/// The encode Object.
///
[HandleProcessCorruptedStateExceptions]
public void StartEncode(JsonEncodeObject encodeObject)
{
string encode = JsonSerializer.Serialize(encodeObject, JsonSettings.Options);
this.StartEncode(encode);
}
///
/// Starts an encode with the given job JSON.
///
/// The JSON for the job to start.
[HandleProcessCorruptedStateExceptions]
public void StartEncode(string encodeJson)
{
HBFunctions.hb_add_json(this.Handle, InteropUtilities.ToUtf8PtrFromString(encodeJson));
HBFunctions.hb_start(this.Handle);
this.encodePollTimer = new Timer();
this.encodePollTimer.Interval = EncodePollIntervalMs;
this.encodePollTimer.Elapsed += (o, e) =>
{
try
{
this.PollEncodeProgress();
}
catch (Exception exc)
{
Debug.WriteLine(exc);
}
};
this.encodePollTimer.Start();
}
///
/// Pauses the current encode.
///
[HandleProcessCorruptedStateExceptions]
public void PauseEncode()
{
HBFunctions.hb_pause(this.Handle);
}
///
/// Resumes a paused encode.
///
[HandleProcessCorruptedStateExceptions]
public void ResumeEncode()
{
HBFunctions.hb_resume(this.Handle);
}
///
/// Stops the current encode.
///
[HandleProcessCorruptedStateExceptions]
public void StopEncode()
{
HBFunctions.hb_stop(this.Handle);
// 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.Handle);
for (int i = 0; i < jobs; i++)
{
currentJobs.Add(HBFunctions.hb_job(this.Handle, 0));
}
foreach (IntPtr job in currentJobs)
{
HBFunctions.hb_rem(this.Handle, job);
}
}
///
/// Checks the status of the ongoing encode.
///
///
/// The .
///
[HandleProcessCorruptedStateExceptions]
public JsonState GetEncodeProgress()
{
IntPtr json = HBFunctions.hb_get_state_json(this.Handle);
string statusJson = Marshal.PtrToStringAnsi(json);
JsonState state = JsonSerializer.Deserialize(statusJson, JsonSettings.Options);
return state;
}
///
/// Frees any resources associated with this object.
///
public void Dispose()
{
if (this.disposed)
{
return;
}
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Gets a value indicating whether the object is disposed.
///
public bool IsDisposed
{
get
{
return this.disposed;
}
}
///
/// 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.Handle);
HBFunctions.hb_close(handlePtr);
Marshal.FreeHGlobal(handlePtr);
this.disposed = true;
}
///
/// Checks the status of the ongoing scan.
///
[HandleProcessCorruptedStateExceptions]
private void PollScanProgress()
{
IntPtr json = HBFunctions.hb_get_state_json(this.Handle);
string statusJson = Marshal.PtrToStringAnsi(json);
JsonState state = null;
if (!string.IsNullOrEmpty(statusJson))
{
state = JsonSerializer.Deserialize(statusJson, JsonSettings.Options);
}
TaskState taskState = state != null ? TaskState.FromRepositoryValue(state.State) : null;
if (taskState != null && (taskState == TaskState.Scanning || taskState == TaskState.Searching))
{
if (this.ScanProgress != null && state.Scanning != null)
{
this.ScanProgress(this, new ScanProgressEventArgs(state.Scanning.Progress, state.Scanning.Preview, state.Scanning.PreviewCount, state.Scanning.Title, state.Scanning.TitleCount));
}
}
else if (taskState != null && taskState == TaskState.ScanDone)
{
this.scanPollTimer.Stop();
var jsonMsg = HBFunctions.hb_get_title_set_json(this.Handle);
this.TitlesJson = InteropUtilities.ToStringFromUtf8Ptr(jsonMsg);
if (!string.IsNullOrEmpty(this.TitlesJson))
{
this.Titles = JsonSerializer.Deserialize(this.TitlesJson, JsonSettings.Options);
if (this.Titles != null)
{
this.FeatureTitle = this.Titles.MainFeature;
}
}
if (this.ScanCompleted != null)
{
this.ScanCompleted(this, new System.EventArgs());
}
}
}
///
/// Checks the status of the ongoing encode.
///
[HandleProcessCorruptedStateExceptions]
private void PollEncodeProgress()
{
IntPtr json = HBFunctions.hb_get_state_json(this.Handle);
string statusJson = Marshal.PtrToStringAnsi(json);
JsonState state = JsonSerializer.Deserialize(statusJson, JsonSettings.Options);
TaskState taskState = state != null ? TaskState.FromRepositoryValue(state.State) : null;
if (taskState != null && (taskState == TaskState.Working || taskState == TaskState.Muxing || taskState == TaskState.Searching))
{
if (this.EncodeProgress != null)
{
TimeSpan eta = TimeSpan.FromSeconds(state?.Working?.ETASeconds ?? 0);
var progressEventArgs = new EncodeProgressEventArgs(0, 0, 0, TimeSpan.MinValue, 0, 0, 0, taskState.Code);
if (taskState == TaskState.Muxing || state.Working == null)
{
progressEventArgs = new EncodeProgressEventArgs(100, 0, 0, TimeSpan.MinValue, 0, 0, 0, taskState.Code);
}
else
{
progressEventArgs = new EncodeProgressEventArgs(state.Working.Progress, state.Working.Rate, state.Working.RateAvg, eta, state.Working.PassID, state.Working.Pass, state.Working.PassCount, taskState.Code);
}
this.EncodeProgress(this, progressEventArgs);
}
}
else if (taskState != null && taskState == TaskState.WorkDone)
{
this.encodePollTimer.Stop();
if (this.EncodeCompleted != null)
{
this.EncodeCompleted(
this,
new EncodeCompletedEventArgs(state.WorkDone.Error));
}
}
}
}
}