Frame Pool Blanking Effect - A Custom UWP Video Effect

Frame Pool Blanking will blacken any pixel that changes over a set time. It is meant for reading static text in an environment where non-fixed imagery interferes with the reader's coherence. One uses two computing machines. The user browses on one, with the video coming from that playing on the other through a HDMI-USB converter.  

When someone goes to a newspaper page, everything is black at first, but then all the non-moving things come out. Scrolling will also smear. But what eventually shows up is non-moving content.

Mousing can be ... tricky.  

Here is a picture of my program in operation. One can see that the video frame cnn was running, located under my menu, is totally blacked out. 

Guess I am not watching. Heh, heh.

There are a more settings than usual because I needed flexibility. At thirty fps, a frame pool of twenty frames is two-thirds of a second. I can extend that time period without creating a gigantic frame pool by using the property "PoolLag." The frame pool takes (poolLag + 1) cycles to update any pool frame. 

As a result the sampling back frame is static for lag cycles as well. This does not seem to effect operation much. Syncing can be a problem. The frame pool size might be the same time length as some underlying flickering, for instance. 

The property secondReadBack allows for a second frame to be added to the comparison. The number represented by secondReadBack is the number of frames backward from the currently read frame. In our examples here that is set to be three frames back. I picked something close to the current frame, as to have different-sized time spaces sampled, and also a non-factor fraction of  poolSize as to not coincide with each pool cycle's potential fractional visual harmonics. 

A non zero value for secondReadBack will trigger this two-frame comparison routine, otherwise only the furthest back frame will be compared to the current one. The limits of secondReadBack are zero to framePoolSize-2. 

Both PoolLag and SecondReadBack can be zero. PoolSize should be at least two, or the effect will not operate. 

Another example, this time gifs: 


As long as Firefox works right with my settings, I don't need this any more. But It came in handy. 

I first started researching this when Edge replaced IE. The only reason I used Internet Explorer in the first place was the escape key killed gifs. They got rid of that in Edge, because they are geniuses. 

