陈义锋 黄陂电视台新闻经济中心技术部主任,工程师
前言:
自2003年起,我们一直从事播控软件的开发设计,相继推出了TVPLAYER系列硬盘播出软件、广告插播软件、互动点播游戏软件和资讯制播软件,软件用户有十余家电视台、商场、部队等机构。
本文介绍了我们今年推出新一代小型硬盘播出系统的软硬件设计实现的诸多细节,并详细讨论了基于GDI+的字幕软件设计的基本技术细节。
本文所涉及的软件实现细节均是目前流行硬盘播出软件和字幕软件的重要内容,到目前为止我们还不能从任何资料和网络上检索到相关内容。
提纲:
一、 基于MCI API的硬盘播出系统回顾
二、 基于Topack卡的硬盘播出系统介绍
三、 使用桌面型数据库的小型系统
四、 一体化的解决方案
五、 快捷的节目单编制程序设计
六、 使用字幕的问题
七、 怎样抗抖晃
八、 使用GDI Plus设计字幕软件
九、 使用VC++设计播出软件
十、 智能化的矛盾
一、 基于MCI API的硬盘播出系统回顾
四年前我们开始开发适合市县级电视台的播出系统时并没有合适的专用播出卡,当时几乎所有中低档硬盘播出产品都是基于sigma公司系列解压芯片的产品。驱动解压卡的手段有sigma SDK、Direct show和MCI API,然后使用各种数据库作为节目单管理系统,也有使用配置文件进行管理的。
由于地方小台节目来源不太好,基本上以PAL制、NTSC制的MPEG1和MPEG2文件为主,要想通过程序将各种各样的节目文件组织在一起准确流畅地播放出去并不是一件容易的事。特别是节目间相衔接的问题,此外还有定位不准、音频丢失等一系列难题。这时候有一个新名词——“无缝链接”出现了,指的是在两个节目切换时平滑不抖动不夹帧不花屏不停顿,实现的途径也有很大的区别,有些干脆用一屏字幕遮一下。
当时我们采用了MCI API进行设计,MCI(Media Control Interface)——媒体控制接口,API(Application Programming Interface)——应用编程接口;是Windows提供的一组16位多媒体驱动接口,可以控制多媒体设备的播放、停止、录制和关闭等操作,由于是16位API,故并不支持超过2GB的大文件也不支持长文件名。
从系统播放视频文件的角度看,系统打开视频文件,建立缓冲区,播放缓冲区数据。但是大多数驱动解压卡的方法采用了常见的为不同播放实例建立缓冲区的办法,那么在一个缓冲区停止写数据而切换到另一个缓冲区开始缓冲播放的过程时“有缝链接”自然就产生了。
MCI指令可以通过一定的技巧来避免这个问题,MCI指令支持以别名的方式同时打开256个媒体文件,当一个文件正在解压播放时,我们可以切换到另一个打开的文件而后再关闭先前播放的文件,只要解压卡不需要调用不同解压驱动(即不是不同压缩格式或制式的文件相互切换),就绝对不会出现缓冲区数据中断而引起的各种图像输出不正常的现象,即使是不同制式不同压缩格式的两个文件相切换也可以将影响控制到最小。这就是所谓的“无
缝链接”技术。
图1 TVPLAYER EXPRESS节目播出界面图
图1是我们设计的第一代硬盘播出软件TVPLAYER EXPRESS在节目播出时的界面,该软件使用Visual Basic 6.0设计,采用了ACCESS数据库,播出卡为Sigma公司产品。
二、 基于Topack卡的硬盘播出系统介绍
针对第一代中低端硬盘播出卡存在的诸多问题,北京华视恒通公司推出了Topack系列硬盘播出和资讯播出卡,Topack卡系统全面地解决了第一代播出卡的节目衔接、定位、花屏、无声等问题,同时在卡上集成了字幕功能,通过华视恒通提供的开发包可以很方便的设计硬盘播出软件。该系列卡目前在中低端市场占有率较高。
由于字幕功能的引入,开发人员不得不把更多的精力投入到字幕软件的设计上,也有些公司干脆外包了字幕程序,如华视恒通公司的产品就采用了北京飞尚的字幕软件。此外由于开发门槛的相对降低,硬盘播出系统的比拼更集中到软件功能、播控逻辑、稳定性和可操作性上。
三、 使用桌面型数据库的小型系统
现在不使用数据库的硬盘播出系统几乎是不可思议的,使用数据库就要在数据库部署成本、运行速度、稳定性和维护难度上均衡考虑,我们有使用MS SQL和ACCESS数据库的不同版本让客户选择,基本上出于成本和维护的考虑客户更倾向于ACCESS版本。ACCESS是微软公司的一个小型桌面型数据库,ACCESS软件是MS OFFICE系列软件中的一个,非常小巧也易于维护。
四、 一体化的解决方案
在经济实力较弱的小台,会要求设备的集成度越高越好,而且设备运行时间非常长,我们销售的硬盘播出设备常常是一台机器负责一个频道的播出,甚至也没有备用机。而操作人员常常根本就不懂计算机。
节目采集、存储、节目单编制、播出包括字幕叠加、切换器管理都被要求集中到一台机器中,至于使用播出服务器的模式几乎很难推广。
Topack卡可以支持MPEG1和MPEG2的视频文件播放,可以直接播放VCD和DVD的视频文件,所以在节目采集方面可以通过拷贝光碟来实现。
至于自办节目可以通过网络传输,一般我们建议将制作网络和播出网络隔离,通过一台FTP服务器来实现文件的中继传输以保证播出节目的安全性。
早期的硬盘播出要集成字幕是一件复杂的事情,需要一块额外的字幕卡,字幕软件和播出软件还不一定能够集成到一起,不过在新的播出系统中一切都得到了妥善的解决。
此外,我们还集成了一块监控卡,用来监控播放的节目画面,有些型号的Topack卡还有一个内置同步机和掉电直通的二选一切换器,方便只有一个频道的播出机构使用。
五、 快捷的节目单编制程序设计
图2是我们新一代硬盘播出系统的节目编排软件测试版的界
图2 节目编排软件测试版的界面图
面,和以往的设计不同,新的硬盘播出系统我们一共设计了三款不同功能的软件,包括节目单编排软件,字幕制作软件和播出控制软件。其中字幕制作软件和播出软件使用Visual C++ 6.0设计,而节目单编排软件则是用Visual Basic 6.0设计的。
节目单编排是一件非常繁琐的工作,要求又非常严格。要想既准确又高效率并不是一件很容易的事。对于这个软件的设计来说,困难的并不是什么技术问题,而是对播控逻辑的定义。一个好的程序不仅仅是技术实现的问题,更重要的是设计思想的问题。
大体上有两种逻辑思维,一种将节目进行严格的归类,然后根据客户对这些分类的不同定义来安排节目的播出顺序,但在程序设计时要实现这样的智能化或者说学习型逻辑控制是很困难的,基本上程序员会自己给出一个自认为最佳的分类模式,以此确定播控的逻辑关系,那么用户必须适应这个逻辑关系。
我们一直采用另外一种逻辑关系,即播出时间上的线性关系,通过节目单编排软件将非线性存在的节目文件通过入点、出点的设置线性地组织到一个播出数据库中,这样一来对于用户而言播控逻辑非常清晰明了,对于程序设计而言主线条也是非常清晰的,我们可以通过一系列技术手段使节目的编排更具智能化,从而更进一步简化操作。
六、 使用字幕的问题
图3 SmartCG
软件界面图
从软件设计的角度看字幕程序是一个比较困难的项目,对程序员的要求更高一些,而各种资料更是非常少见了,这是大部分硬盘播出系统将字幕外包的原因。我们通过两个途径来解决这个难题,一是通过对通用图像格式的支持如TAG格式、PNG格式来实现第三方字幕软件文件的导入,一是对常用的字幕功能,如左飞字幕、挂角字幕、台标和时钟通过自行设计软件来实现。事实上Topack播出卡集成的字幕功能就是CG1000系列字幕卡的基本功能,我们设计这样一个软件,将来也可以很方便地移植到CG1000系列字幕卡上去。我们使用Visual C++和微软新一代图形接口GDI Plus设计了一个能够快速生成上述字幕文件的软件SmartCG。(如图3)
七、 怎样抗抖晃
如果简单地将图片数据给字幕卡直接叠加出去,抖晃是不可避免的,我们甚至在一些较低端的非编和虚拟演播室中看到这样的缺陷。由于电视图像采用隔行扫描,越是细小的横线抖晃越厉害,另外,越是锐利的图案越容易闪烁,这和模拟信号波形的特点有非常大的关系。
抗抖晃如果不能由字幕卡硬件提供,那就需要采用软件的方法来实现了。为了让液晶显示屏显示的文字看起来更漂亮,微软提供了一种新的显示技术,称作ClearType,可以通过平滑抗锯齿让文字更显圆滑美观。当然这个离抗抖晃还有一点距离。
综合上述分析和无数次实验,我们认为抗抖晃可以通过对图像进行平滑滤波处理来实现,而平滑滤波处理实际上就是一种卷积算法!
下面给出三个平滑算法的卷积核:
| 1 1 1 |
| 1 1 1 |
| 1 1 1 |9 //divisor= 9,
| 1 1 1 |
| 1 2 1 |
| 1 1 1 | 10 //divisor= 10,
| 1 2 1 |
| 2 4 2 |
| 1 2 1 |16 // divisor= 16,
通过选择不同的卷积核就会得到不同的抗抖晃效果了,基本上通过一次卷积运算抗抖晃的效果就非常好了。
下面是一个完整的卷积处理函数,对RGB数据进行了卷积,和微软的ClearType不同——他们对Alpha数据也进行了卷积,而且卷积核是不一样的。
void CSmartCGDlg::Cirro(ULONG *pp, UINT BmW, UINT BmH,int level)
{
ULONG a1,r1,g1,b1;
UINT x,y,j,k,l,r[9],g[9],b[9];
for (x=1;x<BmH;x++)
{
for(y=1;y<BmW;y++)
{
k=0;
for(j=0;j<3;j++)
{
for(l=0;l<3;l++)
{
r[k]=(UINT)(pp[(x-1+l)*BmW+y-1+j]<<8)>>24;
g[k]=(UINT)(pp[(x-1+l)*BmW+y-1+j]<<16)>>24;
b[k]=(UINT)(pp[(x-1+l)*BmW+y-1+j]<<24)>>24;
k++;
}
}
a1=(pp[x*BmW+y]>>24)<<24;
//三种卷积核
switch (level)
{
case 1:
{
r1=(ULONG)(r[0]+r[1]+r[2]+r[3]+r[4]+r[5]+r[6]+r[7]+r[8])/9;
g1=(ULONG)(g[0]+g[1]+g[2]+g[3]+g[4]+g[5]+g[6]+g[7]+g[8])/9;
b1=(ULONG)(b[0]+b[1]+b[2]+b[3]+b[4]+b[5]+b[6]+b[7]+b[8])/9;
break;
}
case 2:
{
r1=(ULONG)(r[0]+r[1]+r[2]+r[3]+2*r[4]+r[5]+r[6]+r[7]+r[8])/10;
g1=(ULONG)(g[0]+g[1]+g[2]+g[3]+2*g[4]+g[5]+g[6]+g[7]+g[8])/10;
b1=(ULONG)(b[0]+b[1]+b[2]+b[3]+2*b[4]+b[5]+b[6]+b[7]+b[8])/10;
break;
}
case 3:
{
r1=(ULONG)(r[0]+2*r[1]+r[2]+2*r[3]+4*r[4]+2*r[5]+r[6]+2*r[7]+r[8])/16;
g1=(ULONG)(g[0]+2*g[1]+g[2]+2*g[3]+4*g[4]+2*g[5]+g[6]+2*g[7]+g[8])/16;
b1=(ULONG)(b[0]+2*b[1]+b[2]+2*b[3]+4*b[4]+2*b[5]+b[6]+2*b[7]+b[8])/16;
break;
}
}
r1=r1<<16;
g1=g1<<8;
pp[x*BmW+y]=a1+r1+g1+b1;
}
}
}
八、 使用GDI Plus设计字幕软件
GDI Plus即GDI+是一个应用编程接口,通过一组C++类来提供接口功能,是早期Windows版本中包含的图形设备接口GDI的升级版本。相对于与图形设备高度相关的GDI,GDI+更灵活并具有更多功能,可以把GDI+看作GDI的一个高层次的封装类。我们可以通过VC++来调用GDI+类来设计字幕软件。
值得注意的是,GDI+作为.net语言的组成部分,默认支持UNICODE字符集,直观的看GDI+使用WCHAR代替char类型,但是即使通过字符类型转换,程序编译正确也存在问题——输出中文会变成乱码。所以直接建立UNICODE而不是ANSI工程来设计字幕软件是最好的选择。
通过GDI+可以很方便地实现很多二维图形和文字排版功能,包括GDI中较复杂的字边特效使用GDI+就可以很轻松的实现,前面的贴图显示了一个渐变色字边的效果,下面的程序演示了输出一条“发光字”左飞字幕到一个数据文件的全部功能。
void CSmartCGDlg::OnClickoutput()
{
double h,x0,x1,y0,y1,a;
const double pi=3.1415926;
long i,j,ipage,length,ts;
//中心点(360,530)
CString strpath;
CFileDialog dlg(FALSE);
dlg.m_ofn.lpstrFilter=L"CG Files (*.cyy)\0*.cyy\0\0";
if (dlg.DoModal()==IDOK)
{
strpath=dlg.GetFileName();
CString rstr;
rstr=dlg.GetFileExt();
rstr.MakeLower();
if ((rstr!=L"cyy")) strpath=strpath+L".cyy";
//Invalidate(TRUE);
}else
{
return;
}
HWND hwnd=m_mainpic.GetSafeHwnd();
Graphics graphics(hwnd);
Bitmap bm(721,576);
CSize ClientSize(720,576);
Bitmap bm1(ClientSize.cx/5,ClientSize.cy/5);
Graphics * graphicsm=Graphics::FromImage(&bm);
//设置变换位图和矩阵
Graphics * bmpg=Graphics::FromImage(&bm1);
Matrix mx(1.0f/5,0,0,1.0f/5,-(1.0f/5),-(1.0f/5));
bmpg->SetSmoothingMode(SmoothingModeAntiAlias);
bmpg->SetTransform(&mx);
SolidBrush fbrush(FColor);
CDC * dc=this->GetDC();
HDC idc=dc->m_hDC;
Font font(idc,logfont);
PointF pointf(0,500.0f);
RectF rect;
SolidBrush brush(BkColor1);
//BSTR cstr;
UpdateData(TRUE);
char c=10;
char c2=32;
//BitmapData pp;
m_str.Replace(c,c2);
Color icolor;
int len=m_str.GetLength();
//cstr=m_str.AllocSysString();
//获取输出字幕屏数
graphics.MeasureString(m_str, len, &font, pointf,&rect);
m_txtwidth=(long)rect.Width;
ipage=m_txtwidth/720+2;
m_bmwidth=ipage*720;
length=m_bmwidth*50+2;
FontFamily lf(L"黑体");
font.GetFamily(&lf);
//Image bImage(L"c:\\TransBK.bmp");
GraphicsPath path;
h=25;
a=((double)m_linrot1.GetValue())/180*pi;
y0=h;
x0=tan(a)*y0;
x1=360+x0;
y1=530+y0;
x0=360-x0;
y0=530-y0;
LinearGradientBrush lbrushzs(Point((int)x0,(int)y0),Point((int)x1,(int)y1),FColor,BColor);
h=25;
a=((double)m_linrot3.GetValue())/180*pi;
y0=h;
x0=tan(a)*y0;
x1=360+x0;
y1=530+y0;
x0=360-x0;
y0=530-y0;
LinearGradientBrush lbrushzb(Point((int)x0,(int)y0),Point((int)x1,(int)y1),LColor1,LColor2);
Pen pen(&lbrushzb,2);
graphicsm->SetSmoothingMode(SmoothingModeAntiAlias);
graphicsm->SetTextRenderingHint(TextRenderingHintAntiAliasGridFit);
graphicsm->SetTransform(&Matrix(0,0,0,0,5,5));
graphicsm->SetSmoothingMode(SmoothingModeAntiAlias);
graphicsm->SetInterpolationMode(InterpolationModeHighQualityBicubic);
ULONG * pdat=new ULONG [ length ];
ULONG cc,k,l;
k=0;
m_pro1.SetMin(0);
m_pro1.SetMax(ipage);
m_pro1.UpdateData(TRUE);
Rect irect(0,0,721,576);
for (l=0;l<ipage;l++)
{
path.Reset();
path.AddString(m_str,len,&lf,font.GetStyle(),font.GetSize(),Point(720-720*l,500),NULL);
graphicsm->Clear(Color(0,0,0,0));
bmpg->Clear(Color(0,0,0,0));
bmpg->DrawPath(&pen,&path);
bmpg->FillPath(&lbrushzb,&path);
graphicsm->FillRectangle(&brush,-10,495,733 ,55 ); graphicsm->DrawImage(&bm1,irect,0,0,bm1.GetWidth(),bm1.GetHeight(),UnitPixel);
graphicsm->FillPath(&lbrushzs,&path);
graphics.DrawImage(&bm,0,0);
for (i=1;i<720;i++)
{
for(j=0;j<50;j++)
{
bm.GetPixel(i,500+j,&icolor);
cc=icolor.GetValue();
pdat[k]=cc;
k++;
}
}
m_pro1.SetValue(l);
}
Cirro(pdat,50,m_bmwidth,3);
pdat[0]=m_bmwidth;
pdat[1]=50;
CFile file(strpath,CFile::modeCreate|CFile::modeWrite);
file.Write(pdat,(UINT)length*4);
file.Close();
//Sleep(200);
delete [] pdat;
//graphics.DrawImage(&bImage,0,0);
graphics.DrawImage(&bm,0,0);
m_pro1.SetValue(ipage);
ReleaseDC(dc);
}
说明:这段程序通过矩阵变换和图像二次插值使通过填充文字路径得到的图形形成虚化效果经过贴图和文字多次绘制来实现发光文字(虚边)效果,程序可将一大段文字分屏绘制并写入一个自动分配的数组中经过卷积处理后输出到一个二进制左飞字幕图形数据文件中,而这样一个文件在硬盘播出软件中可以非常方便地作为左飞字幕输出。
通过GDI+我们实现了常用字幕制作功能,作为一款硬盘播出系统附带的字幕工具,其功能和方便快捷的特性已足以满足要求。
九、 使用VC++设计播出软件
通过前面提到的节目单编制软件和字幕制作软件的支持,使用Topack SDK设计一款硬盘播出软件并不是一件很困难的事。当然,稳定是播控的第一要务,所以在设计播出软件时,我们更多的精力放在了软件容错处理上。
CGandPlayer是我们设计的新一代硬盘播出软件,这款软件在Topack硬盘播出卡上设计了一套适合中小型电视台使用的高度集成化硬盘播出系统,该软件使用Visual C++6.0设计完成。
图4是CGandPlayer硬盘播出软件测试版播出时的界面截图。
图4 CGandPlayer界面截图
下面是一段对SDK的调用实现播出和使用ADO控件操作数据库的程序。
void CCGandPlayerDlg::PlayNextMov()
{
CString strID;
short ii;
FNo=FieldNo=0;
long ast,subt;
UpdateData();
if (!Recset1.GetEof()&&!Recset1.GetBof())
{
Fld=Flds.GetItem(_variant_t((long)12));
StrFile=(Fld.GetValue()).bstrVal;//字符串
//AfxMessageBox(StrFile);
Fld=Flds.GetItem(_variant_t((long)5));
movin=(Fld.GetValue()).lVal;
Fld=Flds.GetItem(_variant_t((long)6));
movout=(Fld.GetValue()).lVal ;
movlen=movout-movin;
Fld=Flds.GetItem(_variant_t((long)19));
ast=(Fld.GetValue()).lVal;
Fld=Flds.GetItem(_variant_t((long)20));
subt=(Fld.GetValue()).lVal ;
ii=tpkOpen2Frame(0,StrFile,movin);
tpkSetParams(0,NEXT_AUDIO_ID,ast);
tpkSetParams(0,NEXT_SUBTITLE_ID,subt);
if (selAC3)
{
if (StrFile.Right(3)="vob")
{
if (StrFile.Left(StrFile.GetLength()-12)==vobpath)
{
if (Ustream)
{
tpkSetParams(0,NEXT_AUDIO_ID,Astream);
}
}
if (StrFile.Left(StrFile.GetLength()-12)==vobpath1)
{
if (Usub)
{
tpkSetParams(0,NEXT_SUBTITLE_ID,Tsub);
}
}
}
}
if (StrLR=="立体声")
{
ii=tpkSetParams(0,AUDIO_TRACK,STEREO);
}else if (StrLR=="左声道")
{
ii=tpkSetParams(0,AUDIO_TRACK,LEFT_TRACK);
}else if (StrLR=="右声道")
{
ii=tpkSetParams(0,AUDIO_TRACK,RIGHT_TRACK);
}
ii=tpkSetParams(0,SWITCH_MODE,SWITCH_NOW);
Fld=Flds.GetItem(_variant_t((long)7));//第0列
StrLR=(Fld.GetValue()).bstrVal;
Fld=Flds.GetItem(_variant_t((long)3));
CString stu="播出";
Fld.SetValue(_variant_t((CString)stu));
vBook=Recset1.GetBookmark();
Fld=Flds.GetItem(_variant_t((long)9));
ii=(Fld.GetValue()).boolVal;
if (ii==-1)
{
if (!RecLeft.GetBof()&&!RecLeft.GetEof())
{
showLeft=TRUE;
}else
{
showLeft=FALSE;
}
}else
{
showLeft=FALSE;
tpkClearCurrentScreen(0);
if (isTlog) tpkSetRectangle(0,0,0,Tww,Thh,TB);
}
Fld=Flds.GetItem(_variant_t((long)10));
ii=(Fld.GetValue()).boolVal;
if (ii==-1)
{
showLogo=TRUE;
}else
{
showLogo=FALSE;
tpkClearCurrentScreen(0);
if (isTlog) tpkSetRectangle(0,0,0,Tww,Thh,TB);
}
Fld=Flds.GetItem(_variant_t((long)11));
strID=(Fld.GetValue()).bstrVal;
strID="SELECT * FROM CGLogo WHERE (((CGLogo.字幕编号)='" + strID +"'))";
m_adoLogo.SetRecordSource(strID);
m_adoLogo.Refresh();
RecLogo=m_adoLogo.GetRecordset();
FldsLogo=RecLogo.GetFields();
lngTi=0;
}
}
十、 智能化的矛盾