[FFmpeg] 解决av_find_input_format找不到v4l2的问题

📅 2024-12-10 16:15 👁️ 67 💬 0

从问题入手
在网上找了段代码测试v4l2,在第一行调用av_find_input_format("v4l2")发现找不到v4l2,用ffmpeg.exe 执行ffmpeg -formats发现有v4l2,所以就从这里开始找为什么找不到v4l2。

先找到av_find_input_format的定义在源码的format.c文件。

const AVInputFormat *av_find_input_format(const char *short_name){ const AVInputFormat *fmt = NULL; void *i = 0; while ((fmt = av_demuxer_iterate(&i)))//遍历 if (av_match_name(short_name, fmt->name))//匹配名称 return fmt; return NULL;}

在libavformat 58 版本后添加的av_demuxer_iterate这个函数。
在allformats.c里找到了av_demuxer_iterate的定义:

const AVInputFormat *av_demuxer_iterate(void **opaque){ static const uintptr_t size = sizeof(demuxer_list)/sizeof(demuxer_list[0]) - 1; uintptr_t i = (uintptr_t)*opaque; const AVInputFormat *f = NULL; uintptr_t tmp; if (i < size) { f = demuxer_list[i]; } else if (tmp = atomic_load_explicit(&indev_list_intptr, memory_order_relaxed)) { const AVInputFormat *const *indev_list = (const AVInputFormat *const *)tmp; f = indev_list[i - size]; } if (f) *opaque = (void*)(i + 1); return f;}

这里是在迭代AVInputFormat demuxer_list,而迭代的所有的dmuxer包含在allformats.c。

/* (de)muxers */extern const FFOutputFormat ff_a64_muxer;extern const AVInputFormat ff_aa_demuxer;extern const AVInputFormat ff_aac_demuxer;extern const AVInputFormat ff_aax_demuxer;extern const AVInputFormat ff_ac3_demuxer;extern const FFOutputFormat ff_ac3_muxer;extern const AVInputFormat ff_ac4_demuxer;extern const FFOutputFormat ff_ac4_muxer;extern const AVInputFormat ff_ace_demuxer;extern const AVInputFormat ff_acm_demuxer;....

翻完了都没找到v4l2。
在libavdevice/alldevices.c里找到了v4l2 的fmt。难怪找不到。
extern const AVInputFormat ff_v4l2_demuxer;

所以正确的用法是,先调用
avdevice_register_all()

阅读更多

[C#]WPF 分辨率的无关性的问题

📅 2023-07-27 10:20 👁️ 1251 💬 2

什么是WPF的分辨率无关性?

首先得解什么是Dpi(Density independent pixels ,设备无关像素),百度百科的解释DPI是指每英寸的像素,对应界面显示即是屏幕上每英寸的像素。

如标准的Windows DPI(96Dpi),代表1英寸96个像素。

假设有一个96px*96px的按钮,如果在标准标准的Windows DPI的情况下看起来就是英寸的大小,如果在其他Dpi的情况下假设为192Dpi,实际只需要将原来的96px*96px按钮像素大小翻倍即可。

而WPF就是通过改变像素值来实现分辨率无关,具体公式如下:

[物理单位尺寸]=[设备无关单位尺寸]×[系统DPI]

其中设备无关单位尺寸值和Windows DPI的尺寸是一样的。上面那个例子变成公式就是

1/96英寸 * 192Dpi  = 2px;而真实尺寸 = 设置的像素值 * 物理单位尺寸 即 96px*2px = 192px;

 

WPF分辨率无关的问题?

提一下我遇到问题的情况,我写了一个在图片上定位然后显示框的功能,然后那个框不是图片上的,而是一个组件,需要的是将框和定位点和长度传递给显示框。

图片很大,图片要适应程序的宽高,所以有个缩放值。然后图片长宽除缩放值的即为渲染到界面图片的大小。然后我想着计算出来框的定位点和长度,同理除缩放值就可以。

结果在我的电脑上运行正常,然后在其他电脑上运行,定位点和框大小都变大了。

最后测试是屏幕的缩放不同导致的。

对于图片显示在缩放后,WPF的真实尺寸是设置的像素值 * 物理单位尺寸 。而给定的自定义控件属性是并不会乘物理单位尺寸,也就是缩放情况下是显示是正常的。导致实际像素得到了缩放。

解决方法其实很简单,在将框和定位点和长度除缩放值再除屏幕缩放值就可得到真实值。

而WPF中屏幕缩放值最简单的获取方法:

     double screenscale = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth;//windows
     double screenscale1 = NSScreen.MainScreen.Frame.Width/ SystemParameters.PrimaryScreenWidth;//MacOs

后面发现  其实WPF正是因为分辨率无关性,导致你将屏幕比例缩放后,各个控件都被放大。

对于一些需求是,无论这么缩放,软件的实际渲染大小不应该改变。

例如先在1080p分辨率下创建一个WPF程序

界面设置为全屏化,只有两个按钮一个按钮高1000,一个按钮高80。

运行结果如下

然后改变缩放为125%

再运行结果如下

可以看到小按钮被挤出屏幕了。同理,我遍历Button,然后将Button的大小除于缩放比例

       private void StackPanel_Loaded(object sender, RoutedEventArgs e)
        {
            double screenscale = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth;
            if (sender is StackPanel panel)
            {
                foreach (Button button in panel.Children)
                {
                    button.Width /= screenscale;
                    button.Height /= screenscale;
                }
            }
        }

最后样式和缩放为100%一致。

 

 

如有问题,欢迎批评指正。

                                  

阅读更多

[C#] FFmpeg 音视频开发总结

📅 2023-06-08 10:33 👁️ 2336 💬 8

为什么选择FFmpeg?

  1. 延迟低,参数可控,相关函数方便查询,是选择FFmpeg作为编解码器最主要原因,如果是处理实时流,要求低延迟,最好选择是FFmpeg。
  2. 如果需要用Opencv或者C#的Emgucv这种库来处理视频流,也多是用FFmpeg做编解码然后再转换图像数据给Opencv去处理。用Opencv编解码延迟很高。
  3. 其他的库多是基于FFmpeg封装,如果做一个视频播放器,像vlc这种库是非常方便的,缺点是臃肿,需要手动剔除一些文件,当然也有一些是基于FFmpeg封装好的视频播放器库,也能快速实现一个播放器。
  4. 如果是加载单Usb接口中的多Usb摄像头,FFmpeg这时就无能为力了,经过测试使用DirectShow能够实现。AForge一个很好的学习简单样例,它将DirectShow封装。能轻松实现加载单Usb接口中的多Usb摄像头(不过它很久没更新了,目前无法设置摄像头参数,也没有Usb摄像头直接录制,所以我把它重写了),还有WPF-Mediakit这个库,实现使用Directshow封装的更好(功能和结构也更复杂)。经过学习和代码编写,DirectShow录制视频能力有限,还是推荐使用DirectShow显示Media Foundation录制。
  5. 写此文章时才发现CaptureManager这个2023年4月发布的非常简便好用的基于D3D封装的音视频库,它的官方样例非常丰富,能实现很多功能。我尝试了运行了他的官方样例,打开相同规格的Usb摄像头,发觉cpu占用是FFmpeg的两倍。

如何学习FFmpeg?

记录一下我是如何学习FFmpeg。首先是C#使用FFmpeg基本上用的是FFmpeg.autogen这个库。也可以使用FFmpeg.exe,先不谈论FFmpeg.exe的大小,我尝试过从exe中取数据到C#前端显示,相同参数情况下,延迟比使用FFmpeg.autogen高,主要是不能边播放边录制(可以用其它的库来录制,但是效率比不上只使用一个库)。

当然如果只需要部分功能也可以自己封装FFmpeg(太花时间了,我放弃了。如果是专门从事这一行的可以试试)。

学习FFmpeg.autogen可以先去Github上下载它的样例(其实样例有个小问题,后面说),学习基础的编解码。

后面有人把官网的C++的样例用FFmpeg.autogen写了一遍,我把样例压缩好放夸克网盘了:https://pan.quark.cn/s/c579aad1d8e0

然后是查看一些博客和Github上一些项目,了解编解码整体架构,因为FFmpeg很多参考代码都是c++的所以我基本是参考C++写C#,写出整体的编解码代码。

无论是编解码还是开发Fliter都会涉及到很多参数设置。要查找这些参数,我先是去翻博客,最后还是去FFmpeg官网(官网文档,编解码参数很全),当然制作视频滤镜和一些其他功能,也是参考官网的参数。

对于部分基础函数(有些函数会把帧用掉就释放,要注意)查看FFmpeg的源码,理解原理。

对于一些概念性的东西,我是翻阅硕博论文(一般都有总结这些)。

 C#使用FFmpeg需要注意什么?

  1. FFmpeg.autogen是有一个缺点的,它是全静态的,不支持多线程(这个我问作者了),所以用多进程,而用多进程渲染到同一画面,可以参考我上一篇MAF的文章。
  2. 尤其要注意帧释放,编解码的帧如果没有释放是一定会产生内存泄漏的,而且速度很快。
  3. 其次是c# 要将图像数据渲染到界面显示,最最好使用WriteableBitmap,将WriteableBitmap和绑定到一个Image然后更新WriteableBitmap。我记得在一篇博客中提到高性能渲染,使用MoveMemory来填充WriteableBitmap的BackBuffer,核心代码如下。
    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
        private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);
     writeableBitmap.Lock();
            unsafe
            {
                 fixed (byte* ptr = intPtr)
                  {
                    MoveMemory(writeableBitmap.BackBuffer, new IntPtr(ptr), (uint)intPtr.Length);
                   }
           }
           writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
    
    writeableBitmap.Unlock();
    这样处理有个致命的缺点。WriteableBitamp的宽高必须为2的整数倍,即使是修正过大小,当传入数据为特殊尺寸使用此方法时还是会出现显示异常的情况。所以还是老实使用WriteableBitmap的WritePixels。
  4. 对于FFmpeg很多函数都是会返回错误信息,一定要将错误信息记录到日志,方便查找和查看(基本每个函数要加错误信息判断)。
    一般的错误返回的是一个int类型的负值,可以使用下面函数转为string:
    public static string ErrorToString(int tag)
            {
     tag = Math.Abs(tag);
     if (tag < 100) return tag.ToString();
    return Convert.ToChar((byte)(tag)).ToString() + Convert.ToChar((byte)(tag >> 8)) + Convert.ToChar((byte)(tag >> 16)) + Convert.ToChar((byte)(tag >> 24));
    }
  5. 软编码会占用大量的CPU资源,所以最好采用硬编码。FFmpeg有一个查找编解码器的函数,它并不能查看硬件编码器。如果要使用硬件加速查找编解码器最好是用其他方式获取系统设备或者直接一个一个打开NVDIA和QSV等加速,都失败了再启用软编解码。
  6. QSV硬编码要求输入的像素格式必须为AVPixelFormat.AV_PIX_FMT_NV12,如果是硬解码出的数据,可以直接编码,否则需要添加格式转换。FFmepg.autogen的官方样例中有格式转换函数,但由于它没有指定转换后的格式会出问题(踩坑)。
  7. 尽量少的格式转换,或者帧复制。这两种方式会提高cpu和内存使用率同时也会有更高的延迟。
  8. 在制作FFmpeg的带有文本的Filter时,将需要使用的字体复制到项目目录然后指定字体位置而不是调用系统的字体(不知道是版本原因还是什么问题,一用系统字体就会产生内存泄漏)。
  9.  注意编解码数据的格式。一些老的格式,虽然解码没有什么问题(ffmpeg 会有提示)但是编码是不支持的,出现这种问题,程序会直接死掉(踩坑)。
  10. 解码时可以通过解码数据自动搜寻硬件解码器,而硬件编码需要手动指定编码器(可以通过,查找并自动选择GPU来实现自动选择)。
  11. 多线程实现播放同时录制时,最好采用帧复制ffmpeg.av_frame_clone(hwframe)不用对同一个帧进行操作。当然也可以不用多线程,同一个帧在播放完成后进行,录制。
  12. ffmpeg查找编解码器的函数,找到了编码器并不是物理存在的,而已支持的编码器(比如我一个inter核显的本子也能找到cuda的编码器),所以要确认是否能够打开,打不开就继续找。
    可以通过循环查找硬件配置
      AVCodecHWConfig* config = ffmpeg.avcodec_get_hw_config(codec, i);

    然后是检查是否能够打开硬件编码器

    int error = ffmpeg.av_hwdevice_ctx_create(&_avcodecCtx->hw_device_ctx, deviceType, null, null, 0);
    if(error!=0){
    //硬件设备打开失败
    }

    稳妥起见还是通过查找硬件后来指定,硬件编码器

      private void ConfigureHWDecoder(out AVHWDeviceType type,out AVPixelFormat aVPixel)
            {
                type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
                aVPixel = AVPixelFormat.AV_PIX_FMT_NONE;
                if (!Hw) return;
                switch (Gputype)
                {
                    case 0:
                        type = AVHWDeviceType.AV_HWDEVICE_TYPE_CUDA;
                        aVPixel = AVPixelFormat.AV_PIX_FMT_CUDA;
                        return;
                    case 1:
                        type = AVHWDeviceType.AV_HWDEVICE_TYPE_QSV;
                        aVPixel = AVPixelFormat.AV_PIX_FMT_QSV;
                        return;
                    case 2:
                        type = AVHWDeviceType.AV_HWDEVICE_TYPE_OPENCL;
                        aVPixel = AVPixelFormat.AV_PIX_FMT_OPENCL;
                        return;
                }
            }

     

  13. 对于H264或者H265这种网络流,接收到错误的配置信息错误的设置会导致获取的 FPS是实际两倍,直接录制就会出现录制时长为真实时长一半的问题(踩坑)

  14. 对于解码或者编码时会创建和使用AVPacket和AVFrame。因为音视频一般是连续的,一般只初始化然后重复使用例如:
    AVFrame* pDecodedFrame = ffmpeg.av_frame_alloc();
    AVPacket* pPacket = ffmpeg.av_packet_alloc();
    while(!CloseCommand){
      error = ffmpeg.av_read_frame(formatContext, pPacket);//读取frame
       ....
    }    

    这是解码,当然编码也是同理。
    原因是在C#上连续多次重复创建unsafe对象,即使主动释放有GC回收,还是会慢慢泄漏(跑俩小时解码一个视频,内存泄漏了50m,短期看不出来)

  15. 录制音视频开始的前几帧可能会出现dts错误(坏帧) Application provided invalid, non monotonically increasing dts to muxer in stream x: -xxx>= xxx,所以要在 ffmpeg.av_interleaved_write_frame(写入帧)之前对前几帧进行检查。
    #region 帧检查
            int checkcount = 10;//检查前10帧
            long m_last_video_dts =ffmpeg. AV_NOPTS_VALUE;  
            long m_last_audio_dts = ffmpeg.AV_NOPTS_VALUE;  
            long m_video_interframe_dts = ffmpeg.AV_NOPTS_VALUE;
            bool Check_packet(AVPacket* pkt)
            {
                AVStream* in_stream = _formatContext->streams[pkt->stream_index];
                if (in_stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
                {
                    if (m_last_video_dts == ffmpeg.AV_NOPTS_VALUE)
                    {
                        if (pkt->dts != 0)
                        {
                            pkt->pts = pkt->dts = m_last_video_dts = 0;
                        }
                    }
    
                    if ( m_last_video_dts >= pkt->dts)
                    {
                        if (m_video_interframe_dts == ffmpeg.AV_NOPTS_VALUE)
                        {
                            AVRational r =ffmpeg.av_d2q(in_stream->avg_frame_rate.den, in_stream->avg_frame_rate.num);
                            m_video_interframe_dts = ffmpeg.av_rescale_q_rnd(1, r, in_stream->time_base,
                                          (AVRounding.AV_ROUND_NEAR_INF | AVRounding.AV_ROUND_PASS_MINMAX));
    
                        }
                        pkt->pts = pkt->dts = m_last_video_dts += m_video_interframe_dts;
                    }
                    else
                    {
                        m_last_video_dts = pkt->dts;
                    }
                }
                else if (in_stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
                {
                    if (m_last_audio_dts >= pkt->dts)
                    {
                        ffmpeg.av_packet_unref(pkt);
                        return false;
                    }
                    else
                    {
                        m_last_audio_dts = pkt->dts;
                    }
                }
                else
                {
                   ffmpeg.av_packet_unref(pkt);
                    return false;
                }
                return true;
            }
            #endregion
  16. 显示Usb摄像头时需要设置rtbufsize缓存大小,看其他人设置的1080p视频大小是 622080 ,有时会出现丢帧(因为是软解码)
    dshow @ 000001d76c1ce000] real-time buffer [OBS Virtual Camera] [video input] too full or near too full (500% of size: 622080 [rtbufsize parameter])! frame dropped!
    所以我把rtbufsize大小扩大了1000倍,没有出现丢包情况了
        ffmpeg.av_dict_set_int(&options, "rtbufsize", 622080000, 0);//1080P
  17. 连接rtsp这类流要实现秒连需要设置以下两个参数的值,经过测试连接延迟的最主要的影响是max_analyze_duration(值太小的话会有错误)
                ffmpeg.av_dict_set(&options, "buffer_size", "2048000", 0);
                ffmpeg.av_dict_set(&options, "max_analyze_duration", $"{ffmpeg.AV_TIME_BASE}/2", 0);//ffmpeg.AV_TIME_BASE的倍数

暂时只想到这些,不定时更新     、

如果有任何错误欢迎批评指正。

阅读更多

[C#]插件编程框架 MAF 开发总结

📅 2023-05-26 16:28 👁️ 1897 💬 1

1. 什么是MAF和MEF?

MEF和MEF微软官方介绍:https://learn.microsoft.com/zh-cn/dotnet/framework/mef/

MEF是轻量化的插件框架,MAF是复杂的插件框架。

因为MAF有进程隔离和程序域隔离可选。我需要插件进程隔离同时快速传递数据,最后选择了MAF。

如果不需要真正的物理隔离还是建议使用简单一点的MEF框架。

2.什么情况下会使用MAF框架?

例如想做一个浏览器,每点开一个页面都是一个独立的进程,然后进程之间相互隔离。或者多个不同的进程到同一个界面渲染,实现后台进程和界面之间的隔离。

多进程之间相互传递数据也可以使用MAF。

3. 如何学习MAF?

MAF其实是一项很老的技术,入门我看的是《WPF编程宝典》第32章 插件模型。里面有MAF和MEF的详细介绍和许多样例。

但是要深入理解还是看了很多其他的东西,下面我详细说明,我自己理解和总结的MAF。

4. MAF框架入门

4.1 MAF框架构成与搭建

MAF框架模式是固定的,这里做一个详细介绍。

首先是要添加几个新项目,下图中不包含主项目。

Addin文件夹是放置插件用的,其余都是必要项目。

假设HostView项目和主项目的输出路径是..\Output\

然后修改每个项目的输出文件夹,例如AddInSideAdapter项目输出路径可以设置为..\Output\AddInSideAdapters\

注意插件项目输出到Addin文件夹中的子文件夹是..\..\Output\AddIns\MyAddin\

最后项目的输出文件夹结构是:

D:\Demo\Output\AddIns

D:\Demo\Output\AddInSideAdapters

D:\Demo\Output\AddInViews

D:\Demo\Output\Contracts

D:\Demo\Output\HostSideAdapters

来看看MAF框架模型构成。

 上图中绿色的是被引用蓝色项目所引用。例如HostSideAdapter就要引用Contract和Hostview,如下图所示。

 注意引用时取消勾选复制本地。

 这时就完成基本项目结构的搭建。

4.2 MAF框架实现

这里想实现宿主项目和插件项目的双向通信。即插件项目将相关函数接口在宿主实现,然后将宿主项目相关函数接口用委托类的方式注册给插件项目。实现双向通信。

用《WPF编程宝典》样例代码来说,样例中,插件程序实现ProcessImageBytes处理图像数据的函数,处理同时需要向宿主项目报告处理进度,宿主中 ReportProgress函数实现进度可视化。

MAF实现一般是先写Contract协议,明确需要的函数接口。然后写AddlnView和HostView。实际上这两个是将函数接口抽象化,在接口里函数复制过来前面加 public abstract 就行。

之后HostSideAdapter和AddInSideAdapter直接快速实现接口。

首先从Contract开始,Contract是定义接口,需要设置对象标识符[AddInContract],且必须继承IContract。

 [AddInContract]
    public interface IImageProcessorContract : IContract
    {
        byte[] ProcessImageBytes(byte[] pixels);

        void Initialize(IHostObjectContract hostObj);
    }

    public interface IHostObjectContract : IContract
    {
        void ReportProgress(int progressPercent);
    }

Initialize函数是提供宿主函数注册的接口。

 然后在HostView和AddInView分别定义主程序和插件程序的接口抽象类。

public abstract class ImageProcessorHostView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);

        public abstract void Initialize(HostObject host);
    }

    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }

注意AddlnView需要设置对象标识符[AddInBase]。

 [AddInBase]
    public abstract class ImageProcessorAddInView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);

        public abstract void Initialize(HostObject hostObj);
    }

    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }

