玩转视频
同时播放您喜欢的视频。
引言
在我之前的文章《声音的乐趣》中,我们探讨了使用 winmm.dll mciSendString
API 同时播放多个声音文件。本着同样的乐趣精神,这次我们将用视频来做同样的事情,即在不同的显示区域播放多个视频或异步播放同一视频。
背景
视频文件有不同的格式。一些最常见的格式是:mpg, mp4, wmv, mov, rmvb
。这些文件可能包含嵌入式音轨,也可能不包含。每种文件格式的主要区别在于用于编码和压缩一系列图像和音频数据的编码器。
最常见的格式之一是 mp4
,因为它具有非常高的压缩率,同时又能保持合理的图像质量。这使得它成为移动设备上录制视频的流行编码标准。
要播放音频和视频,我们需要解码视频文件中的数据,因此我们需要编解码器(编码器-解码器)软件。编解码器软件可能是应用程序特定的,也可能是通用的,能够被 OpenGL
或 DirectShow
等常见接口/平台支持。
在 Windows(XP、7 和 8)中,对 DirectShow
的支持非常好,操作系统自带了许多编解码器和过滤器。使用 DirectShow
,我们应该能够播放大多数常见的视频和音频文件格式:mpg, wmv, wma, avi, animated gif
。然而,对于 mp4
和 rmvb
等一些非常流行的视频格式,操作系统不提供通用的 DirectShow
编解码器,而且看起来微软甚至不在其下载网站上提供这些。
然而,这些编解码器很容易从第三方下载网站获得。其中一个提供相对无忧下载的网站是 http://www.windows7codecs.com/
从该网站下载的内容提供了适用于大多数近期流行视频格式的 DirectShow
编解码器,包括 mp4
和 rmvb
。
如果您安装了 DirectShow SDK
,您将安装一个非常实用的工具,称为 GraphEdit
。要检查您的 DirectShow
安装是否支持某种文件类型,只需将该文件类型的一个样本拖入 GraphEdit
。下图显示了我将一个 mp4
文件拖入其中时创建的数据流图。
如果 GraphEdit
能够找到所有用于处理和渲染此类文件的 DirectShow
编解码器和过滤器,则会创建该图。如果您没有安装所需的编解码器和过滤器,则不会创建任何图,并且 GraphEdit 会弹出错误消息。
讨论 DirectShow
的原因是因为 mciSendString
实际上就是利用 DirectShow
来播放视频和音频文件的。
如果您的文件类型在 GraphEdit
中无法正常工作,那么它也很可能在 mciSendString
中无法正常工作。
在这篇文章中,我将一些 animated gif
和 mp4
文件包含在 zip 源分发包中。但是您也可以使用从您的 IOS
、Andriod
或 Windows Phone
设备录制的 mp4
文件。只需将这些文件复制到 MCIDemo.exe 所在的当前目录中。请注意,您需要 ffd Video Decoder
,可从 http://www.windows7codecs.com/ 获取,才能播放 mp4
文件。
使用代码
下面的代码来自 MciPlayer
类的定义。MCISendString(string Pcommand)
是 winmm.dll mciSendString() API
的一个非常简单的封装。
[DllImport("winmm.dll")]
private static extern int mciSendString(String strCommand, StringBuilder strReturn, int iReturnLength, IntPtr hwndCallback);
[DllImport("winmm.dll")]
public static extern int mciGetErrorString(int errCode, StringBuilder errMsg, int buflen);
public string MCISendString(string Pcommand)
{
StringBuilder sb = new StringBuilder(1024);
int ret = mciSendString(Pcommand, sb, sb.Capacity, IntPtr.Zero);
if (ret == 0)
return sb.ToString();
else
{
mciGetErrorString(ret, sb, sb.Capacity);
return sb.ToString();
}
}
<DllImport("winmm.dll")> _
Private Shared Function mciSendString(strCommand As [String], strReturn As StringBuilder, iReturnLength As Integer, hwndCallback As IntPtr) As Integer
End Function
<DllImport("winmm.dll")> _
Public Shared Function mciGetErrorString(errCode As Integer, errMsg As StringBuilder, buflen As Integer) As Integer
End Function
Public Function MCISendString(Pcommand As String) As String
Dim sb As New StringBuilder(1024)
' Pcommand = Pcommand.Replace("@@", Alias);
Dim ret As Integer = mciSendString(Pcommand, sb, sb.Capacity, IntPtr.Zero)
If ret = 0 Then
Return sb.ToString()
Else
mciGetErrorString(ret, sb, sb.Capacity)
Return sb.ToString()
End If
End Function
要打开一个视频文件,我们创建一个 MciPlayer
类的实例。然后我们构建要发送的字符串。在这种情况下,我们使用 open 命令字符串。
"open mpegvideo!\"" + Application.StartupPath + "\\test-mpeg_512kb.mp4\" alias " + _index
如果我们用一些示例值替换所有变量。
"open mpegvideo!\"c:\\test-mpeg_512kb.mp4\" alias 1"
我们以别名 1 打开文件 c:\test-mpeg_512kb.mp4。请注意,我们在文件名之前附加了 mpegvideo!,这是为了告诉 MCI 系统该文件是 digitalvideo
文件类型。
string Pcommand = "";
MciPlayer m = new MciPlayer();
Pcommand = "open mpegvideo!\"" + Application.StartupPath + "\\test-mpeg_512kb.mp4\" alias " + _index;
m.MCISendString(Pcommand);
Dim Pcommand As String = ""
Dim m As New MciPlayer()
Pcommand = "open mpegvideo!""" & filename & """ alias " & _index
m.MCISendString(Pcommand)
在我们打开(加载)与别名关联的文件后,我们随后可以使用该别名代替文件名来引用媒体文件。请注意,每个已打开的文件都必须具有唯一的别名。但是,我们可以用不同的别名打开同一个文件,只要每个别名始终只指向一个特定的文件。
MCISendString()
命令也可以用于运行查询。例如,命令字符串 status 0 mode
查询别名为 0 的媒体文件的播放状态。MCISendString()
对此命令字符串的返回可以是 "playing"、"paused" 或 "stopped"。
命令字符串
所有 MCI 命令请参考 多媒体命令字符串。但并非所有不同的命令都适用于视频文件,并且对于每个命令,您的 MCI 系统可能只支持其中的一小部分操作。
我将命令按以下常见任务进行分类:
- 加载视频文件
- 显示视频文件
- 播放视频文件
- 跟踪视频文件
- 获取视频文件信息
- set 命令
- 卸载视频文件
加载视频文件
我们使用 open
命令将视频文件加载到 MCI 系统中。使用 open
命令处理数字视频的最简单方法是加载媒体文件并为其分配一个别名。
For example: open c:\mymedia.mpg alias 0.
MCI 系统将尝试理解文件类型并为其分配别名 0。但是,由于我们处理的是视频文件,我们可以通过在文件名后附加设备名称来帮助 MCI 系统。对于所有视频/音频类型,我们可以使用设备名称 mpegvideo。因此,对于上面的示例,我们将使用:
open mpegvideo!c:\mymedia.mpeg alias 0.
open
命令也可以用于加载文件并使用指定的父窗口进行显示渲染。
open mpegvideo!c:\mymedia.mpg.mpg alias 0 parent 10024 style child;
在上面的命令中,我们使用别名 0 加载文件 c:\mymedia.mpg.mpg,并将窗口句柄 10024 指定为显示窗口的父窗口,该显示窗口将具有作为父窗口子窗口的样式。
当命令成功运行时,mciSendString 将返回 0 (成功)。任何其他返回值都是错误代码。要获取错误代码的描述,我们使用 mciGetErrorString()
,传入错误代码。例如,错误代码 275 表示“文件未找到”。错误代码列表请参考 错误消息 (多媒体 MCI 控制)。
显示视频文件
为了渲染视频文件,我们可以使用 open
命令为其指定父窗口句柄。如果未指定默认父窗口句柄,MCI 系统将在文件首次播放时创建一个默认窗口。默认情况下,这是一个弹出式可调整大小的窗口,大小会根据媒体文件的视频分辨率进行调整。如果视频分辨率是 VGA(640x480),默认窗口的客户区域将是 640x480 像素。该窗口可调整大小,并且显示的视频会缩放以保持其纵横比。
在我们的演示中,我们将面板的窗口句柄指定为父窗口,创建的显示窗口将是面板的子窗口。
Panel1
用作显示最多 4 个视频的主区域。Panel1
是 640x480,我将其分为 4 个子区域:左上、右上、左下、右下。显示在左上角的视频将分配别名 0,右上角为 1,左下角为 2,右下角为 3。
close 0
open mpegvideo!"test-mpeg_512kb.mp4" alias 0 parent 655830 style child
where 0 source
-->0 0 320 240
put 0 window at 10 80 300 225
上面的脚本显示了一系列用于将视频文件显示和渲染到我们面板的一个子区域的命令。
我们首先 close
别名 0 所引用的所有资源,因为我们要使用此别名来标识我们要加载的文件。
close 0
然后我们使用 open
命令加载文件,指定面板作为父窗口,并使用 0 作为别名。
open mpegvideo!"test-mpeg_512kb.mp4" alias 0 parent 655830 style child
然后我们获取源视频的视频分辨率。这是为了计算纵横比,以便在显示中保持视频的纵横比。在这种情况下,视频分辨率是 320x240。
where 0 source
-->0 0 320 240
我们使用我们的函数 getAspectRatioSize()
来获取视频所需的显示大小,在这种情况下,计算出的区域是 300x225。并将显示窗口定位在坐标 10,80。所有这些值都相对于由窗口句柄标识的窗口,在本例中是 panel1
。下一个 put 命令指示 MCI 系统使用此显示区域。
put 0 window at 10 80 300 225
播放视频
要播放视频,请使用 play
命令。有选项:
循环播放别名为 0 的文件,在达到末尾时从头开始。
play 0 repeat
从位置 0 开始播放别名为 0 的文件。
play 0 from 0
从暂停的当前位置播放别名为 0 的文件。
play 0
暂停别名为 0 的文件的播放。
pause 0
恢复播放别名为 0 的文件。
resume 0
停止播放别名为 0 的文件。
stop 0
将别名为 0 的文件的当前帧重新定位到距离开始位置 1000ms 的位置。
seek 0 1000
请注意,重新定位当前帧可能会暂停视频。我们可能需要使用 play 命令恢复播放。
有关更多选项,请参阅 MSDN 文档 多媒体命令字符串。
跟踪视频
要跟踪视频播放,我们需要有关播放状态和当前显示帧的信息。为此,我们使用非常通用的 status
命令。
要找出别名为 0 的文件的播放模式。这将返回 "playing"、"pause" 和 "stopped" 状态。
status 0 mode
获取别名为 0 的文件的当前播放帧。这将返回一个数字,表示帧号或经过的时间(毫秒)。
status 0 position
获取别名为 0 的视频文件的长度。同样,它以帧数或时间(毫秒)为单位。
status 0 length
为了跟踪当前视频播放,我们可以计算位置/长度的比率。在我们的演示中,我们使用一个定时器,每 500 毫秒调用上述两个命令来跟踪视频播放。位置/长度的比率计算为百分比,该百分比用于定位每个正在运行的视频的跟踪条。
获取视频信息
要获取与显示窗口相关的信息,我们使用 where
命令。
要获取别名为 0 的视频文件的视频分辨率。where
命令的返回值是一个由 x1 y1 width height
定义的矩形,例如 0 0 200 300
表示一个位于坐标 0,0,大小为 200x300 的矩形。将下面的字符串发送到 MCISendString()
,如果视频宽度为 200,高度为 300,则返回的字符串将是 0 0 200 300
。
where 0 source
获取显示别名为 0 的视频的矩形。
where 0 destination
对于大多数其他信息,我们使用 status
命令。
获取当前显示别名为 0 的视频的窗口句柄。如果您想截屏显示,这将非常有用。
status 0 window handle
了解文件别名为 0 的音频轨播放的响度。
status 0 volume
然后,我们可以使用 setaudio
命令来调整音量。setaudio
音量的取值范围是 0 到 1000。
setaudio 0 volume to 500
要获取更多技术信息,我们使用 info 命令。
要获取文件别名为 0 的产品(设备)信息。我已经测试过文件类型:gif、mpg、mp4、wav、mp3,在 open 命令中标记为 mpegvideo,在我的系统上,info <alias> product
命令总是返回 "DirectShow
",所以我怀疑 DirectShow
是 winmm.dll
使用的底层技术。
info 0 product
set 命令
set 命令可用于更改音频和视频播放。
设置音频左/右/全部开/关。下面的命令打开别名为 0 的媒体文件的左声道。
set 0 audio left on
设置视频播放速度。下面的命令将播放速度设置为正常播放速度的 50%。
set 0 speed 500
设置时间格式。大多数视频文件的默认时间格式是毫秒。但是,时间格式可以设置为帧/毫秒。下面的命令将时间格式设置为帧。
set 0 time format frames
卸载视频文件
使用 close
命令。
关闭别名为 0 的文件。
close 0
或者关闭所有已加载的文件。
close all
close
命令成功运行后,文件将从 MCI 系统中卸载,我们可以重新使用之前使用的别名。在我们的演示中,我在面板的 4 个象限中分别使用了别名 0、1、2 和 3。
如果这些别名标识的任何文件已被关闭,我会重用这些别名来加载下一个视频文件,否则我只会关闭加载的最早的文件并回收其别名来加载新文件。
演示
启动时,左上角的列表框列出了当前目录中的所有媒体文件。目前,我列出了所有 ".mpg"、".gif"、".mp4"
文件。对于您的系统,您可能希望使用 DirectShow
GraphEdit
来找出您的系统支持的所有视频和音频文件类型,并编辑窗体加载处理程序以加载这些文件。
从列表框中选择一个文件,然后单击“循环播放”按钮。所选文件将首先加载到默认窗口中(这发生得很快,可能不明显),然后显示将粘贴到右侧面板中的第一个可用子区域。
面板中的每个子区域都由以下组成:
- Label
- 音频开关复选框
- 跟踪条
- 暂停/播放按钮
- 关闭按钮
- 视频显示
标签的文本显示这些信息:
- 别名;
- 视频长度;
- 文件名。
如果在此标签上进行鼠标左键双击,视频显示将弹出到其查看窗口。查看窗口可调整大小,因此您可以调整它以更好地观看视频。再次双击标签将使视频显示返回到面板中的子区域。您也可以关闭查看窗口,它也将返回到面板中。
查看窗口的功能:
- 全屏切换
- 音频开关
- 前进/后退滚动
- 屏幕截图
- 暂停/恢复
当查看窗口处于活动状态时,这些是激活功能的快捷方式:
全屏切换:F11
音频开关:鼠标左键单击
前进/后退滚动:左箭头键后退,右箭头键前进。每次释放按键时,会跳过 2% 的帧。
屏幕截图:鼠标右键单击
暂停/恢复:按空格键。
现在回到面板控件。
“音频开启?”复选框用于切换显示的音频开启/关闭。
请注意,当您单击这些 UI 组件时,Command Log 文本框会使用发送到 MCI 系统的每个 MCI 命令进行更新。这是一个很好的学习工具,可以了解用于执行每项任务的命令。您也可以直接在 Current Command 文本框中键入一些 MCI 命令,每行一个命令,然后单击 Send String 按钮。
底部有一个标签,用于显示已执行命令的状态。如果状态标签中没有任何内容,则命令执行正确,否则将显示描述性错误字符串。
要重新定位视频播放,请拖动跟踪条。视频显示将显示所选位置的帧。您需要单击暂停/播放按钮才能从选定位置继续观看视频。
要卸载视频,请单击“关闭”按钮。
视频叠加
在版本 3 中,我在视频显示窗口上添加了叠加。此功能是通过 FormDisplay
和 FormTransparent
的耦合实现的。
对于 FormDisplay
,我们添加了以下代码:
public FormTransparent fTp=new FormTransparent();
fTp._frmdisplay = this;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.TopMost = true;
private void FormDisplay_VisibleChanged(object sender, EventArgs e)
{
if(fTp!=null)
fTp.Visible = this.Visible;
}
private void FormDisplay_Activated(object sender, EventArgs e)
{
if (fTp != null)
{
fTp.Visible = true;
fTp.BringToFront();
fTp.Focus();
}
}
Public fTp As New FormTransparent()
fTp._frmdisplay = Me
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
Me.TopMost = True
Private Sub FormDisplay_VisibleChanged(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.VisibleChanged
If fTp IsNot Nothing Then
fTp.Visible = Me.Visible
End If
End Sub
Private Sub FormDisplay_Activated(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Activated
If fTp IsNot Nothing Then
fTp.Visible = True
fTp.BringToFront()
fTp.Focus()
End If
End Sub
对于 FormTransparent
:
private Image logo;
public string statusText = "";
public string subtitleText = "";
public FormDisplay _frmdisplay=null;
this.TopMost = true;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
this.TransparencyKey = System.Drawing.Color.White;
private void FormTransparent_LocationChanged(object sender, EventArgs e)
{
if (!isFirstActivated) return;
Rectangle rect = ScreenCapture.GetClientRect(this.Handle);
if (_frmdisplay != null)
// _frmdisplay.Location = this.Location;
_frmdisplay.Location = new Point(this.Left + rect.Left, this.Top + rect.Top);
}
private void FormTransparent_Resize(object sender, EventArgs e)
{
if (!isFirstActivated) return;
if (_frmdisplay != null)
{
System.Diagnostics.Debug.WriteLine("fTp resize " + _nextnum++);
Rectangle rect = ScreenCapture.GetClientRect(this.Handle);
_frmdisplay.Size = new Size(rect.Width, rect.Height);
_frmdisplay.Refresh();
}
}
private void FormTransparent_KeyPress(object sender, KeyPressEventArgs e)
{
_frmdisplay.FormDisplay_KeyPress(sender, e);
}
private void FormTransparent_KeyUp(object sender, KeyEventArgs e)
{
_frmdisplay.FormDisplay_KeyUp(sender, e);
}
private void FormTransparent_FormClosing(object sender, FormClosingEventArgs e)
{
_frmdisplay.FormDisplay_FormClosing(sender, e);
}
private void FormTransparent_VisibleChanged(object sender, EventArgs e)
{
if (this.Visible)
{
this.BringToFront();
}
}
private void FormTransparent_Activated(object sender, EventArgs e)
{
if (_frmdisplay == null) return;
this.Text = _frmdisplay.Text;
_frmdisplay.BringToFront();
this.BringToFront();
if (isFirstActivated) return;
Rectangle rect = ScreenCapture.GetClientRect(this.Handle);
//Set the location and size of the transparent window
//such the the display window will
//occupy exactly the client area of the transparent window
this.Top = _frmdisplay.Top - rect.Top;
this.Left = _frmdisplay.Left - rect.Left;
this.Width = _frmdisplay.Width + (rect.Left *2);
this.Height = rect.Top + _frmdisplay.Height + (rect.Left);
//_nextnum++;
//System.Diagnostics.Debug.WriteLine("frm ftp activated " + _nextnum +
// _frmdisplay.Location.ToString() + "," + _frmdisplay.Size.ToString() + " " +
// this.Location.ToString() + "," + this.Size.ToString());
isFirstActivated = true;
}
private void FormTransparent_Paint(object sender, PaintEventArgs e)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(255, 255, 254)),
new Rectangle(0, 0, this.Width , this.Height ));
Rectangle rect = ScreenCapture.GetClientRect(this.Handle);
//e.Graphics.DrawRectangle(Pens.Red, new Rectangle(0, 0, rect.Width-1 , rect.Height-1));
if (statusText != "")
{
StringFormat format = new StringFormat();
format.LineAlignment = StringAlignment.Center;
format.Alignment = StringAlignment.Far;
e.Graphics.DrawString(statusText, new Font("Courier", 10), Brushes.Yellow ,new RectangleF(0,0,rect.Width,20),format);
}
if (subtitleText != "")
{
StringFormat format = new StringFormat();
format.LineAlignment = StringAlignment.Center;
format.Alignment = StringAlignment.Center;
e.Graphics.DrawString(subtitleText, new Font("Courier", 10), Brushes.Yellow, new RectangleF(0, rect.Height-50, rect.Width, 40), format);
}
if (logo != null)
e.Graphics.DrawImage(logo, new Rectangle(0, 0, logo.Width , logo.Height), new Rectangle(0, 0, logo.Width, logo.Height), GraphicsUnit.Pixel);
}
Public _frmdisplay As FormDisplay = Nothing
Me.TopMost = True
Me.TransparencyKey = System.Drawing.Color.White
Private Sub FormTransparent_Activated(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Activated
If _frmdisplay Is Nothing Then
Return
End If
Me.Text = _frmdisplay.Text
_frmdisplay.BringToFront()
Me.BringToFront()
If isFirstActivated Then
Return
End If
Dim rect As Rectangle = ScreenCapture.GetClientRect(Me.Handle)
'Set the location and size of the transparent window
'such the the display window will
'occupy exactly the client area of the transparent window
Me.Top = _frmdisplay.Top - rect.Top
Me.Left = _frmdisplay.Left - rect.Left
Me.Width = _frmdisplay.Width + (rect.Left * 2)
Me.Height = rect.Top + _frmdisplay.Height + (rect.Left)
'Minimum width correction
If (Me.Width) <= 132 Then
_frmdisplay.Width = 132 - (rect.Left * 2)
_frmdisplay.Refresh()
End If
isFirstActivated = True
End Sub
Private Sub FormTransparent_LocationChanged(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.LocationChanged
If Not isFirstActivated Then
Return
End If
Dim rect As Rectangle = ScreenCapture.GetClientRect(Me.Handle)
If _frmdisplay IsNot Nothing Then
' _frmdisplay.Location = this.Location;
_frmdisplay.Location = New Point(Me.Left + rect.Left, Me.Top + rect.Top)
End If
End Sub
Private Sub FormTransparent_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Resize
If Not isFirstActivated Then
Return
End If
If _frmdisplay IsNot Nothing Then
System.Diagnostics.Debug.WriteLine("fTp resize " & System.Math.Max(System.Threading.Interlocked.Increment(_nextnum), _nextnum - 1))
Dim rect As Rectangle = ScreenCapture.GetClientRect(Me.Handle)
_frmdisplay.Size = New Size(rect.Width, rect.Height)
_frmdisplay.Refresh()
End If
End Sub
Private Sub FormTransparent_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles MyBase.KeyPress
_frmdisplay.FormDisplay_KeyPress(sender, e)
End Sub
Private Sub FormTransparent_KeyUp(ByVal sender As Object, ByVal e As KeyEventArgs) Handles MyBase.KeyUp
_frmdisplay.FormDisplay_KeyUp(sender, e)
End Sub
Private Sub FormTransparent_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles MyBase.Paint
e.Graphics.FillRectangle(New SolidBrush(Color.FromArgb(255, 255, 254)), _
New Rectangle(0, 0, Me.Width, Me.Height))
Dim rect As Rectangle = ScreenCapture.GetClientRect(Me.Handle)
'e.Graphics.DrawRectangle(Pens.Red, new Rectangle(0, 0, rect.Width-1 , rect.Height-1));
If statusText <> "" Then
Dim format As New StringFormat()
format.LineAlignment = StringAlignment.Center
format.Alignment = StringAlignment.Far
e.Graphics.DrawString(statusText, New Font("Courier", 10), Brushes.Yellow, New RectangleF(0, 0, rect.Width, 20), format)
End If
If subtitleText <> "" Then
Dim format As New StringFormat()
format.LineAlignment = StringAlignment.Center
format.Alignment = StringAlignment.Center
e.Graphics.DrawString(subtitleText, New Font("Courier", 10), Brushes.Yellow, New RectangleF(0, rect.Height - 50, rect.Width, 40), format)
End If
If logo IsNot Nothing Then
e.Graphics.DrawImage(logo, New Rectangle(0, 0, logo.Width, logo.Height), New Rectangle(0, 0, logo.Width, logo.Height), GraphicsUnit.Pixel)
End If
End Sub
FrmTransparent 中的透明度是通过使用窗体的 TransparencyKey
属性来实现的。窗体上与 TransparencyKey
属性颜色相同的任何部分都不会被渲染,而是会渲染 z 顺序下方该部分的所有像素。由于 FrmTransparent 的大部分设置为 TransparencyKey 的颜色,因此它基本上是透明的,但徽标、状态文本和标题文本除外,它们的颜色不是该颜色。
this.TopMost = true;
this.TransparencyKey = System.Drawing.Color.FromArgb(255, 255, 254);
Me.TopMost = True
this.TransparencyKey = System.Drawing.Color.FromArgb(255,255,254)
这样做的目的是使 FormTransparent
始终位于 FormDisplay
的 z 顺序之上,并且 FormDisplay
永远不会获得键盘焦点。FormDisplay
和 FormTransparent
中的这些事件处理程序可确保此要求。
- FormDisplay_VisibleChanged
- FormDisplay_Activated
- FormTransparent_VisibleChanged
- FormTransparent_Activated
由于 FormDisplay
现在具有 FormBorderStyle.none,因此我们无法使用鼠标重新定位或调整它的大小。我们依赖 FormTransparent
中的这些事件处理程序来完成此操作。
- FormTransparent_Resize
- FormTransparent_LocationChanged
此外,由于 FormDisplay
无法获得键盘焦点,因此我们无法使用某些为它定义的快捷方式,这些快捷方式需要键盘焦点或鼠标捕获。我们再次在 FormTransparent
的以下事件处理程序中处理这些问题。基本上,FormTransparent
充当 FormDisplay
的代理,将键盘活动重定向到调用 FormDisplay
中的原始事件处理程序。
- FormTransparent_KeyPress
- FormTransparent_KeyUp
最后,所有这些重定向的主要原因:添加状态、徽标和字幕。这通过 FormTransparent_Paint 事件处理程序完成。每次刷新 FormTransparent 时都会调用此 Paint 事件。
下面的变量用于 FormTransparent_Paint 处理程序,并在 timer1 事件处理程序中由 FrmMain 更新:
- statusText
- subtitleText
//update the trackbar to show video play position while video is playing
private void timer1_Tick(object sender, EventArgs e)
{
timer1.Enabled = false;
MciPlayer p = new MciPlayer();
string Pcommand = "";
for (int i = 0; i < 4; i++)
{
if (!availnum[i])
{
Pcommand = "status " + i + " mode";
string s1 = p.MCISendString(Pcommand);
//we remove the logging as it may be too frequent and it cluster up the logs
//LogPcommand(Pcommand);
//LogPcommand("--->" + s1);
if (s1 == "playing")
{
Pcommand = "status " + i + " position";
s1 = p.MCISendString(Pcommand);
//LogPcommand(Pcommand);
//LogPcommand("--->" + s1);
int ipos = int.Parse(s1);
Pcommand = "status " + i + " length";
string s2 = p.MCISendString(Pcommand);
//LogPcommand(Pcommand);
//LogPcommand("--->" + s1);
int ilen = int.Parse(s2);
int ipc = (int)((float)(ipos * 100) / (float)ilen);
trkMedia[i].Value = ipc;
trkMedia[i].Refresh();
string s3 = "";
s3 = GetSubTitleText(i, ipos);
if(frmDisplay[i]!=null)
if (frmDisplay[i].fTp != null)
{
frmDisplay[i].fTp.statusText = s1 + " " + s2;
frmDisplay[i].fTp.subtitleText = s3;
frmDisplay[i].fTp.Refresh();
}
}
}
}
timer1.Enabled = true;
}
'update the trackbar to show video play position while video is playing
Private Sub timer1_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles timer1.Tick
timer1.Enabled = False
Dim p As New MciPlayer()
Dim Pcommand As String = ""
For i As Integer = 0 To 3
If Not availnum(i) Then
Pcommand = "status " & i & " mode"
Dim s1 As String = p.mciSendString(Pcommand)
'we remove the logging as it may be too frequent and it cluster up the logs
'LogPcommand(Pcommand);
'LogPcommand("--->" + s1);
If s1 = "playing" Then
Pcommand = "status " & i & " position"
s1 = p.mciSendString(Pcommand)
'LogPcommand(Pcommand);
'LogPcommand("--->" + s1);
Dim ipos As Integer = Integer.Parse(s1)
Pcommand = "status " & i & " length"
Dim s2 As String = p.mciSendString(Pcommand)
'LogPcommand(Pcommand);
'LogPcommand("--->" + s1);
Dim ilen As Integer = Integer.Parse(s2)
Dim ipc As Integer = CInt(Math.Truncate(CSng(ipos * 100) / CSng(ilen)))
trkMedia(i).Value = ipc
trkMedia(i).Refresh()
'this.mouse_down = saved_mousedown;
'update with caption file
Dim s3 As String = ""
s3 = GetSubTitleText(i, ipos)
If frmDisplay(i) IsNot Nothing Then
If frmDisplay(i).fTp IsNot Nothing Then
frmDisplay(i).fTp.statusText = s1 & " " & s2
frmDisplay(i).fTp.subtitleText = s3
frmDisplay(i).fTp.Refresh()
End If
End If
End If
End If
Next
timer1.Enabled = True
End Sub
Subtitle
视频的字幕是通过使用一个与视频文件同名且位于同一目录下的文本文件实现的。使用的扩展名是“.cap.txt”。该文件保存为 Unicode 文本文件,以便我们可以输入拉丁语以外的其他语言的文本。
格式非常简单。
1137@#@<taj mahal, india>
2358@#@<guy making a call>
3533@#@<street view>
文本文件以“\r\n”分隔。每行的内容由“@#@”分隔。此分隔符左侧的部分显示字幕文本右侧文本应出现的时间(毫秒)或帧号。
FormMain
中的计时器使用下面的 GetSubTitleText()
函数根据当前时间/帧查找要显示的字幕。
private string GetSubTitleText(int alias, int pos)
{
if (subtitle[alias] == "") return "";
string[] subtitles=subtitle[alias].Split(new string[] { "\r\n" },StringSplitOptions.None);
for (int i = subtitles.Length-1; i >=0; i--)
{
var subtitle1=subtitles[i].Split(new string[] { "@#@" },StringSplitOptions.None);
if (subtitle1.Length<2) continue;
try
{
int subpos = int.Parse(subtitle1[0]);
if ( subpos<=pos)
{
if (subtitle1.Length >=1)
return (subtitle1[1]);
else
return "";
}
}
catch { return ""; }
}
return "";
}
Private Function GetSubTitleText(ByVal [alias] As Integer, ByVal pos As Integer) As String
If subtitle([alias]) = "" Then
Return ""
End If
Dim subtitles As String() = subtitle([alias]).Split(New String() {vbCr & vbLf}, StringSplitOptions.None)
For i As Integer = subtitles.Length - 1 To 0 Step -1
Dim subtitle1 = subtitles(i).Split(New String() {"@#@"}, StringSplitOptions.None)
If subtitle1.Length < 2 Then
Continue For
End If
Try
Dim subpos As Integer = Integer.Parse(subtitle1(0))
If subpos <= pos Then
If subtitle1.Length >= 1 Then
Return (subtitle1(1))
Else
Return ""
End If
End If
Catch
Return ""
End Try
Next
Return ""
End Function
关注点
Command Log 和 Current Command 执行功能对于您了解如何使用各种 MCI 命令非常有帮助。事实上,本文中用于验证想法的大多数测试都通过这两个功能完成。
随着版本 2 的发布,我们拥有了一个几乎全功能的查看窗口。通过全屏模式,感觉就像在商业媒体播放器中播放一样。实际上,我们做得更好,我们可以同时观看一个视频,并在面板中保留另外三个,随时准备好进入全屏视图。
通过版本 3,您可以为视频添加字幕。
玩得开心!
历史
2014年6月12日:视频的乐趣 V1
2014年6月13日:视频的乐趣 V2
使演示更用户友好。
2014年6月16日:视频的乐趣 V3a
添加了透明叠加层以显示徽标、状态和字幕,修复了与鼠标捕获相关的某些错误。
添加了额外的 VB.Net 代码选项卡部分和 VB.Net 源下载。
2016年10月5日:视频的乐趣 V3b
将透明键设置为 Color(255,255,254) 而不是白色。这将禁用鼠标事件穿透透明窗体。鼠标事件将在透明窗体上处理。结果是调整和移动透明窗体时鼠标处理更稳定。