// -------------------------------------------------------------------------------------------------------------------- // // 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.ApplicationServices.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.ApplicationServices.Interop.EventArgs; using HandBrake.ApplicationServices.Interop.Factories; using HandBrake.ApplicationServices.Interop.HbLib; using HandBrake.ApplicationServices.Interop.Helpers; using HandBrake.ApplicationServices.Interop.Interfaces; using HandBrake.ApplicationServices.Interop.Json.Encode; using HandBrake.ApplicationServices.Interop.Json.Scan; using HandBrake.ApplicationServices.Interop.Json.Shared; using HandBrake.ApplicationServices.Interop.Json.State; using HandBrake.ApplicationServices.Interop.Model; using HandBrake.ApplicationServices.Interop.Model.Encoding; using HandBrake.ApplicationServices.Interop.Model.Preview; using HandBrake.ApplicationServices.Services.Logging; using HandBrake.ApplicationServices.Services.Logging.Model; using Newtonsoft.Json; using Size = HandBrake.ApplicationServices.Interop.Model.Size; /// /// A wrapper for a HandBrake instance. /// public class HandBrakeInstance : IHandBrakeInstance, IDisposable { /// /// The number of MS between status polls when scanning. /// private const double ScanPollIntervalMs = 250; /// /// The number of MS between status polls when encoding. /// private const double EncodePollIntervalMs = 250; /// /// The native handle to the HandBrake instance. /// private IntPtr hbHandle; /// /// The number of previews created during scan. /// private int previewCount; /// /// 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 JsonScanObject 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.hbHandle; } } /// /// Gets the number of previews created during scan. /// public int PreviewCount { get { return this.previewCount; } } /// /// Gets the list of titles on this instance. /// public JsonScanObject 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 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.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) => { try { this.PollScanProgress(); } catch (Exception exc) { Debug.WriteLine(exc); } }; this.scanPollTimer.Start(); } /// /// Stops an ongoing scan. /// [HandleProcessCorruptedStateExceptions] 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(PreviewSettings settings, int previewNumber) { SourceTitle title = this.Titles.TitleList.FirstOrDefault(t => t.Index == settings.TitleNumber); Validate.NotNull(title, "GetPreview: Title should not have been null. This is probably a bug."); // 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)AnamorphicFactory.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 = settings.Anamorphic != Anamorphic.Custom ? new hb_rational_t { den = title.Geometry.PAR.Den, num = title.Geometry.PAR.Num } : new hb_rational_t { den = settings.PixelAspectY, num = settings.PixelAspectX } } }; // Sanitize the input. Geometry resultGeometry = AnamorphicFactory.CreateGeometry(settings, new SourceVideoInfo(new Size(title.Geometry.Width, title.Geometry.Height), new Size(title.Geometry.PAR.Num, title.Geometry.PAR.Den))); int width = resultGeometry.Width * resultGeometry.PAR.Num / resultGeometry.PAR.Den; int height = resultGeometry.Height; uiGeometry.geometry.width = width; uiGeometry.geometry.height = height; uiGeometry.geometry.par.num = settings.PixelAspectX; uiGeometry.geometry.par.den = settings.PixelAspectY; // Fetch the image data from LibHb IntPtr resultingImageStuct = HBFunctions.hb_get_preview2(this.hbHandle, settings.TitleNumber, 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(width, height); BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), 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, width * 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; } } /// /// 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.hbHandle, title, trackNumber, encoder.Id) > 0; } /// /// Starts an encode with the given job. /// /// /// The encode Object. /// [HandleProcessCorruptedStateExceptions] public void StartEncode(JsonEncodeObject encodeObject) { 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) => { try { this.PollEncodeProgress(); } catch (Exception exc) { Debug.WriteLine(exc); } }; 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); } /// /// 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.hbHandle); 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.hbHandle); string statusJson = Marshal.PtrToStringAnsi(json); LogHelper.LogMessage(new LogMessage(statusJson, LogMessageType.progressJson, LogLevel.debug)); JsonState state = JsonConvert.DeserializeObject(statusJson); if (state != null && 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 != null && state.State == NativeConstants.HB_STATE_SCANDONE) { var jsonMsg = HBFunctions.hb_get_title_set_json(this.hbHandle); string scanJson = InteropUtilities.ToStringFromUtf8Ptr(jsonMsg); LogHelper.LogMessage(new LogMessage(scanJson, LogMessageType.scanJson, LogLevel.debug)); this.titles = JsonConvert.DeserializeObject(scanJson); this.featureTitle = this.titles.MainFeature; 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. /// [HandleProcessCorruptedStateExceptions] private void PollEncodeProgress() { IntPtr json = HBFunctions.hb_get_state_json(this.hbHandle); string statusJson = Marshal.PtrToStringAnsi(json); LogHelper.LogMessage(new LogMessage(statusJson, LogMessageType.progressJson, LogLevel.debug)); JsonState state = JsonConvert.DeserializeObject(statusJson); if (state != null && 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 = state.Working.Pass, PassCount = state.Working.PassCount }; this.EncodeProgress(this, progressEventArgs); } } else if (state != null && 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 }); } } } } }