之后在HostSideAdapter实现抽象类。

注意HostSideAdapter继承HostView的抽象类,在构造函数里需设置ContractHandle插件生存周期,ContractHandle不能为readonly。

  [HostAdapter]
    public class ImageProcessorContractToViewHostAdapter : HostView.ImageProcessorHostView
    {
        private Contract.IImageProcessorContract contract;
        private ContractHandle contractHandle;

        public ImageProcessorContractToViewHostAdapter(Contract.IImageProcessorContract contract)
        {            
            this.contract = contract;
            contractHandle = new ContractHandle(contract);
        }              

        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            return contract.ProcessImageBytes(pixels);
        }

        public override void Initialize(HostView.HostObject host)
        {            
            HostObjectViewToContractHostAdapter hostAdapter = new HostObjectViewToContractHostAdapter(host);
            contract.Initialize(hostAdapter);
        }
    }

    public class HostObjectViewToContractHostAdapter : ContractBase, Contract.IHostObjectContract
    {
        private HostView.HostObject view;

        public HostObjectViewToContractHostAdapter(HostView.HostObject view)
        {
            this.view = view;
        }

        public void ReportProgress(int progressPercent)
        {
            view.ReportProgress(progressPercent);
        }        
    }

 

在AddInSideAdapter实现Contract接口,基本和HostSideAdapter类似,只是继承的类不同。

