一款高效视频播放控件的设计思路(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, , , PixelFormats.Bgr24, null);
_drawBitmap.WritePixels(new Int32Rect(, , bitmapInfo.CurSize.Width, bitmapInfo.CurSize.Height),
bitmapInfo.RgbData, _drawBitmap.BackBufferStride, );
return _drawBitmap;
}
else
{
newBitmap = false;
_drawBitmap.WritePixels(new Int32Rect(, , bitmapInfo.CurSize.Width, bitmapInfo.CurSize.Height),
bitmapInfo.RgbData, _drawBitmap.BackBufferStride, );
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 = ; //时间间隔 毫秒。每秒25帧 public long _spanPerFrameCur = ; public void SetFramerate(int framerate)
{
_spanPerFrame = / framerate;
_spanPerFrameCur = _spanPerFrame;
} ObjectPool<BitmapDecodeInfo> _listVideoStream = new ObjectPool<BitmapDecodeInfo>();
public void PutBitmap(BitmapDecodeInfo image)
{
_listVideoStream.PutObj(image);
if (_listVideoStream.CurPoolCount > _framePoolCountMax * )
{
_listVideoStream.RemoveFirst();
}
SetCutPoolStage();
} public int ImagePoolCount => _listVideoStream.CurPoolCount; long _playImageCount = ;
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 = ;
long _curPlayerTime = ;
internal void OnTimeout(int spanMs)
{
_curPlayerTime += spanMs;
} int _framePoolCount = ;
int _framePoolCountMax = ; //缓冲数据大于此值,播放变快
int _framePoolCountMin = ; //缓冲数据小于此值,播放变慢
EN_PoolStage _poolStage = EN_PoolStage.normal;
public BitmapDecodeInfo GetBitmapInfo()
{
if (_listVideoStream.CurPoolCount == )
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 = , //Event occurs once, after uDelay milliseconds.
TIME_PERIODIC = ,
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 = ; /// <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 != )
{
timeKillEvent(id);
Debug.WriteLine("MMTimer " + id.ToString() + " stopped");
id = ;
}
}
} /// <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, , thisCB, UIntPtr.Zero, (uint)f);
if (id == )
{
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);
}
}
总结:
把视频控件处理流程梳理一下:1视频数据放入缓冲-->定时器每隔一端时间取出数据-->将数据显示到image控件上。 后记:
交通部2016年发布一个规范《JT/T1076-2016道路运输车辆卫星定位系统车载视频终端技术要求》,规范的目的是一个平台可以接入多个硬件厂家的视频数据。本人就是依据这个规范开发的系统。视频解码采用ffmpeg开源库。整个系统包括视频数据采集、流媒体服务器、视频播放器。所有程序采用c#编写。视频数据的数据量一般都很大;所以,在开发过程中,十分注重性能。
有些人对c#的性能有些担忧的,毕竟市面上的流媒体服务器、播放器大部分都是c语言编写的。我从事c#开发10多年,认为c#性能上是没有问题,关键还是个人要对算法有所了解,对所处理的逻辑有所了解。一切都是拿来主义,性能肯定不会高。开发本系统中,好多处理算法都是自己从头编写。事实证明,c#也可以开发出高效的系统。
我大概用了3个月,把整个系统设计完成。
一款高效视频播放控件的设计思路(c# WPF版)的更多相关文章
- 将VLC库封装为duilib的万能视频播放控件
转载请说明出处,谢谢~~ 昨天封装好了基于webkit的浏览器控件,修复了duilib的浏览器功能的不足,而我的仿酷狗播放器项目中不光需要浏览器,同时也需要视频播放功能,也就是完成MV的功能.所以我打 ...
- 分享12款 JavaScript 表格控件(DataGrid)
JavaScript 表格控件可以操作大数据集的 HTML 表格,提供各种功能,如分页.排序.过滤以及行编辑.在本文中,我们整理了13个最好的 JavaScript 表格插件分享给开发人员,开发者可以 ...
- 用户控件的设计要点 System.Windows.Forms.UserControl
用户控件的设计要点 最近的项目中有一个瀑布图(彩图)的功能,就是把空间和时间上的点量值以图的形式呈现出来,如下图: X坐标为空间,水平方向的一个像素代表一个空间单位(例如50米) Y坐标为时间,垂直方 ...
- 玩转Android之在线视频播放控件Vitamio的使用
其实Android中自带的MediaPlayer本身就能播放在线视频,MediaPlayer结合SurfaceView播放在线视频也是不错的选择(如果你没有性能或者用户体验上的要求),关于MediaP ...
- 推荐一款JavaScript日历控件:kimsoft-jscalendar
一.什么是 kimsoft-jscalendar 一个简洁的avaScript日历控件,可在Java Web项目,.NET Web 项目中使用 二.kimsoft-jscalendar 有什么 ...
- Android高级_视频播放控件
一.Android系统自带VideoView控件 1. 创建步骤: (1)自带视频文件放入res/raw文件夹下: (2)声明初始化VideoView控件: (3)创建视频文件Uri路径,Uri调用p ...
- 《zw版·delphi与halcon系列原创教程》zw版_THOperatorSetX控件函数列表 v11中文增强版
<zw版·delphi与halcon系列原创教程>zw版_THOperatorSetX控件函数列表v11中文增强版 Halcon虽然庞大,光HALCONXLib_TLB.pas文件,源码就 ...
- Xceed WPF 主题皮肤控件Xceed Professional Themes for WPF详细介绍
Xceed Professional Themes for WPF是一款为你的整个应用程序提供完美WPF主题风格的控件,包含Office 2007和Windows 7,可以应用到任何微软官方的WPF控 ...
- 浅谈WPF中对控件的位图特效(WPF Bitmap Effects)
原文:浅谈WPF中对控件的位图特效(WPF Bitmap Effects) -------------------------------------------------------------- ...
随机推荐
- hdu 4996 1~n排列LIS值为k个数
http://acm.hdu.edu.cn/showproblem.php?pid=4996 直接贴bc题解 按数字1-N的顺序依次枚举添加的数字,用2N的状态保存在那个min数组中的数字,每次新添加 ...
- 【PAT Advanced Level】1014. Waiting in Line (30)
简单模拟题,注意读懂题意就行 #include <iostream> #include <queue> using namespace std; #define CUSTOME ...
- 团队博客-第三周:需求改进&系统设计(科利尔拉弗队)
针对课堂讨论环节老师和其他组的问题及建议,对修改选题及需求进行修改 需求规格说明书: 1.打开网页,弹出询问时候创建账号.是:分配数字组成账号,用户填写密码,确定登录进入首页:否,用已有账号登录(传参 ...
- wc.java
GitHub代码链接 1.项目相关要求 •基本功能列表: -c 统计文件中字符的个数 -w 统计文件中的词数 -l 统计文件中的行数 •拓展功能: -a 统计文件中代码行数.注释行数.空行 2 ...
- [ASP.NET]JQuery直接调用asp.net后台WebMethod方法
在项目开发碰到此类需求,特此记录下经项目验证的方法总结. 利用JQuery的$.ajax()可以很方便的调用asp.net的后台方法. [WebMethod] 命名空间 1.无参数的方法调用 注意:方 ...
- C#treeView控件单击事件选中节点滞后问题解决方法
问题描述:在treeView的Click事件中,选中的节点SelectedNode并不是您刚才点击的节点,总是上一次选中的节点,节点选中滞后的问题. 解决方案:在treeView的MouseDown事 ...
- Wpf中显示Unicode字符
1. 引言 今天在写一个小工具,里面有些字符用Unicode字符表示更合适.但是一时之间却不知道怎么写了.经过一番查找,终于找到了办法.记到这里,一是加深印象,二则以备查询. 2. C#中使用Unic ...
- Python 读取大文件的方式
对于读取容量小的文件,可以使用下面的方法: with open("path", "r") as f: f.read() 但是如果文件容量很大,高达几个G或者十几 ...
- python脚本 读取excel格式文件 并进行处理的方法
一.安装xlrd模块 pip install xlrd 二.读取excel文件 try: excel_obj = xlrd.open_workbook("文件路径") except ...
- MySQL(存储过程,支持事务操作)
day61 保存在MySQL上的一个别名 > 一坨SQL语句 -- delimiter // -- create procedure p1() -- BEGIN -- select * ...