Anyhow, here is the code. 

   public sealed class FramePoolBlanking : IBasicVideoEffect
    {
        private int frameCount, poolLagCount;
        private bool skipWrite;
        private int bmpWidth, bmpHeight;
        private IntPtr poolMemoryPointer;
        private int byteCount;
        private int secondReadBack;
        private int poolSize, poolLag;

        public bool TimeIndependent { get { return true; } }

        public MediaMemoryTypes SupportedMemoryTypes { get { return MediaMemoryTypes.Gpu; } }

        public bool IsReadOnly { get { return false; } }

        private CanvasDevice canvasDevice;

        public IReadOnlyList<VideoEncodingProperties> SupportedEncodingProperties
        {
            get
            {
                var encodingProperties = new VideoEncodingProperties();
                encodingProperties.Subtype = "ARGB32";
                return new List<VideoEncodingProperties>() { encodingProperties };
            }
        }

        public void SetEncodingProperties(VideoEncodingProperties encodingProperties, IDirect3DDevice device)
        {
            canvasDevice = CanvasDevice.CreateFromDirect3D11Device(device);
        }

        public void SetProperties(IPropertySet configuration)
        {
            object brb;
            if (configuration.TryGetValue("SkipWrite", out brb))
            {
                skipWrite = (bool)brb;
            }
            else
            {
                skipWrite = false;
            }
            object brb4;
            if (configuration.TryGetValue("PoolSize", out brb4))
            {
                poolSize = (int)brb4;
            }
            else
            {
                poolSize = 0;
            }
            if (configuration.TryGetValue("PoolLag", out brb4))
            {
                poolLag = (int)brb4;
            }
            else
            {
                poolLag = 0;
            }
            if (configuration.TryGetValue("SecondReadBack", out brb4))
            {
                secondReadBack = (int)brb4;
            }
            else
            {
                secondReadBack = 0;
            }

            if (poolSize == 0)
            {
                skipWrite = true;
            }
        }

        public void DiscardQueuedFrames()
        {
            frameCount = 0;
        }

        public void Close(MediaEffectClosedReason reason)
        {
            if (reason == MediaEffectClosedReason.EffectCurrentlyUnloaded)
            {
                if (poolMemoryPointer != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(poolMemoryPointer);
                    poolMemoryPointer = IntPtr.Zero;
                }
            }
        }

        public void ProcessFrame(ProcessVideoFrameContext context)
        {
            long lo = DateTime.Now.Ticks;
            if (skipWrite)
            {
                using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromDirect3D11Surface(canvasDevice, context.InputFrame.Direct3DSurface))
                using (CanvasRenderTarget renderTarget = CanvasRenderTarget.CreateFromDirect3D11Surface(canvasDevice, context.OutputFrame.Direct3DSurface))
                {
                    renderTarget.CopyPixelsFromBitmap(inputBitmap);
                }
                Debug.WriteLine("skip " + (DateTime.Now.Ticks - lo) / 10000);
                return;
            }
            using (CanvasBitmap inputBitmap = CanvasBitmap.CreateFromDirect3D11Surface(canvasDevice, context.InputFrame.Direct3DSurface))
            using (CanvasRenderTarget renderTarget = CanvasRenderTarget.CreateFromDirect3D11Surface(canvasDevice, context.OutputFrame.Direct3DSurface))
            {
                if ((bmpWidth != inputBitmap.SizeInPixels.Width) | (bmpHeight != inputBitmap.SizeInPixels.Height))
                {
                    if (poolMemoryPointer != IntPtr.Zero)
                    {
                        Marshal.FreeHGlobal(poolMemoryPointer);
                        poolMemoryPointer = IntPtr.Zero;
                    }
                    bmpWidth = (int)inputBitmap.SizeInPixels.Width;
                    bmpHeight = (int)inputBitmap.SizeInPixels.Height;
                    byteCount = bmpWidth * bmpHeight * 4;
                    poolMemoryPointer = Marshal.AllocHGlobal(byteCount * poolSize);
                }
                byte[] bt6 = { 0, 0, 0, 255, 0, 0, 0, 255 };
                byte[] bt2 = inputBitmap.GetPixelBytes();
                int poolWriteNum = frameCount % poolSize;
                int ofs1 = poolWriteNum * byteCount;
                IntPtr fp1 = IntPtr.Add(poolMemoryPointer, ofs1);
                Marshal.Copy(bt2, 0, fp1, bt2.Length); //copy .......
                int poolReadNum1 = (frameCount + poolSize + 1) % poolSize;
                int ofs2 = poolReadNum1 * byteCount;
                IntPtr fp2 = IntPtr.Add(poolMemoryPointer, ofs2);
                if (secondReadBack != 0)
                {
                    int poolReadNum2 = (frameCount + poolSize - secondReadBack) % poolSize;
                    int ofs3 = poolReadNum2 * byteCount;
                    IntPtr fp3 = IntPtr.Add(poolMemoryPointer, ofs3);
                    for (int lt1 = 0; lt1 < byteCount; lt1 += 8)
                    {
                        if (Marshal.ReadInt32(fp1, lt1) != Marshal.ReadInt32(fp2, lt1))
                        {
                            bt6.CopyTo(bt2, lt1);
                        }
                        else if (Marshal.ReadInt32(fp1, lt1) != Marshal.ReadInt32(fp3, lt1))
                        {
                            bt6.CopyTo(bt2, lt1);
                        }
                    }
                }
                else
                {
                    for (int lt1 = 0; lt1 < byteCount; lt1 += 8)
                    {
                        if (Marshal.ReadInt32(fp1, lt1) != Marshal.ReadInt32(fp2, lt1))
                        {
                            bt6.CopyTo(bt2, lt1);
                        }
                    }
                }
                renderTarget.SetPixelBytes(bt2);
                if (poolLagCount == 0)
                {
                    frameCount++;
                    poolLagCount = poolLag;
                }
                else
                {
                    poolLagCount--;
                }
            }
            Debug.WriteLine((DateTime.Now.Ticks - lo) / 10000);
        }
    }  


Note about code listing. When copied the right arrow (<) and left arrow (>) may not show up properly, and instead be represented by the string literals (&lt;) and (&gt;) respectively. Replace those four character literals with the correct arrow in that case. 

Disclaimer: This is free for personal use only. Place of origin, California, US.