[AddInAdapter]
    public class ImageProcessorViewToContractAdapter : ContractBase, Contract.IImageProcessorContract
    {
        private AddInView.ImageProcessorAddInView view;

        public ImageProcessorViewToContractAdapter(AddInView.ImageProcessorAddInView view)
        {
            this.view = view;
        }

        public byte[] ProcessImageBytes(byte[] pixels)
        {
            return view.ProcessImageBytes(pixels);
        }

        public void Initialize(Contract.IHostObjectContract hostObj)
        {            
            view.Initialize(new HostObjectContractToViewAddInAdapter(hostObj));            
        }
    }

    public class HostObjectContractToViewAddInAdapter : AddInView.HostObject
    {
        private Contract.IHostObjectContract contract;
        private ContractHandle handle;

        public HostObjectContractToViewAddInAdapter(Contract.IHostObjectContract contract)
        {
            this.contract = contract;
            this.handle = new ContractHandle(contract);            
        }
                
        public override void ReportProgress(int progressPercent)
        {
            contract.ReportProgress(progressPercent);
        }
    }

 

宿主项目中需要实现HostView里HostObject抽象类。

 private class AutomationHost : HostView.HostObject
        {
            private ProgressBar progressBar;
            public AutomationHost(ProgressBar progressBar)
            {
                this.progressBar = progressBar;
            }
            public override void ReportProgress(int progressPercent)
            {
                // Update the UI on the UI thread.
                progressBar.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                {
                    progressBar.Value = progressPercent;
                }
                );                
            }
        }

 

