diff options
author | sr55 <[email protected]> | 2020-10-24 16:52:16 +0100 |
---|---|---|
committer | sr55 <[email protected]> | 2020-10-24 16:52:16 +0100 |
commit | 030ecc52477a7636aeab96a5496b640884b85f1b (patch) | |
tree | ba023cc872eeb1ab5c34304db74071c634599021 /win/CS/HandBrakeWPF/Services | |
parent | 85bba8d17e5398183ba380225fe6b121a57e75f2 (diff) |
WinGui: Simultaneous encode improvements.
- Improvements to the hardware resource manager that determines simultaneous job processing.
- Reliability improvements in multiple log file handling.
Diffstat (limited to 'win/CS/HandBrakeWPF/Services')
8 files changed, 137 insertions, 187 deletions
diff --git a/win/CS/HandBrakeWPF/Services/Encode/EncodeBase.cs b/win/CS/HandBrakeWPF/Services/Encode/EncodeBase.cs index 5563c2ed5..75dcee5e9 100644 --- a/win/CS/HandBrakeWPF/Services/Encode/EncodeBase.cs +++ b/win/CS/HandBrakeWPF/Services/Encode/EncodeBase.cs @@ -19,17 +19,14 @@ namespace HandBrakeWPF.Services.Encode using HandBrakeWPF.Services.Interfaces; using HandBrakeWPF.Utilities; - using EncodeCompletedEventArgs = HandBrakeWPF.Services.Encode.EventArgs.EncodeCompletedEventArgs; - using EncodeCompletedStatus = HandBrakeWPF.Services.Encode.Interfaces.EncodeCompletedStatus; - using EncodeProgessStatus = HandBrakeWPF.Services.Encode.Interfaces.EncodeProgessStatus; - using EncodeProgressEventArgs = HandBrakeWPF.Services.Encode.EventArgs.EncodeProgressEventArgs; - using EncodeTask = HandBrakeWPF.Services.Encode.Model.EncodeTask; - using GeneralApplicationException = HandBrakeWPF.Exceptions.GeneralApplicationException; - using ILog = HandBrakeWPF.Services.Logging.Interfaces.ILog; - - /// <summary> - /// A Base Class for the Encode Services. - /// </summary> + using EncodeCompletedEventArgs = EventArgs.EncodeCompletedEventArgs; + using EncodeCompletedStatus = Interfaces.EncodeCompletedStatus; + using EncodeProgessStatus = Interfaces.EncodeProgessStatus; + using EncodeProgressEventArgs = EventArgs.EncodeProgressEventArgs; + using EncodeTask = Model.EncodeTask; + using GeneralApplicationException = Exceptions.GeneralApplicationException; + using ILog = Logging.Interfaces.ILog; + public class EncodeBase { protected ILog encodeLogService; @@ -41,90 +38,33 @@ namespace HandBrakeWPF.Services.Encode this.userSettingService = userSettingService; } - #region Events - - /// <summary> - /// Fires when a new QueueTask starts - /// </summary> public event EventHandler EncodeStarted; - /// <summary> - /// Fires when a QueueTask finishes. - /// </summary> public event EncodeCompletedStatus EncodeCompleted; - /// <summary> - /// Encode process has progressed - /// </summary> public event EncodeProgessStatus EncodeStatusChanged; - #endregion - - #region Properties - - /// <summary> - /// Gets or sets a value indicating whether IsEncoding. - /// </summary> public bool IsEncoding { get; protected set; } - #endregion - - #region Invoke Events - - /// <summary> - /// Invoke the Encode Status Changed Event. - /// </summary> - /// <param name="e"> - /// The EncodeProgressEventArgs. - /// </param> public void InvokeEncodeStatusChanged(EncodeProgressEventArgs e) { EncodeProgessStatus handler = this.EncodeStatusChanged; handler?.Invoke(this, e); } - /// <summary> - /// Invoke the Encode Completed Event - /// </summary> - /// <param name="e"> - /// The EncodeCompletedEventArgs. - /// </param> public void InvokeEncodeCompleted(EncodeCompletedEventArgs e) { EncodeCompletedStatus handler = this.EncodeCompleted; handler?.Invoke(this, e); } - /// <summary> - /// Invoke the Encode Started Event - /// </summary> - /// <param name="e"> - /// The EventArgs. - /// </param> public void InvokeEncodeStarted(System.EventArgs e) { EventHandler handler = this.EncodeStarted; handler?.Invoke(this, e); } - #endregion - - #region Methods - - /// <summary> - /// Save a copy of the log to the users desired location or a default location - /// if this feature is enabled in options. - /// </summary> - /// <param name="destination"> - /// The Destination File Path - /// </param> - /// <param name="isPreview"> - /// The is Preview. - /// </param> - /// <param name="configuration"> - /// The configuration. - /// </param> - public string ProcessLogs(string destination, bool isPreview, HBConfiguration configuration) + public string ProcessLogs(string destination) { try { @@ -165,15 +105,6 @@ namespace HandBrakeWPF.Services.Encode return null; } - /// <summary> - /// Verify the Encode Destination path exists and if not, create it. - /// </summary> - /// <param name="task"> - /// The task. - /// </param> - /// <exception cref="Exception"> - /// If the creation fails, an exception is thrown. - /// </exception> protected void VerifyEncodeDestinationPath(EncodeTask task) { // Make sure the path exists, attempt to create it if it doesn't @@ -192,15 +123,6 @@ namespace HandBrakeWPF.Services.Encode } } - /// <summary> - /// The write file. - /// </summary> - /// <param name="content"> - /// The content. - /// </param> - /// <param name="fileName"> - /// The file name. - /// </param> private void WriteFile(string content, string fileName) { try @@ -215,7 +137,5 @@ namespace HandBrakeWPF.Services.Encode Debug.WriteLine(exc); } } - - #endregion } }
\ No newline at end of file diff --git a/win/CS/HandBrakeWPF/Services/Encode/LibEncode.cs b/win/CS/HandBrakeWPF/Services/Encode/LibEncode.cs index 82504aa0e..0cfd6e0f3 100644 --- a/win/CS/HandBrakeWPF/Services/Encode/LibEncode.cs +++ b/win/CS/HandBrakeWPF/Services/Encode/LibEncode.cs @@ -44,7 +44,6 @@ namespace HandBrakeWPF.Services.Encode private IEncodeInstance instance; private DateTime startTime; private EncodeTask currentTask; - private HBConfiguration currentConfiguration; private bool isPreviewInstance; private bool isLoggingInitialised; private bool isEncodeComplete; @@ -74,12 +73,11 @@ namespace HandBrakeWPF.Services.Encode // Setup this.startTime = DateTime.Now; this.currentTask = task; - this.currentConfiguration = configuration; - + this.isPreviewInstance = task.IsPreviewEncode; if (this.userSettingService.GetUserSetting<bool>(UserSettingConstants.ProcessIsolationEnabled)) { - this.InitLogging(task.IsPreviewEncode); + this.InitLogging(); } else { @@ -120,7 +118,6 @@ namespace HandBrakeWPF.Services.Encode this.instance.EncodeProgress += this.InstanceEncodeProgress; this.IsEncoding = true; - this.isPreviewInstance = task.IsPreviewEncode; // Verify the Destination Path Exists, and if not, create it. this.VerifyEncodeDestinationPath(task); @@ -252,8 +249,10 @@ namespace HandBrakeWPF.Services.Encode this.ServiceLogMessage(completeMessage); + this.logInstanceManager.Deregister(this.GetLogFilename()); + // Handling Log Data - string hbLog = this.ProcessLogs(this.currentTask.Destination, this.isPreviewInstance, this.currentConfiguration); + string hbLog = this.ProcessLogs(this.currentTask.Destination); long filesize = this.GetFilesize(this.currentTask.Destination); // Raise the Encode Completed Event. @@ -283,19 +282,27 @@ namespace HandBrakeWPF.Services.Encode return 0; } - private void InitLogging(bool isPreview) + private void InitLogging() { if (!isLoggingInitialised) { - string logType = isPreview ? "preview" : "encode"; - string filename = string.Format("activity_log.{0}.{1}.{2}.txt", encodeCounter, logType, GeneralUtilities.ProcessId); - string logFile = Path.Combine(DirectoryUtilities.GetLogDirectory(), filename); this.encodeLogService = new LogService(); - this.encodeLogService.ConfigureLogging(logFile); + this.encodeLogService.ConfigureLogging(GetFullLogPath()); this.encodeLogService.SetId(this.encodeCounter); - this.logInstanceManager.RegisterLoggerInstance(filename, this.encodeLogService, false); + this.logInstanceManager.Register(this.GetLogFilename(), this.encodeLogService, false); isLoggingInitialised = true; } } + + private string GetLogFilename() + { + string logType = this.isPreviewInstance ? "preview" : "encode"; + return string.Format("activity_log.{0}.{1}.{2}.txt", encodeCounter, logType, GeneralUtilities.ProcessId); + } + + private string GetFullLogPath() + { + return Path.Combine(DirectoryUtilities.GetLogDirectory(), GetLogFilename()); + } } } diff --git a/win/CS/HandBrakeWPF/Services/Logging/Interfaces/ILogInstanceManager.cs b/win/CS/HandBrakeWPF/Services/Logging/Interfaces/ILogInstanceManager.cs index c8af64601..a5c151308 100644 --- a/win/CS/HandBrakeWPF/Services/Logging/Interfaces/ILogInstanceManager.cs +++ b/win/CS/HandBrakeWPF/Services/Logging/Interfaces/ILogInstanceManager.cs @@ -32,7 +32,13 @@ namespace HandBrakeWPF.Services.Logging.Interfaces /// <param name="isMaster"> /// True indicates it's the log instance for the parent handbrake process. /// </param> - void RegisterLoggerInstance(string filename, ILog log, bool isMaster); + void Register(string filename, ILog log, bool isMaster); + + /// <summary> + /// Remove a log file when we are done with it. + /// </summary> + /// <param name="filename">The filename of the log to remove.</param> + void Deregister(string filename); /// <summary> /// Gets a list of files without their associated ILog instances. diff --git a/win/CS/HandBrakeWPF/Services/Logging/LogInstanceManager.cs b/win/CS/HandBrakeWPF/Services/Logging/LogInstanceManager.cs index 42f85cc53..02983da21 100644 --- a/win/CS/HandBrakeWPF/Services/Logging/LogInstanceManager.cs +++ b/win/CS/HandBrakeWPF/Services/Logging/LogInstanceManager.cs @@ -12,42 +12,21 @@ namespace HandBrakeWPF.Services.Logging using System.Collections.Generic; using System.IO; using System.Linq; - using System.Windows.Media.Animation; - using HandBrakeWPF.Services.Interfaces; using HandBrakeWPF.Services.Logging.Interfaces; public class LogInstanceManager : ILogInstanceManager { - private readonly IUserSettingService userSettingService; - private readonly object instanceLock = new object(); private Dictionary<string, ILog> logInstances = new Dictionary<string, ILog>(); - private int maxInstances; - - public LogInstanceManager(IUserSettingService userSettingService) - { - this.userSettingService = userSettingService; - this.maxInstances = this.userSettingService.GetUserSetting<int>(UserSettingConstants.SimultaneousEncodes); - userSettingService.SettingChanged += this.UserSettingService_SettingChanged; - } - - private void UserSettingService_SettingChanged(object sender, HandBrakeWPF.EventArgs.SettingChangedEventArgs e) - { - if (e.Key == UserSettingConstants.SimultaneousEncodes) - { - this.maxInstances = this.userSettingService.GetUserSetting<int>(UserSettingConstants.SimultaneousEncodes); - } - } - public event EventHandler NewLogInstanceRegistered; public string ApplicationAndScanLog { get; private set; } public ILog MasterLogInstance { get; private set; } - public void RegisterLoggerInstance(string filename, ILog log, bool isMaster) + public void Register(string filename, ILog log, bool isMaster) { lock (this.instanceLock) { @@ -59,8 +38,6 @@ namespace HandBrakeWPF.Services.Logging this.logInstances.Add(filename, log); - this.CleanupInstance(); - if (isMaster) { this.MasterLogInstance = log; @@ -70,11 +47,34 @@ namespace HandBrakeWPF.Services.Logging this.OnNewLogInstanceRegistered(); } + public void Deregister(string filename) + { + lock (this.instanceLock) + { + if (this.logInstances.ContainsKey(filename)) + { + this.logInstances.Remove(filename); + } + } + + this.OnNewLogInstanceRegistered(); + } + public List<string> GetLogFiles() { lock (this.instanceLock) { - return this.logInstances.Keys.ToList(); + List<string> encodeLogs = this.logInstances.Keys.Where(s => !s.Contains("main")).OrderBy(s => s).ToList(); + + List<string> finalList = new List<string>(); + if (this.MasterLogInstance != null) + { + finalList.Add(Path.GetFileName(this.MasterLogInstance.FileName)); + } + + finalList.AddRange(encodeLogs); + + return finalList; } } @@ -101,33 +101,5 @@ namespace HandBrakeWPF.Services.Logging { this.NewLogInstanceRegistered?.Invoke(this, System.EventArgs.Empty); } - - private void CleanupInstance() - { - List<int> encodeLogs = new List<int>(); - foreach (ILog logInstance in this.logInstances.Values) - { - if (logInstance.LogId != -1) - { - encodeLogs.Add(logInstance.LogId); - } - } - - encodeLogs.Sort(); - - if (encodeLogs.Count > 0 && encodeLogs.Count > this.maxInstances) - { - int idToRemove = encodeLogs.FirstOrDefault(); - - KeyValuePair<string, ILog> service = this.logInstances.FirstOrDefault(i => i.Value.LogId == idToRemove); - - string filename = Path.GetFileName(service.Value.FileName); - - if (this.logInstances.ContainsKey(filename)) - { - this.logInstances.Remove(filename); - } - } - } } } diff --git a/win/CS/HandBrakeWPF/Services/Queue/ActiveJob.cs b/win/CS/HandBrakeWPF/Services/Queue/ActiveJob.cs index d433e1625..3658a4120 100644 --- a/win/CS/HandBrakeWPF/Services/Queue/ActiveJob.cs +++ b/win/CS/HandBrakeWPF/Services/Queue/ActiveJob.cs @@ -10,12 +10,9 @@ namespace HandBrakeWPF.Services.Queue { using System; - - using HandBrakeWPF.Services.Encode; + using HandBrakeWPF.Services.Encode.EventArgs; using HandBrakeWPF.Services.Encode.Interfaces; - using HandBrakeWPF.Services.Interfaces; - using HandBrakeWPF.Services.Logging.Interfaces; using HandBrakeWPF.Services.Queue.JobEventArgs; using HandBrakeWPF.Services.Queue.Model; @@ -24,10 +21,10 @@ namespace HandBrakeWPF.Services.Queue private readonly QueueTask job; private readonly IEncode encodeService; - public ActiveJob(QueueTask task, IUserSettingService userSettingService, ILogInstanceManager logInstanceManager, int jobId, IPortService portService) + public ActiveJob(QueueTask task, IEncode encodeService) { this.job = task; - this.encodeService = new LibEncode(userSettingService, logInstanceManager, jobId, portService); + this.encodeService = encodeService; } public event EventHandler<ActiveJobCompletedEventArgs> JobFinished; diff --git a/win/CS/HandBrakeWPF/Services/Queue/Model/QueueTask.cs b/win/CS/HandBrakeWPF/Services/Queue/Model/QueueTask.cs index bc83f219b..27e7eaa0c 100644 --- a/win/CS/HandBrakeWPF/Services/Queue/Model/QueueTask.cs +++ b/win/CS/HandBrakeWPF/Services/Queue/Model/QueueTask.cs @@ -71,7 +71,7 @@ namespace HandBrakeWPF.Services.Queue.Model public string ScannedSourcePath { get; set; }
[JsonIgnore]
- public Guid? HardwareResourceToken { get; set; }
+ public Guid? TaskToken { get; set; }
[JsonProperty]
public QueueItemStatus Status
diff --git a/win/CS/HandBrakeWPF/Services/Queue/QueueResourceService.cs b/win/CS/HandBrakeWPF/Services/Queue/QueueResourceService.cs index 0da86be98..819a5ae87 100644 --- a/win/CS/HandBrakeWPF/Services/Queue/QueueResourceService.cs +++ b/win/CS/HandBrakeWPF/Services/Queue/QueueResourceService.cs @@ -27,21 +27,54 @@ namespace HandBrakeWPF.Services.Queue private readonly HashSet<Guid> qsvInstances = new HashSet<Guid>(); private readonly HashSet<Guid> nvencInstances = new HashSet<Guid>(); private readonly HashSet<Guid> vceInstances = new HashSet<Guid>(); + private readonly HashSet<Guid> totalInstances = new HashSet<Guid>(); private List<int> qsvGpus = new List<int>(); private int intelGpuCounter = -1; // Always default to the first card when allocating. + private int maxAllowedInstances; + private int totalQsvInstances; + private int totalVceInstances; + private int totalNvidiaInstances; + public QueueResourceService(IUserSettingService userSettingService) { this.userSettingService = userSettingService; } + public int TotalActiveInstances + { + get + { + lock (this.lockOjb) + { + return this.totalInstances.Count; + } + } + } + public void Init() { + this.maxAllowedInstances = this.userSettingService.GetUserSetting<int>(UserSettingConstants.SimultaneousEncodes); + + // Allow QSV adapter scaling. this.qsvGpus = HandBrakeEncoderHelpers.GetQsvAdaptorList(); + this.totalQsvInstances = this.qsvGpus.Count * 2; // Allow two instances per GPU + + // Most Nvidia cards support 3 instances. + this.totalNvidiaInstances = 3; + + // VCE Support still TBD + this.totalVceInstances = 1; + + // Whether using hardware or not, some CPU is needed so don't allow more jobs than CPU. + if (this.maxAllowedInstances > Utilities.SystemInfo.GetCpuCoreCount) + { + this.maxAllowedInstances = Utilities.SystemInfo.GetCpuCoreCount; + } } - public Guid? GetHardwareLock(EncodeTask task) + public Guid? GetToken(EncodeTask task) { lock (this.lockOjb) { @@ -50,51 +83,64 @@ namespace HandBrakeWPF.Services.Queue case VideoEncoder.QuickSync: case VideoEncoder.QuickSyncH265: case VideoEncoder.QuickSyncH26510b: - if (this.qsvInstances.Count < 3) + if (this.qsvInstances.Count < this.totalQsvInstances && this.TotalActiveInstances <= this.maxAllowedInstances) { this.AllocateIntelGPU(task); Guid guid = Guid.NewGuid(); this.qsvInstances.Add(guid); + this.totalInstances.Add(guid); return guid; } else { - return Guid.Empty; + return Guid.Empty; // Busy } case VideoEncoder.NvencH264: case VideoEncoder.NvencH265: - if (this.nvencInstances.Count < 3) + if (this.nvencInstances.Count < this.totalNvidiaInstances && this.TotalActiveInstances <= this.maxAllowedInstances) { Guid guid = Guid.NewGuid(); this.nvencInstances.Add(guid); + this.totalInstances.Add(guid); return guid; } else { - return Guid.Empty; + return Guid.Empty; // Busy } case VideoEncoder.VceH264: case VideoEncoder.VceH265: - if (this.vceInstances.Count < 1) + if (this.vceInstances.Count < this.totalVceInstances && this.TotalActiveInstances <= this.maxAllowedInstances) { Guid guid = Guid.NewGuid(); this.vceInstances.Add(guid); + this.totalInstances.Add(guid); return guid; } else { - return Guid.Empty; + return Guid.Empty; // Busy + } + + default: + if (this.TotalActiveInstances <= this.maxAllowedInstances) + { + Guid guid = Guid.NewGuid(); + this.totalInstances.Add(guid); + return guid; + } + else + { + return Guid.Empty; // Busy } } } - - return null; } - public void UnlockHardware(VideoEncoder encoder, Guid? unlockKey) + public void ReleaseToken(VideoEncoder encoder, Guid? unlockKey) { if (unlockKey == null) { @@ -103,6 +149,11 @@ namespace HandBrakeWPF.Services.Queue lock (this.lockOjb) { + if (this.totalInstances.Contains(unlockKey.Value)) + { + this.totalInstances.Remove(unlockKey.Value); + } + switch (encoder) { case VideoEncoder.QuickSync: @@ -135,14 +186,8 @@ namespace HandBrakeWPF.Services.Queue } } - public void AllocateIntelGPU(EncodeTask task) + private void AllocateIntelGPU(EncodeTask task) { - // Validation checks. - if (task.VideoEncoder != VideoEncoder.QuickSync && task.VideoEncoder != VideoEncoder.QuickSyncH265 && task.VideoEncoder != VideoEncoder.QuickSyncH26510b) - { - return; // Not a QSV job. - } - if (this.qsvGpus.Count <= 1) { return; // Not a multi-Intel-GPU system. diff --git a/win/CS/HandBrakeWPF/Services/Queue/QueueService.cs b/win/CS/HandBrakeWPF/Services/Queue/QueueService.cs index 3ccb41044..c3c65f3c3 100644 --- a/win/CS/HandBrakeWPF/Services/Queue/QueueService.cs +++ b/win/CS/HandBrakeWPF/Services/Queue/QueueService.cs @@ -23,7 +23,9 @@ namespace HandBrakeWPF.Services.Queue using HandBrakeWPF.Factories; using HandBrakeWPF.Helpers; using HandBrakeWPF.Properties; + using HandBrakeWPF.Services.Encode; using HandBrakeWPF.Services.Encode.Factories; + using HandBrakeWPF.Services.Encode.Interfaces; using HandBrakeWPF.Services.Encode.Model; using HandBrakeWPF.Services.Interfaces; using HandBrakeWPF.Services.Logging.Interfaces; @@ -58,7 +60,7 @@ namespace HandBrakeWPF.Services.Queue private readonly string queueFile; private readonly object queueFileLock = new object(); - private readonly QueueResourceService hardwareEncoderResourceManager; + private readonly QueueResourceService hardwareResourceManager; private int allowedInstances; private int jobIdCounter = 0; @@ -69,7 +71,7 @@ namespace HandBrakeWPF.Services.Queue public QueueService(IUserSettingService userSettingService, ILog logService, IErrorService errorService, ILogInstanceManager logInstanceManager, IPortService portService) { this.userSettingService = userSettingService; - this.hardwareEncoderResourceManager = new QueueResourceService(userSettingService); + this.hardwareResourceManager = new QueueResourceService(userSettingService); this.logService = logService; this.errorService = errorService; this.logInstanceManager = logInstanceManager; @@ -83,7 +85,7 @@ namespace HandBrakeWPF.Services.Queue this.encodeTaskFactory = new EncodeTaskFactory(this.userSettingService); - this.hardwareEncoderResourceManager.Init(); + this.hardwareResourceManager.Init(); } public event EventHandler<QueueProgressEventArgs> JobProcessingStarted; @@ -354,7 +356,7 @@ namespace HandBrakeWPF.Services.Queue QueueTask task = this.queue.FirstOrDefault(q => q.Status == QueueItemStatus.Waiting); if (task != null) { - task.HardwareResourceToken = this.hardwareEncoderResourceManager.GetHardwareLock(task.Task); + task.TaskToken = this.hardwareResourceManager.GetToken(task.Task); return task; } } @@ -606,7 +608,7 @@ namespace HandBrakeWPF.Services.Queue if (job != null) { // Hardware encoders can typically only have 1 or two instances running at any given time. As such, we must have a HardwareResourceToken to continue. - if (job.HardwareResourceToken == Guid.Empty) + if (job.TaskToken == Guid.Empty) { return; // Hardware is busy, we'll try again later when another job completes. } @@ -617,13 +619,14 @@ namespace HandBrakeWPF.Services.Queue } this.jobIdCounter = this.jobIdCounter + 1; - ActiveJob activeJob = new ActiveJob(job, this.userSettingService, this.logInstanceManager, this.jobIdCounter, this.portService); + IEncode libEncode = new LibEncode(this.userSettingService, this.logInstanceManager, this.jobIdCounter, this.portService); + ActiveJob activeJob = new ActiveJob(job, libEncode); activeJob.JobFinished += this.ActiveJob_JobFinished; activeJob.JobStatusUpdated += this.ActiveJob_JobStatusUpdated; this.activeJobs.Add(activeJob); activeJob.Start(); - + this.IsProcessing = true; this.InvokeQueueChanged(EventArgs.Empty); this.InvokeJobProcessingStarted(new QueueProgressEventArgs(job)); @@ -650,7 +653,7 @@ namespace HandBrakeWPF.Services.Queue private void ActiveJob_JobFinished(object sender, ActiveJobCompletedEventArgs e) { - this.hardwareEncoderResourceManager.UnlockHardware(e.Job.Job.Task.VideoEncoder, e.Job.Job.HardwareResourceToken); + this.hardwareResourceManager.ReleaseToken(e.Job.Job.Task.VideoEncoder, e.Job.Job.TaskToken); this.activeJobs.Remove(e.Job); this.OnEncodeCompleted(e.EncodeEventArgs); |