zoukankan      html  css  js  c++  java
  • 一款高效视频播放控件的设计思路(c# WPF版)

      因工作的需要,开发了一款视频播放程序。期间也经历许多曲折,查阅了大量资料,经过了反复测试,终于圆满完成了任务。

    我把开发过程中的一些思路、想法写下来,以期对后来者有所帮助。

    视频播放的本质

      就是连续的图片。当每秒播放的图片超过一定数量,人眼就很难觉察到每帧图像播放间隔,看到的就是连续的视频流。

    视频播放的过程

      必须有数据源,数据源一般是摄像头采集后,再经过压缩传送到程序。摄像头采集的视频信号一般转换为YUV格式、这个格式再经过h264压缩,传送出去。(视频信号不经过压缩,数据量非常大,h264是当今最流行的压缩格式)

      程序处理的过程要经过相反的过程。先对h264解压缩获取YUV格式数据,再将YUV格式数据转换为RGB格式。视频控件的功能就是高效的把RGB数据显示出来。后续主要介绍这个处理流程。

      h264解压缩采用的ffmpeg库,如何处理解压缩见我另一篇文章:使用ffmpeg实现对h264视频解码。YUV格式转换为RGB格式的原理并不复杂,关键是转换效率,一般的算法占用CPU非常高,我这里也是采用ffmpeg库提供的转换算法。

    视频播放代码解析

      1)播放视频的本质就是rgb数据的高效显示。播放控件输入数据如下: 

        public class BitmapDecodeInfo
        {
            public VideoClientTag ClientTag; //视频源唯一标识
            public Size OrginSize;          //视频原始大小
            public Size CurSize;            //视频当前大小
            public int Framerate;           //每秒播放帧数
    
            public byte[] RgbData { get; internal set; }  //视频数据
        }

        RgbData数据就是ffmpeg解压缩后的数据,该数据根据播放控件的大小,对视频做了缩放。

      2)RGB数据转换

      视频播放使用WPF Image控件,对此控件做了进一步封装。设置Image.Source属性,就可以显示图片。Source属性的类型是ImageSource。RGB数据必须转换为ImageSource类型,才能对Image.Source赋值。这里,我是把RGB数据使用WriteableBitmap封装,能很好的实现这个功能。

           WriteableBitmap _drawBitmap;
            public WriteableBitmap GetWriteableBitmap(BitmapDecodeInfo bitmapInfo, out bool newBitmap)
            {
                if (_drawBitmap == null
                    || _drawBitmap.Width != bitmapInfo.CurSize.Width
                    || _drawBitmap.Height != bitmapInfo.CurSize.Height)
                {
                    newBitmap = true;
                    _drawBitmap = new WriteableBitmap(bitmapInfo.CurSize.Width, bitmapInfo.CurSize.Height, 96, 96, PixelFormats.Bgr24, null);
                    _drawBitmap.WritePixels(new Int32Rect(0, 0, bitmapInfo.CurSize.Width, bitmapInfo.CurSize.Height),
                        bitmapInfo.RgbData, _drawBitmap.BackBufferStride, 0);
                    return _drawBitmap;
                }
                else
                {
                    newBitmap = false;
                    _drawBitmap.WritePixels(new Int32Rect(0, 0, bitmapInfo.CurSize.Width, bitmapInfo.CurSize.Height),
                       bitmapInfo.RgbData, _drawBitmap.BackBufferStride, 0);
                    return _drawBitmap;
                }
            }

    将WriteableBitmap赋值给Image.Source,就能显示图片了。还有几点需要注意:对界面控件的赋值必须在界面线程处理:

      Dispatcher.Invoke(new Action(() =>
                {
                    if (AppValue.WpfShowImage)
                    {
                        BitmapSource source = GetWriteableBitmap(bitmapInfo, out bool newBitmap);
                        if (newBitmap)
                            Source = source;
                    }
                }));

        3)数据缓冲和精确定时

       视频数据的来源不可能是均匀连续的,需要对数据做缓冲,再均匀连续的播放出来。需要将数据放到缓冲类中,每隔固定的时间去取。

       现在假定播放帧数为25帧每秒。该缓冲类有自适应功能,就是缓冲数据帧数小于一定值时,播放变慢;否则,播放变快。

     //图像缓冲,播放速度控制
        public class ImageVideoPool
        {
            public long _spanPerFrame = 40; //时间间隔 毫秒。每秒25帧
    
            public long _spanPerFrameCur = 40;
    
            public void SetFramerate(int framerate)
            {
                _spanPerFrame = 1000 / framerate;
                _spanPerFrameCur = _spanPerFrame;
            }
    
            ObjectPool<BitmapDecodeInfo> _listVideoStream = new ObjectPool<BitmapDecodeInfo>();
            public void PutBitmap(BitmapDecodeInfo image)
            {
                _listVideoStream.PutObj(image);
                if (_listVideoStream.CurPoolCount > _framePoolCountMax * 2)
                {
                    _listVideoStream.RemoveFirst();
                }
                SetCutPoolStage();
            }
    
            public int ImagePoolCount => _listVideoStream.CurPoolCount;
    
            long _playImageCount = 0;
            public long PlayImageCount => _playImageCount;
    
            void SetCutPoolStage()
            {
                Debug.Assert(_framePoolCount > _framePoolCountMin && _framePoolCount < _framePoolCountMax);
                //设置当前的状态
                if (_listVideoStream.CurPoolCount < _framePoolCountMin)
                {
                    SetPoolStage(EN_PoolStage.up_to_normal);
                }
                else if (_listVideoStream.CurPoolCount > _framePoolCountMax)
                {
                    SetPoolStage(EN_PoolStage.down_to_normal);
                }
                else if (_listVideoStream.CurPoolCount == _framePoolCount)
                {
                    SetPoolStage(EN_PoolStage.normal);
                }
            }
    
            long _lastPlayerTime = 0;
            long _curPlayerTime = 0;
            internal void OnTimeout(int spanMs)
            {
                _curPlayerTime += spanMs;
            }
    
            int _framePoolCount = 30;
            int _framePoolCountMax = 50; //缓冲数据大于此值,播放变快
            int _framePoolCountMin = 10; //缓冲数据小于此值,播放变慢
            EN_PoolStage _poolStage = EN_PoolStage.normal;
            public BitmapDecodeInfo GetBitmapInfo()
            {
                if (_listVideoStream.CurPoolCount == 0)
                    return null;
    
                int timeSpan = (int)(_curPlayerTime - _lastPlayerTime);
                if (timeSpan < _spanPerFrameCur)
                    return null;
    
                BitmapDecodeInfo result = _listVideoStream.GetObj();
                if (result != null)
                {
                    SetCutPoolStage();
                    _lastPlayerTime = _curPlayerTime;
                    _playImageCount++;
                }
                return result;
            }
    
            void SetPoolStage(EN_PoolStage stag)
            {
                bool change = (_poolStage == stag);
                _poolStage = stag;
                if (change)
                {
                    switch (_poolStage)
                    {
                        case EN_PoolStage.normal:
                            {
                                _spanPerFrameCur = _spanPerFrame;//恢复正常播放频率
                                break;
                            }
                        case EN_PoolStage.up_to_normal:
                            {
                                //播放慢一些
                                _spanPerFrameCur = (int)(_spanPerFrame * 1.2);
                                break;
                            }
                        case EN_PoolStage.down_to_normal:
                            {
                                //播放快一些
                                _spanPerFrameCur = (int)(_spanPerFrame * 0.8);
                                break;
                            }
                    }
                }
            }
    
    
            enum EN_PoolStage
            {
                normal,
                up_to_normal,//从FramePoolCountMin--》FramePoolCount
                down_to_normal,//FramePoolCountMax--》FramePoolCount
            }
        }

      需要一个精确时钟,每隔一段时间从缓冲区取数据,再将数据显示出来。Windows下多媒体时钟精度较高,定时器代码如下:

        class MMTimer
        {
            //Lib API declarations
            [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
            static extern uint timeSetEvent(uint uDelay, uint uResolution, TimerCallback lpTimeProc, UIntPtr dwUser,
                                            uint fuEvent);
    
            [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
            static extern uint timeKillEvent(uint uTimerID);
    
            [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
            static extern uint timeGetTime();
    
            [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
            static extern uint timeBeginPeriod(uint uPeriod);
    
            [DllImport("Winmm.dll", CharSet = CharSet.Auto)]
            static extern uint timeEndPeriod(uint uPeriod);
    
            //Timer type definitions
            [Flags]
            public enum fuEvent : uint
            {
                TIME_ONESHOT = 0, //Event occurs once, after uDelay milliseconds. 
                TIME_PERIODIC = 1,
                TIME_CALLBACK_FUNCTION = 0x0000, /* callback is function */
                                                 //TIME_CALLBACK_EVENT_SET = 0x0010, /* callback is event - use SetEvent */
                                                 //TIME_CALLBACK_EVENT_PULSE = 0x0020  /* callback is event - use PulseEvent */
            }
    
            //Delegate definition for the API callback
            delegate void TimerCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2);
    
            //IDisposable code
            private bool disposed = false;
    
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
    
            private void Dispose(bool disposing)
            {
                if (!this.disposed)
                {
                    Stop();
                }
                disposed = true;
            }
    
            ~MMTimer()
            {
                Dispose(false);
            }
    
            /// <summary>
            /// The current timer instance ID
            /// </summary>
            uint id = 0;
    
            /// <summary>
            /// The callback used by the the API
            /// </summary>
            TimerCallback thisCB;
    
            /// <summary>
            /// The timer elapsed event 
            /// </summary>
            public event EventHandler Timer;
    
            protected virtual void OnTimer(EventArgs e)
            {
                if (Timer != null)
                {
                    Timer(this, e);
                }
            }
    
            public MMTimer()
            {
                //Initialize the API callback
                thisCB = CBFunc;
            }
    
            /// <summary>
            /// Stop the current timer instance (if any)
            /// </summary>
            public void Stop()
            {
                lock (this)
                {
                    if (id != 0)
                    {
                        timeKillEvent(id);
                        Debug.WriteLine("MMTimer " + id.ToString() + " stopped");
                        id = 0;
                    }
                }
            }
    
            /// <summary>
            /// Start a timer instance
            /// </summary>
            /// <param name="ms">Timer interval in milliseconds</param>
            /// <param name="repeat">If true sets a repetitive event, otherwise sets a one-shot</param>
            public void Start(uint ms, bool repeat)
            {
                //Kill any existing timer
                Stop();
    
                //Set the timer type flags
                fuEvent f = fuEvent.TIME_CALLBACK_FUNCTION | (repeat ? fuEvent.TIME_PERIODIC : fuEvent.TIME_ONESHOT);
    
                lock (this)
                {
                    id = timeSetEvent(ms, 0, thisCB, UIntPtr.Zero, (uint)f);
                    if (id == 0)
                    {
                        throw new Exception("timeSetEvent error");
                    }
                    Debug.WriteLine("MMTimer " + id.ToString() + " started");
                }
            }
    
            void CBFunc(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2)
            {
                //Callback from the MMTimer API that fires the Timer event. Note we are in a different thread here
                OnTimer(null);
            }
        }
    View Code

    总结:

    把视频控件处理流程梳理一下:1视频数据放入缓冲-->定时器每隔一端时间取出数据-->将数据显示到image控件上。

    后记:
    交通部2016年发布一个规范《JT/T1076-2016道路运输车辆卫星定位系统车载视频终端技术要求》,规范的目的是一个平台可以接入多个硬件厂家的视频数据。本人就是依据这个规范开发的系统。视频解码采用ffmpeg开源库。整个系统包括视频数据采集、流媒体服务器、视频播放器。所有程序采用c#编写。视频数据的数据量一般都很大;所以,在开发过程中,十分注重性能。
    有些人对c#的性能有些担忧的,毕竟市面上的流媒体服务器、播放器大部分都是c语言编写的。我从事c#开发10多年,认为c#性能上是没有问题,关键还是个人要对算法有所了解,对所处理的逻辑有所了解。一切都是拿来主义,性能肯定不会高。开发本系统中,好多处理算法都是自己从头编写。事实证明,c#也可以开发出高效的系统。
    我大概用了3个月,把整个系统设计完成。

             
  • 相关阅读:
    LAMP环境搭建
    Centos系统下Lamp环境的快速搭建(超详细)
    主题:Windows系统服务器磁盘挂载
    云硬盘
    独立IP与共享IP的区别
    网站备案的注意事项
    云主机与vps虚拟主机的区别
    vim 命令大全 / vi 命令大全
    【Linux】Linux中常用操作命令
    Linux Shell常用技巧(一) RE
  • 原文地址:https://www.cnblogs.com/yuanchenhui/p/videoplay.html
Copyright © 2011-2022 走看看