然后是在宿主项目里激活插件,并初始化AutomationHost。

string path = Environment.CurrentDirectory;            
AddInStore.Update(path);//更新目录中Addins目录里的插件
IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(HostView.ImageProcessorHostView), path);//查找全部插件
lstAddIns.ItemsSource = tokens;//插件可视化
AddInToken token = (AddInToken)lstAddIns.SelectedItem;//选择插件
AddInProcess addInProcess = new AddInProcess();//创建插件进程
addInProcess.Start();//激活插件进程
addin = token.Activate<HostView.ImageProcessorHostView>(addInProcess,AddInSecurityLevel.Internet);//激活插件
//如果只是想隔离程序域,就无需创建AddInProcess,激活插件如下
// HostView.ImageProcessorHostView addin = token.Activate<HostView.ImageProcessorHostView>(AddInSecurityLevel.Host);
automationHost = new AutomationHost(progressBar);//创建AutomationHost类
addin.Initialize(automationHost);//初始化automationHost

 

 插件项目中实现AddInView中的抽象类。

[AddIn("Negative Image Processor", Version = "1.0", Publisher = "Imaginomics",Description = "")]
    public class NegativeImageProcessor : AddInView.ImageProcessorAddInView 
    {
        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            int iteration = pixels.Length / 100;         
            for (int i = 0; i < pixels.Length - 2; i++)
            {
                pixels[i] = (byte)(255 - pixels[i]);
                pixels[i + 1] = (byte)(255 - pixels[i + 1]);
                pixels[i + 2] = (byte)(255 - pixels[i + 2]);
                if (i % iteration == 0)
                {
                    host?.ReportProgress(i / iteration);
                }
            }
            return pixels;
        }

        private AddInView.HostObject host;
        public override void Initialize(AddInView.HostObject hostObj)
        {
            host = hostObj;
        }

 

 这时宿主可以把数据传递给插件程序,插件程序中ProcessImageBytes处理数据然后通过host?.ReportProgress(i / iteration);向宿主传递消息。

