EncoderPackage bundles a set of classes and structures for the core encoder loop of EncoderController
        /// <summary>
        /// Initialise an encoder job based on previously setup capture devices.
        /// Need to have one job per 'ReductionFactor' in the config.
        /// </summary>
        private void EncoderSetup()
        {
            var factors = config.EncoderSettings.ReductionFactors;

            Packages = new List <EncoderPackage>();

            int fps = (cam != null) ? (cam.m_frameRate) : (config.Video.InputFrameRate);

            var needed_packages = ListRequiredPackages();

            int pkg_id = 0;

            foreach (var np in needed_packages)
            {
                EncoderJob job = new EncoderJob();
                job.OldSegmentNumber = 1;
                string joined = Path.Combine(config.EncoderSettings.LocalSystemOutputFolder, config.EncoderSettings.LocalSystemFilePrefix);

                joined += "_" + pkg_id;
                int bitrate = (int)(config.EncoderSettings.VideoBitrate * np.Quality);                 // linear scale

                int error = EncoderBridge.InitialiseEncoderJob(
                    ref job,                                                                            // job to complete
                    np.VideoSize.Width,                                                                 // OUTPUT video width
                    np.VideoSize.Height,                                                                // OUTPUT video height
                    joined,                                                                             // OUTPUT folder + filename prefix
                    fps,                                                                                // INPUT frame rate (output will match)
                    bitrate,                                                                            // OUTPUT video bit rate
                    config.EncoderSettings.FragmentSeconds);                                            // Fragment length (seconds)

                if (error != 0)
                {
                    throw new Exception("Encoder setup error #" + error);
                }
                if (!job.IsValid)
                {
                    throw new Exception("Job rejected");
                }

                var mf = new MediaFrame();
                mf.ForceAudioConsumption = (np.HasVideo) ? ((byte)0) : ((byte)1);                 // don't sync if no video.

                var pkg = new EncoderPackage(np, pkg_id, job, mf);

                ConnectPackageToBuffers(pkg, np);

                Packages.Add(pkg); pkg_id++;
            }
        }
        /// <summary>
        /// Create, start and return a new core encoder thread.
        /// </summary>
        private Thread NewEncoderThread(EncoderPackage pkg)
        {
            ParameterizedThreadStart encodeLoop = new ParameterizedThreadStart(EncoderCoreLoop);

            var encloop = new Thread(encodeLoop);

            encloop.IsBackground = true;             // When the parent thread ends, the encoder thread will be aborted.

            encloop.SetApartmentState(ApartmentState.MTA);

            // Don't use 'Highest' priority, as that can cause issues with the capture drivers (which will be working hard too)
            encloop.Priority = ThreadPriority.AboveNormal;
            encloop.Start(pkg);             // start with the job id
            return(encloop);
        }
        /// <summary>
        /// Create encoder buffers for each package, and add to capture buffer lists.
        /// </summary>
        private void ConnectPackageToBuffers(EncoderPackage Package, PackageSpec Spec)
        {
            if (Spec.HasVideo && ImageBuffers != null)
            {
                var vbuf = new ImageBuffer(Spec.VideoSize.Width, Spec.VideoSize.Height);
                ImageBuffers.Add(vbuf);
                Package.Buffers.Add(vbuf);
            }

            if (Spec.HasAudio && AudioBuffers != null)
            {
                AudioBufferMono abuf = null;
                if (mic != null)
                {
                    abuf = new AudioBufferMono(mic.SampleRate, mic.Channels);
                }
                else
                {
                    abuf = new AudioBufferMono(config.Audio.SampleRate, config.Audio.Channels);
                }
                AudioBuffers.Add(abuf);
                Package.Buffers.Add(abuf);
            }
        }
        /// <summary>
        /// This loop does the actual work of reading caches into the encoder and firing actions.
        /// This loop controls Multiple-bit-rate encoding
        /// </summary>
        private void EncoderCoreLoop(object Package)
        {
            EncoderPackage pkg = Package as EncoderPackage;

            try {
                #region Start up
                if (pkg == null)
                {
                    throw new Exception("Encoder core loop package was lost");
                }
                double lastVideoTime = 0.0;                 // used for Adaptive frame rate

                if (cam != null)
                {
                    lastVideoTime = cam.TimecodeStart;
                }

                int loop_frame_incr = 0;
                if (pkg.Specification.HasVideo)
                {
                    loop_frame_incr = 1;
                    videoJobs++;
                }
                #endregion

                while (!EncoderRunning)                   // wait for the signal!
                {
                    System.Threading.Thread.Sleep(1000);
                }

                WaitForSyncFlag();

                start = DateTime.Now;
                while (EncoderRunning)                   // Encode frames until stopped
                {
                    #region Frame availability checks
                    // Wait for buffers to be populated
                    while (pkg.BuffersEmpty(MinimumBufferPopulation) && EncoderRunning)
                    {
                        foreach (var buf in pkg.Buffers)
                        {
                            buf.RebufferCapturedFrames();
                        }
                        if (pkg.BuffersEmpty(MinimumBufferPopulation))
                        {
                            System.Threading.Thread.Sleep(frameSleep);
                        }
                    }

                    if (DryRun)
                    {
                        System.Threading.Thread.Sleep(frameSleep);
                        continue;                         // don't encode
                    }
                    #endregion

                    pkg.LoadAllBuffers();
                    EncoderBridge.EncodeFrame(ref pkg.Job, ref pkg.Frame);
                    pkg.UnloadAllBuffers();

                    if (!pkg.Job.IsValid)
                    {
                        throw new Exception("Job became invalid. Possible memory or filesystem error");
                    }

                    #region Segment switching
                    if (pkg.Job.SegmentNumber != pkg.Job.OldSegmentNumber)
                    {
                        double real_chunk_duration = pkg.Frame.VideoSampleTime - lastVideoTime;
                        lock (outputRouter) {
                            outputRouter.NewChunkAvailable(pkg.Job.OldSegmentNumber, pkg.JobIndex, real_chunk_duration);
                        }
                        lastVideoTime            = pkg.Frame.VideoSampleTime;
                        pkg.Job.OldSegmentNumber = pkg.Job.SegmentNumber;
                    }

                    FrameCount += loop_frame_incr;
                    #endregion
                }
            } catch (Exception ex) {
                System.Diagnostics.Debug.Fail("EncoderController.cs: Core loop fault.", ex.Message + "\r\n" + ex.StackTrace);

                File.WriteAllText(config.EncoderSettings.LocalSystemOutputFolder + @"/error.txt", "Main loop: " + ex.Message + "\r\n" + ex.StackTrace);
            } finally {
                if (pkg != null)
                {
                    EncoderBridge.CloseEncoderJob(ref pkg.Job);                    // NEVER FORGET THIS!!
                }
                if (EncoderRunning)
                {
                    Halt();                                 // Don't use 'Stop()' in the core loop, or the system will freeze!
                }
                System.Threading.Thread.CurrentThread.Abort();
            }
        }
        /// <summary>
        /// Create, start and return a new core encoder thread.
        /// </summary>
        private Thread NewEncoderThread(EncoderPackage pkg)
        {
            ParameterizedThreadStart encodeLoop = new ParameterizedThreadStart(EncoderCoreLoop);

            var encloop = new Thread(encodeLoop);
            encloop.IsBackground = true; // When the parent thread ends, the encoder thread will be aborted.

            encloop.SetApartmentState(ApartmentState.MTA);

            // Don't use 'Highest' priority, as that can cause issues with the capture drivers (which will be working hard too)
            encloop.Priority = ThreadPriority.AboveNormal;
            encloop.Start(pkg); // start with the job id
            return encloop;
        }
        /// <summary>
        /// Initialise an encoder job based on previously setup capture devices.
        /// Need to have one job per 'ReductionFactor' in the config.
        /// </summary>
        private void EncoderSetup()
        {
            var factors = config.EncoderSettings.ReductionFactors;

            Packages = new List<EncoderPackage>();

            int fps = (cam != null) ? (cam.m_frameRate) : (config.Video.InputFrameRate);

            var needed_packages = ListRequiredPackages();

            int pkg_id = 0;
            foreach (var np in needed_packages) {
                EncoderJob job = new EncoderJob();
                job.OldSegmentNumber = 1;
                string joined = Path.Combine(config.EncoderSettings.LocalSystemOutputFolder, config.EncoderSettings.LocalSystemFilePrefix);

                joined += "_" + pkg_id;
                int bitrate = (int)(config.EncoderSettings.VideoBitrate * np.Quality); // linear scale

                int error = EncoderBridge.InitialiseEncoderJob(
                    ref job,											// job to complete
                    np.VideoSize.Width,									// OUTPUT video width
                    np.VideoSize.Height,								// OUTPUT video height
                    joined,												// OUTPUT folder + filename prefix
                    fps,												// INPUT frame rate (output will match)
                    bitrate,											// OUTPUT video bit rate
                    config.EncoderSettings.FragmentSeconds);			// Fragment length (seconds)

                if (error != 0) throw new Exception("Encoder setup error #" + error);
                if (!job.IsValid) throw new Exception("Job rejected");

                var mf = new MediaFrame();
                mf.ForceAudioConsumption = (np.HasVideo) ? ((byte)0) : ((byte)1); // don't sync if no video.

                var pkg = new EncoderPackage(np, pkg_id, job, mf);

                ConnectPackageToBuffers(pkg, np);

                Packages.Add(pkg); pkg_id++;
            }
        }
        /// <summary>
        /// Create encoder buffers for each package, and add to capture buffer lists.
        /// </summary>
        private void ConnectPackageToBuffers(EncoderPackage Package, PackageSpec Spec)
        {
            if (Spec.HasVideo && ImageBuffers != null) {
                var vbuf = new ImageBuffer(Spec.VideoSize.Width, Spec.VideoSize.Height);
                ImageBuffers.Add(vbuf);
                Package.Buffers.Add(vbuf);
            }

            if (Spec.HasAudio && AudioBuffers != null) {
                AudioBufferMono abuf = null;
                if (mic != null) abuf = new AudioBufferMono(mic.SampleRate, mic.Channels);
                else abuf = new AudioBufferMono(config.Audio.SampleRate, config.Audio.Channels);
                AudioBuffers.Add(abuf);
                Package.Buffers.Add(abuf);
            }
        }