这里有提供样例程序。

项目附件.7z

5. MAF框架常见问题

5.1手动关闭插件

AddInController addInController = AddInController.GetAddInController(addIn);
addInController.Shutdown();

此方法适应于非应用隔离的手动关闭。对于应用隔离式插件,用此方法会抛出异常。

如上面样例就是应用隔离的插件,可以根据进程id直接关闭进程。

public void ProcessClose()
        {
            try
            {
                if (process != null)
                {
                    Process processes = Process.GetProcessById(addInProcess.ProcessId);
                    if (processes?.Id > 0)
                    {
                        processes.Close();
                    }
                }
            }
            catch (Exception)
            {
            }5.2 插件异常

System.Runtime.Remoting.RemotingException: 从 IPC 端口读取时失败: 管道已结束。这是插件最常见的异常,因为插件抛出异常而使得插件程序关闭。

如果是插件调用非托管代码,而产生的异常,可以查Windows应用程序日志来确定异常。其余能捕获的异常尽量捕获保存到日志,方便查看。

5.3 双向通信

实际应用过程中,往往是通过委托来将宿主相关函数暴露給一个类,然后通过在宿主程序初始化后。在插件中实例化后就可以直接调用宿主的相关函数,反之同理。

这里是通过委托暴露宿主的一个函数。

public delegate void UpdateCallBack(string message, bool isclose, int leve);
    public class VideoHost : HostAddInView
    {
        public event UpdateCallBack Updatecallback;
        public override void ProcessVideoCallBack(string message, bool isclose, int leve)
        {
            Updatecallback?.Invoke(message, isclose, leve);
        }
    }

 在插件程序中实例化后调用。

private HostAddInView hostAddInView;
        public override void Initialize(HostAddInView hostAddInView)
        {
            this.hostAddInView = hostAddInView;
        }
        private void ErrorCallback(string message, bool isclose, int leve)
        {
            hostAddInView?.ProcessVideoCallBack(message, isclose, leve);
        }

6. MAF深入理解

 MAF本质是实现IpcChannel通信,在一个期刊中有作者抛弃MAF固定结构自己实现IpcChannel,因为代码很复杂,就不在此详细阐述。

如果要实现应用域隔离,自己实现IpcChannel,MAF中的应用域隔离实现也是非常好的参考资料。

MAF的7层结构主要是实现从插件的宿主函数转换,例如可以在将插件程序的界面放入主界面中渲染,做出像浏览器一样的开一个界面就是一个进程。将插件中的组件在AddInSideAdapter中转换为Stream然后在HostSideAdapter中将Stream实例化为组件。而HostView和AddInView实际上是提供两个转换接口,Contract是定义传输接口。

另外如果传输插件向数组传递图像数据,最后是转换成byte[],或者使用共享内存。

如果有什么遗漏和错误,欢迎指正,批评。

阅读更多

WPF 自定义控件 二次渲染 问题记录

📅 2023-04-13 12:07 👁️ 134 💬 0

问题

将多个自定义控件加载到到一个页面的Grid上显示。然后突然将一个控件从Grid里面清除,控件依然在后台处理数据。

过段时间再加入Grid。然后一些已经改变的页面属性就消失了。

原因

经过查找是一旦控件再次加载,页面属性就会重置。这个有利也有弊端。

1是可以利用这点重置页面

2是清除后再显示,如果界面复杂,会造成恢复困难等问题

阅读更多

c# 基于FFmpeg dll的简单录屏

📅 2023-03-14 14:01 👁️ 547 💬 1

基于FFmpeg dll的简单录屏

使用FFMediaToolkit能较为容易的实现录屏或者录制组件等功能。

此功能在windows桌面端运行情况良好,但是移动端会出现问题,因此被弃用,大致记录一下录屏思想。

类库准备

FFmpeg的dll准备,FFmpeg.AutoGen可以生成相关的dll,不过后来不能生成了,就需要自己去下载相应的dll

下载地址:https://github.com/BtbN/FFmpeg-Builds

然后在NGET里搜索并且安装FFMediaToolkit

代码编写

DLL加载

加载的源码 FFMediaToolkit的GitHub主页有,我这稍微改了改

我只用了x64的dll所以把dll文件在dll/FFMediaToolkit。加载方式其实就是FFmpeg.Autgen的加载方式。

   internal static bool LoadFFmpegDll()
        {
            var current = Environment.CurrentDirectory;
            Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");
            if (!Environment.Is64BitProcess)
            {
                Console.WriteLine("x86环境不支持录制");
                return false;
            }
            var probe = Path.Combine("dll", "FFMediaToolkit");//相对路径
            while (current != null)
            {
                var ffmpegBinaryPath = Path.Combine(current, probe);
                if (Directory.Exists(ffmpegBinaryPath))
                {
                    Console.WriteLine($"FFmpeg binaries found in: {ffmpegBinaryPath}");
                    FFmpegLoader.FFmpegPath = ffmpegBinaryPath;
                    return true;
                }
                current = Directory.GetParent(current)?.FullName;
            }
            return false;
        }
View Code

录屏核心代码

复制屏幕像素转为bitmap

         private readonly Rectangle _bounds = System.Windows.Forms.Screen.PrimaryScreen.Bounds;
        private Bitmap GetScreenImgByteArray()
        {
            Bitmap bitmap = new Bitmap(_bounds.Width, _bounds.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            using (Graphics graphics = Graphics.FromImage(bitmap))
            {
                graphics.CopyFromScreen(System.Drawing.Point.Empty, System.Drawing.Point.Empty, _bounds.Size, CopyPixelOperation.SourceCopy);
                return bitmap;
            }
        }
View Code

创建录制路径

  MediaOutput videofile;
 int recordfps = 25;
  private bool CreatRecordPath(string videotemperpath)
{
  VideoEncoderSettings settings = new VideoEncoderSettings(_bounds.Width, _bounds.Height, framerate: recordfps, codec: VideoCodec.H264)
                    {
                        EncoderPreset = EncoderPreset.Fast,
                        CRF = 17
                    };
                    videofile = MediaBuilder.CreateContainer(videotemperpath).WithVideo(settings).Create();
                }

}
View Code

初始化录制时间类

因为正常写定时器录制总是出现,帧率过快的问题。经过一番查找,使用了别人封装的时间间隔类 TimedIntervalTask,来保证录制帧率正常。

 public sealed class TimedIntervalTask
        {
            /// <summary>
            /// 初始化 时间间隔模式的定时任务类;须给定一些必要参数
            /// </summary>
            /// <param name="action">
            /// 定时任务的方法行为,不可为 null;此方法里须加入 try {} catch {} 异常处理,以保证其稳定运行
            /// </param>
            /// <param name="intervalTime">定时任务的间隔时间,单位:毫秒</param>
            /// <param name="whetherToRunFirst">
            /// 是否先运行行为;默认为 true,先运行行为,再等待时间间隔;若设为 false,则先等待时间间隔,再运行行为
            /// </param>
            public TimedIntervalTask(Action action, int intervalTime, bool whetherToRunFirst = true)
            {
                TimedAction = action;
                IntervalTime = intervalTime;
                WhetherToRunFirst = whetherToRunFirst;
                Restore();
            }

            /// <summary>
            /// 还原
            /// </summary>
            private void Restore()
            {
                IsPause = false;
                IsStop = false;
                IsRunning = false;
                IsStarted = false;
            }

            /// <summary>
            /// 定时的行为
            /// </summary>
            private Action TimedAction { get; set; }

            /// <summary>
            /// 行为运行的间隔时间,单位:毫秒
            /// </summary>
            public int IntervalTime { get; set; }

            /// <summary>
            /// 是否先运行 行为,默认 true
            /// <para>
            /// 若为 true,则先运行行为,再等待时间间隔;若设为 false,则先等待时间间隔,再运行行为
            /// </para>
            /// </summary>
            public bool WhetherToRunFirst { get; set; }

            /// <summary>
            /// 是否已经启动,true 已启动,false 未启动;默认 false
            /// </summary>
            public bool IsStarted { get; private set; } = false;

            /// <summary>
            /// 是否暂停行为,默认 false
            /// <para>
            /// 若为 true,则暂停定时行为的运行;若再设为 false,则继续定时行为的运行
            /// </para>
            /// </summary>
            public bool IsPause { get; private set; } = false;

            /// <summary>
            /// 是否终止定时任务,默认 false
            /// </summary>
            public bool IsStop { get; private set; } = false;

            /// <summary>
            /// 定时任务是否在运行中;true 是,false 否;默认 false
            /// </summary>
            public bool IsRunning { get; private set; } = false;

            /// <summary>
            /// 启动任务运行
            /// <para>注意:再次调用此方法需要先执行 Stop() 终止定时任务</para>
            /// </summary>
            public void Startup()
            {
                if (TimedAction == null || IsStarted)
                {
                    return;
                }

                Task.Run(async () =>
                {
                    IsStarted = true;

                    while (!IsStop)
                    {
                        IntervalTime = IntervalTime < 1 ? 1 : IntervalTime;
                        if (IsPause)
                        {
                            IsRunning = false;
                            await Task.Delay(IntervalTime);
                            continue;
                        }
                        else
                        {
                            IsRunning = true;
                        }

                        if (WhetherToRunFirst)
                        {
                            _ = Task.Run(() =>
                            {
                                try
                                {
                                    TimedAction();
                                }
                                catch (Exception)
                                {
                                    // 不做处理
                                }
                            });
                            await Task.Delay(IntervalTime);
                        }
                        else
                        {
                            await Task.Delay(IntervalTime);
                            _ = Task.Run(() =>
                            {
                                try
                                {
                                    TimedAction();
                                }
                                catch (Exception)
                                {
                                    // 不做处理
                                }
                            });
                        }
                    }

                    Restore();
                });
            }

            /// <summary>
            /// 终止定时任务
            /// <para>注意:调用该方法后,定时任务会在下一个运行周期终止定时任务,这可能会产生一个等待期;</para>
            /// <para>可通过 IsStarted 属性判断,当值为 false 时(未启动),则表示已终止定时任务,可通过 Startup() 重新启动运行</para>
            /// </summary>
            public void Stop()
            {
                IsStop = true;
            }

            /// <summary>
            /// 暂停定时任务
            /// </summary>
            public void Pause()
            {
                IsPause = true;
            }

            /// <summary>
            /// 继续定时任务
            /// </summary>
            public void GoOn()
            {
                IsPause = false;
            }
        }
View Code

录制任务

这里开了两个线程,使用图片队列,一个获取图片,一个写入视频

   private ConcurrentQueue<Bitmap> bitmaps = new ConcurrentQueue<Bitmap>();  // 用来存放 桌面屏幕图片的线程安全队列
        private TimedIntervalTask timedIntervalTaskGetScreenImg;
        private TimedIntervalTask timedIntervalTaskVideoWriter;
        /// <summary>
        /// 录制任务加载
        /// </summary>
        private void RecordLoad()
        {
            bitmaps = new ConcurrentQueue<Bitmap>();
            timedIntervalTaskGetScreenImg = new TimedIntervalTask(() =>
            {
                if (timedIntervalTaskGetScreenImg.IsRunning)
                {
                    Dispatcher.BeginInvoke(new Action(() =>
                    {
                        bitmaps.Enqueue(GetScreenImgByteArray());
                        framescount++;
                    }));
                }
            }, GetFpsTime());
            timedIntervalTaskVideoWriter = new TimedIntervalTask(() =>
            {
                var ts = TimeSpan.FromSeconds(framescount / recordgfps);
                Dispatcher.BeginInvoke(new Action(() =>
                {
                    timetip.Text = $"{ts.Hours}:{ts.Minutes}:{ts.Seconds}";

                    while (bitmaps.Count > 0)
                    {
                        Bitmap bitmap = null;
                        if (bitmaps.TryDequeue(out bitmap))
                        {
                            BitmapData bitLock = bitmap.LockBits(new Rectangle(System.Drawing.Point.Empty, gridsize), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                            ImageData bitmapData = ImageData.FromPointer(bitLock.Scan0, ImagePixelFormat.Bgr24, gridsize);
                            videofile.Video.AddFrame(bitmapData);
                            bitmap.UnlockBits(bitLock);
                            bitmap.Dispose();
                        }
                        GC.Collect();
                    }
                }));
            }, 200);
        }
View Code

录制开始

在录制开始前需要先创建录制路径,录制路径自己设置

int framescount = 0;
private void StartRecord()
{
   framescount = 0;
                    bitmaps = new ConcurrentQueue<Bitmap>();
                    RecordLoad();
                    timedIntervalTaskGetScreenImg.IntervalTime = GetFpsTime();
                    timedIntervalTaskGetScreenImg.Startup();
                    timedIntervalTaskVideoWriter.Startup();
}
View Code

录制结束

因为使用图片队列,所以录制完成需等图片队列清空后再释放资源

 private void StopRecord()
        {
            if (!timedIntervalTaskGetScreenImg.IsStarted)
            {
                return;
            }
            timedIntervalTaskGetScreenImg.Stop();
            timedIntervalTaskVideoWriter.IntervalTime = 100;
            Task.Run(async () =>
            {
                while (bitmaps.Count >= 0)
                {
                    await Task.Delay(30);
                }
            });
            timedIntervalTaskVideoWriter.Stop();
            if (videofile != null)
            {
                videofile.Video.Dispose();
                videofile.Dispose();
            }
        }
View Code

 

阅读更多
点击右上角即可分享
微信分享提示