IIS 平滑流式处理

用上下文数据增强 Silverlight 视频体验

Jit Ghosh

下载代码示例

在基于 Web 的高清数字视频传输中实现稳定的观看体验有两项基本要求。首先,视频提供程序需要在网络上支持较高的视频传输比特率。其次,客户端计算机需要支持连续的处理能力以全分辨率对视频解码。

而实际情况是,随着时间的推移,家庭联网计算机的网络带宽会出现明显波动,并且在世界上的某些地区,高带宽费用高昂,或只向部分用户提供。与此同时,根据任意给定时刻 CPU 的负载,客户端计算机的处理能力也会有变化。结果就是,在播放器等待缓冲足够的数据以便显示下一组视频帧,或等待 CPU 周期对这些帧进行解码时,视频会断断续续或出现定格,从而使用户的观看体验大打折扣。

自适应流式处理是一种视频传输模式,可流畅地传送视频内容并解决解码问题。使用自适应流式处理,视频内容在一定比特率范围内进行编码,并通过专用的流式处理服务器提供。自适应流式处理播放器一直监视客户端计算机上的各种资源利用率指标,使用这些信息计算相应的比特率。在给定的现有资源约束下,客户端能以此比特率最高效地解码和显示。

播放器请求以当前相应比特率编码的视频数据块,流式处理服务器用以此比特率编码的视频源中的内容进行响应。结果是,当资源状况不佳时,播放器可继续显示视频而不会有任何明显干扰,只是整体分辨率会略有降低,直到资源状况的提高或进一步降低导致请求不同的比特率。

若要在播放器和服务器之间实现这种连续的协作,要求流式处理服务器和实现播放器的客户端运行时中都存在专门的处理逻辑实现。Internet Information Server (IIS) 平滑流式处理是 Microsoft 推出的通过 HTTP 进行自适应流式处理的服务器端实现。客户端实现作为 Microsoft Silverlight 的扩展提供。

IIS 平滑流式处理播放器开发工具包是一个 Silverlight 库,它使应用程序能够使用通过 IIS 平滑流式处理功能流式处理的内容。该工具包还提供一个功能丰富的 API,用于提供对平滑流式处理逻辑各方面的编程访问。

在本文中,我将逐步向您介绍平滑流式处理的基础知识,解释如何使用 IIS 平滑流式处理播放器开发工具包创建丰富的用户视频体验。具体而言,我将介绍如何使用播放器开发工具包使用流,进一步检查流和轨道的客户端数据模型。我将向您演示如何使用额外的数据流,如隐藏字幕和动画,以及将外部数据流与现有影片合并。您将了解如何在影片内安排如广告这样的外部剪辑,处理变化的播放速度以及生成造就强大编辑方案的复合清单。

平滑流式处理工作方式

可以使用 Expression Encoder 3.0 提供的配置文件之一对视频编码以便进行平滑流式处理。对于一个源视频文件,会在目标文件夹中创建几个文件。图 1 显示了为一个名为 FighterPilot.wmv 的源视频创建的文件。


图 1 Expression Encoder 为平滑流式处理生成的文件

每个带有 .ismv 扩展名的文件都包含以特定比特率编码的视频。例如,FighterPilot_331.ismv 包含以 331 Kbps 比特率编码的视频,而 FighterPilot_2056.ismv 包含以 2 Mbps 编码的视频。

对于每种比特率,视频内容都拆分为两秒的片段,.ismv 文件以一种名为受保护互操作文件格式 (PIFF) 的文件格式存储这些片段。请注意,可以在具有 .isma 扩展名的类似文件中编码附加音轨(或只是音频,在影片为纯音频时)。

获得平滑流式处理环境

若要试用本文中讨论的示例,需要在开发计算机上准备平滑流式处理环境。

服务器端的要求非常简单:您需要使用 Microsoft Web Platform Installer 从 iis.net/media 下载并安装 IIS Media Services 3.0 for IIS7。

需要 Microsoft Expression Encoder 3.0 的一份副本来为平滑流式处理准备视频。虽然有免费的 Expression Encoder 3.0 评估版,但该版本不包含平滑流式处理支持。您需要 Expression Encoder 的许可安装来创建自己的视频。

有关准备环境的更多详细信息,请访问 learn.iis.net/page.aspx/558/smooth-streaming-for-iis-70---getting-started

FighterPilot.ism 文件是一个服务器清单,其结构采用的是同步多媒体整合语言 (SMIL) 格式,并且包含质量等级和比特率对 .ismv 和 .isma 文件的映射。服务器清单中的此映射由服务器用来访问正确的磁盘文件,以便在响应客户端请求前创建以正确的比特率编码的下一段内容。图 2 显示了一段服务器清单文件的摘录。

图 2 示例服务器清单

<smil xmlns="http://www.w3.org/2001/SMIL20/Language">
  <head>
    <meta name="clientManifestRelativePath"
      content="FighterPilot.ismc" />
  </head>
  <body>
    <switch>
      <video src="FighterPilot_2962.ismv"
        systemBitrate="2962000">
        <param name="trackID"
          value="2" valuetype="data" />
      </video>
      <video src="FighterPilot_2056.ismv"
        systemBitrate="2056000">
        <param name="trackID"
          value="2" valuetype="data" />
      </video>
      ...
      <audio src="FighterPilot_2962.ismv"
        systemBitrate="64000">
        <param name="trackID"
          value="1" valuetype="data" />
      </audio>
    </switch>
  </body>
</smil>

服务器清单还包含到客户端清单文件的映射(由扩展名 .ismc 标识),在我的示例中为 FighterPilot.ismc。客户端清单包含 Silverlight 客户端访问不同媒体和数据流所需的全部信息,以及关于这些流的元数据,如质量等级、可用比特率、定时信息、编解码器初始化数据等。客户端逻辑将使用此元数据对数据段采样和解码,并根据主导的本地条件请求比特率切换。

运行时,影片以从服务器请求客户端清单的客户端开始。客户端收到清单后,就会检查可用比特率并从最低可用比特率开始请求内容片段。作为响应,服务器从磁盘文件读取以该比特率(使用服务器清单中的映射)编码的数据,从而准备并发送片段。然后,内容将显示在客户端。

客户端逐步请求资源监视逻辑所允许的更高比特率,最终达到由主导资源状况决定的所允许的最高比特率。这一交换持续进行,直到客户端监视逻辑感测到的资源状况变化导致需要不同的更低比特率。后续客户端请求针对的是以新比特率编码的媒体,服务器再次做出相应的响应。这一行为持续进行到影片完成或停止。

使用 Silverlight 进行平滑流式处理

在 Silverlight 中播放视频非常简单。从根本上说,您需要做的全部工作就是在您的 XAML 文件中添加 MediaElement 类型的一个实例,设置适当的属性以控制 MediaElement 行为,以及确保 MediaElement.Source 属性指向有效的媒体源 URI。例如,在 Silverlight 页面启动后,以下 XAML 将自动在 640x360 的矩形中播放 FighterPilot.wmv 视频:

<MediaElement AutoPlay="True" 
  Source="http://localhost/Media/FighterPilot.wmv" 
  Width="640" Height="360" />

System.Windows.Controls.MediaElement 类型还公开一个 API,使用它可以在代码中控制播放行为并生成一个包含如 Play、Pause、Seek 等标准控件的全功能播放器。对于渐进下载或 HTTP 流式处理的媒体,只要使用的是 Silverlight 运行时内置支持的容器格式和编码方式,这种方法就非常有效。

如果使用 Silverlight 不直接支持的文件格式或编解码器该怎么办?MediaStreamSource (MSS) 类型启用了一种可扩展性机制,允许您通过在 Silverlight 媒体管道中引入自己的自定义分析器和解码器控制媒体文件分析和解码过程。为此,您需要实现一个扩展抽象 System.Windows.Media.MediaStreamSource 的具体类型,然后使用 MediaElement.SetSource 方法将它的一个实例传递给 MediaElement。

MSS 实现将需要处理媒体使用过程中除实际呈现之外的每个方面:从接收远程位置的媒体流到分析容器和相关元数据,再到采取各个音频和视频样本并将它们传入 MediaElement 以进行呈现。

因为解码平滑流式处理所需的逻辑没有内置到 Silverlight 中,所以第一个版本的平滑流式处理(IIS Media Services 2.0 的一部分)附带了一个自定义 MSS 实现,该实现处理所有通信、分析和采样逻辑,还实现计算机和网络状态监视功能。

对于大多数情况,此方法对平滑流式处理非常有效,但也有一些缺点。MSS 实质上是一个黑盒,因为它直接公开的唯一 API 的用途是促进它本身与 MediaElement 之间的原始音频和视频样本的交换。Silverlight 开发人员在操作中没有与 MSS 交互的直接方法。如果所使用的内容有其他数据,如嵌入文本、动画或辅助相机角度,或者如果流式处理解决方案允许对流进行更精细的控制(如可变播放速度),则您不能通过编程以结构化方式访问这些附加数据,因为您只能与 MediaElement 一直公开的固定 API 集交互。

这对平滑流式处理提出了一个挑战。您在本文后面将看到,平滑流式处理清单和传输/文件格式从可承载的附加内容和元数据角度而言十分丰富,而通过 MSS 方法不能处理这些信息。您需要一个 Silverlight API,通过它提供对平滑流式处理解决方案的更多控制和访问。

IIS 平滑流式处理播放器开发工具包

这种情况使我转向了 IIS 平滑流式处理播放器开发工具包。此播放器开发工具包只包含一个名为 Microsoft.Web.Media.SmoothStreaming.dll 的程序集。其核心是一个名为 Microsoft.Web.Media.SmoothStreaming.SmoothStreamingMediaElement (SSME) 的类型。在代码中使用 SSME 与使用常规 MediaElement 的方式几乎完全相同:

<UserControl x:Class="SSPlayer.Page"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:ss="clr-namespace:Microsoft.Web.Media.SmoothStreaming;assembly=Microsoft.Web.Media.SmoothStreaming">
  <Grid x:Name="LayoutRoot" Background="White">
    <ss:SmoothStreamingMediaElement AutoPlay="True" 
      Width="640" Height="360"
      SmoothStreamingSource="http://localhost/SmoothStreaming/Media/FighterPilot/FighterPilot.ism/manifest"/>
  </Grid>
</UserControl>

SmoothStreamingSource 属性将 SSME 指向一个有效的平滑流式处理影片。通常,SSME API 是 MediaElement API 的超集;此属性是两者之间的几处不同点之一。SSME 像 MediaElement 一样公开 Source 属性,但 SSME 还公开 SmoothStreamingSource 属性以附加到平滑流。如果要使制作的播放器既能使用平滑流又能使用 MediaElement 以往支持的其他格式,您可以放心使用 SSME,但很可能需要编写一些代码来设置正确的属性以附加到媒体源。类似如下输出:

private void SetMediaSource(string MediaSourceUri, 
  SmoothStreamingMediaElement ssme) {

  if (MediaSourceUri.Contains(".ism"))
    ssme.SmoothStreamingSource = new Uri(MediaSourceUri); 
  else
    ssme.Source = new Uri(MediaSourceUri); 
}

要注意的其他主要区别是 SSME 不公开接受 MediaStreamSource 类型的 SetSource 重载。如果需要使用自定义 MSS,应通过 MediaElement 进行。 

流和轨道

平滑流式处理客户端清单包含关于影片的丰富元数据,可用于以编程方式访问播放器应用程序中的元数据。SSME 通过一个明确定义的 API 以流和每个流中的轨道这样的排列公开此元数据的各个部分。

流表示特定类型轨道的所有元数据:视频、音频、文本、广告等等。流还充当基础类型相同的多个轨道的容器。在客户端清单(请参见图 3)中,每个 StreamIndex 条目表示一个流。一个影片中可以有多个流,如多个 StreamIndex 条目所示。还可以有相同类型的多个流。在这种情况下,可以使用流名称来区分同一类型的多个实例。

图 3 客户端清单摘录

<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" 
  Duration="1456860000">
  <StreamIndex Type="video" Chunks="73" QualityLevels="8" 
    MaxWidth="1280" MaxHeight="720" 
    DisplayWidth="1280" DisplayHeight="720"
    Url="QualityLevels({bitrate})/Fragments(video={start time})">
    <QualityLevel Index="0" Bitrate="2962000" FourCC="WVC1" 
      MaxWidth="1280" MaxHeight="720"
      CodecPrivateData="250000010FD37E27F1678A27F859E80490825A645A64400000010E5A67F840" />
    <QualityLevel Index="1" Bitrate="2056000" FourCC="WVC1" 
      MaxWidth="992" MaxHeight="560" 
      CodecPrivateData="250000010FD37E1EF1178A1EF845E8049081BEBE7D7CC00000010E5A67F840" />
    ...
    <c n="0" d="20020000" />
    <c n="1" d="20020000" />
    ...
    <c n="71" d="20020000" />
    <c n="72" d="15010001" />
  </StreamIndex>
  <StreamIndex Type="audio" Index="0" FourCC="WMAP" 
    Chunks="73" QualityLevels="1" 
    Url="QualityLevels({bitrate})/Fragments(audio={start time})">
    <QualityLevel Bitrate="64000" SamplingRate="44100" Channels="2" 
      BitsPerSample="16" PacketSize="2973" AudioTag="354" 
      CodecPrivateData="1000030000000000000000000000E00042C0" />
    <c n="0" d="21246187" />
    <c n="1" d="19620819" />
    ...
    <c n="71" d="19504762" />
    <c n="72" d="14900906" />
  </StreamIndex>
  <StreamIndex Type="text" Name="ClosedCaptions" Subtype="CAPT" 
    TimeScale="10000000" ParentStreamIndex="video" 
    ManifestOutput="TRUE" QualityLevels="1" Chunks="2" 
    Url="QualityLevels({bitrate},{CustomAttributes})/Fragments(ClosedCaptions={start time})">
    <QualityLevel Index="0" Bitrate="1000" 
      CodecPrivateData="" FourCC=""/> 
    <c n="0" t="100000000">
      <f>...</f> 
    </c>
    <c n="1" t="150000000">
      <f>...</f>
    </c>
  </StreamIndex>
  ...
</SmoothStreamingMedia>

StreamInfo 类型表示 Silverlight 代码中的流。SSME 下载客户端清单后,会引发 SmoothStreamingMediaElement.ManifestReady 事件。这时 SmoothStreamingMediaElement.AvailableStreams 集合属性为客户端清单中的每个 StreamIndex 条目包含一个 StreamInfo 实例。

对于客户端清单中的给定视频流,视频轨道拆分为多个持续时间为两秒的片段,清单中的每个 c 元素表示该片段的元数据。在这种情况下,轨道中的片段是连续的,不中断地定义视频轨道的完整持续时间 — 换句话说,流不是稀疏的。

对于隐藏字幕流,轨道只包含两个片段,每个片段各自有定时信息(c 元素的 t 属性)。此外,ParentStreamIndex 属性设置为“video”,使视频流成为隐藏字幕流的父级。这使得隐藏字幕流与来自视频流的定时信息对应起来:隐藏字幕流正好与其父视频流一起开始和结束,第一个字幕在视频流中显示 10 秒,而第二个在视频中显示 15 秒。如果一个流中的时间线基于父流,并且片段是不连续的,则该流称为稀疏流。 

轨道是特定类型内容(视频、音频或文本)片段的定时序列。每个轨道都使用一个 TrackInfo 类型的实例表示,流中的所有轨道通过 StreamInfo.AvailableTracks 集合属性提供。

客户端清单中的每个轨道都通过一个 QualityLevel 唯一标识。QualityLevel 通过关联比特率标识,通过 TrackInfo.Bitrate 属性公开。例如,一个客户端清单中的一个视频流可以有多个 QualityLevel,每个 QualityLevel 都有唯一的比特率。每个 QualityLevel 表示相同视频内容的唯一轨道,以 QualityLevel 指定的比特率编码。

自定义属性和清单输出

自定义属性是向清单中添加其他特定于流或轨道的信息的一种方法。自定义属性是使用 CustomAttribute 元素指定的,该元素可包含以键/值对表示的多个数据元素。每个数据元素都表示为一个 Attribute 元素,使用 Key 和 Value 属性指定数据元素键和数据元素值。在不应用不同质量水平的情况下,如一个流中的多个轨道具有相同的轨道名和比特率时,还可以使用自定义属性对轨道进行区分。图 4 显示了一个自定义属性用法示例。

图 4 在客户端清单中使用自定义属性

<StreamIndex Type="video" Chunks="12" QualityLevels="2" 
  MaxWidth="1280" MaxHeight="720" 
  DisplayWidth="1280" DisplayHeight="720" 
  Url="QualityLevels({bitrate})/Fragments(video={start time})">
  <CustomAttributes>
    <Attribute Key="CameraAngle" Value="RoofCam"/>
    <Attribute Key="AccessLevel" Value="PaidSubscription"/>
  </CustomAttributes>
  <QualityLevel Index="0" Bitrate="2962000" FourCC="WVC1" 
    MaxWidth="1280" MaxHeight="720"
    CodecPrivateData="250000010FD37E27F1678A27F859E80490825A645A64400000010E5A67F840">
    <CustomAttributes>
      <Attribute Name = "hardwareProfile" Value = "10000" />
    </CustomAttributes>
  </QualityLevel>
...
</StreamIndex>

添加到清单中的自定义属性不会自动影响任何 SSME 行为。它们是生产工作流在清单中引入播放器代码可接收和操作的自定义数据的一种方法。例如,在图 4 中,您可能想要在视频流自定义属性集合中查找 AccessLevel 自定义属性键,并根据属性值的指示仅向付费订阅者公开该视频流。

StreamInfo.CustomAttributes 集合属性公开在流级别应用的所有自定义属性的字符串键/值对(作为 StreamIndex 元素的直接 CustomAttribute 子元素)。TrackInfo.CustomAttributes 属性对在轨道级别应用的所有自定义属性也公开相同的内容(作为 QualityLevel 元素的直接子级)。

当流的 ManifestOutput 属性(StreamIndex 元素)设置为 TRUE 时,客户端清单实际上可包含表示流中轨道的每个片段的数据。图 5 显示了一个示例。

图 5 清单输出

<StreamIndex Type="video" Chunks="12" QualityLevels="2" 
  MaxWidth="1280" MaxHeight="720" 
  DisplayWidth="1280" DisplayHeight="720" 
  Url="QualityLevels({bitrate})/Fragments(video={start time})">
  <CustomAttributes>
    <Attribute Key="CameraAngle" Value="RoofCam"/>
    <Attribute Key="AccessLevel" Value="PaidSubscription"/>
  </CustomAttributes>
  <QualityLevel Index="0" Bitrate="2962000" FourCC="WVC1" 
    MaxWidth="1280" MaxHeight="720" 
    CodecPrivateData="250000010FD37E27F1678A27F859E80490825A645A64400000010E5A67F840">
    <CustomAttributes>
      <Attribute Name = "hardwareProfile" Value = "10000" />
    </CustomAttributes>
  </QualityLevel>
...
</StreamIndex>

请注意 f 元素内的嵌套内容,每个均表示要在包含区块所指定的时间显示的字幕项数据。客户端清单规范要求数据表示为原始数据项的 base64 编码的字符串版本。

TrackInfo.TrackData 集合属性包含一个 TimelineEvent 实例列表:每个实例对应于轨道的一个 f 元素。对于每个 TimelineEvent 条目,TimelineEvent.EventTime 表示序列中的时间点,TimelineEvent.EventData 提供 base64 编码的文本字符串。TrackInfo 还支持 Bitrate、CustomAttributes、Index、Name 和 ParentStream 属性。

选择流和轨道

通过许多有趣的方法都可在应用程序代码中使用流、轨道元数据和 API。

能够在流中选择特定轨道并筛选掉其余轨道的能力可能非常有用。一种常见的方案是基于订阅者访问级别提供分级观看体验,这种方案对基本或免费级别提供低分辨率版本的内容,只对高级订阅者提供高清版本:

if (subscriber.AccessLevel != "Premium") {
  StreamInfo videoStream = 
    ssme.GetStreamInfoForStreamType("video");
  List<TrackInfo> allowedTracks = 
    videoStream.AvailableTracks.Where((ti) => 
    ti.Bitrate < 1000000).ToList();
  ssme.SelectTracksForStream(
    videoStream, allowedTracks, false);
}

GetStreamInfoForStreamType 接受流类型文本并返回匹配的 StreamInfo 实例。对 StreamInfo.AvailableTracks 的 LINQ 查询检索提供比特率小于 1 Mbps 的轨道的列表,换句话说,就是对非高级订阅者提供的标准清晰度视频。然后,可以使用 SelectTracksForStream 方法筛选流中的轨道列表,得出要公开的轨道。

当 SelectTracksForStream 的最后一个参数设置为 true 时,向 SSME 指示应立即清除存储在先行缓冲区中的所有数据。若要随时获取当前选择的轨道列表,可以使用 StreamInfo.SelectedTracks 属性,同时 StreamInfo.AvailableTracks 属性继续公开所有可用轨道。

请记住,平滑流式处理允许相同类型的多个流在客户端清单中共存。在 IIS 平滑流式处理播放器开发工具包的当前 Beta 版本中,如果有多个指定类型的流,GetStreamInfoForStreamType 方法将返回该类型的第一个流实例,而这可能不是您需要的结果。但是,您尽可以忽略这个方法,改为直接对 AvailableStreams 集合使用查询获得正确的 StreamInfo。以下代码段显示了一个 LINQ 查询,该查询获取名为“ticker”的文本流:

StreamInfo tickerStream = 
  ssme.AvailableStreams.Where((stm) => 
  stm.Type == "text" && 
  stm.Name == "ticker").FirstOrDefault();

使用文本流

音频/视频影片可能需要在主视频序列的特定时间点显示具有一定时间长度的其他内容。示例有隐藏字幕、广告、新闻快讯、叠加动画等。使用文本流公开这些内容很方便。

在影片中包含文本流的一种方法是在视频编码期间复接文本轨道和视频轨道,这样文本轨道的内容区块以与视频相符的时间从服务器传输出来。

另一种方法是利用前面讨论的清单输出功能将文本内容编制在客户端清单本身中。下面我们进一步了解这种方法。

首先,需要准备一个包含文本流的客户端清单。在生产媒体工作流中,可以通过很多不同方法在编码期间或编码后在清单中注入此类内容,数据可以来自多个不同的数据源,如广告服务平台和字幕生成器。但在此示例中,我将使用一个简单的 XML 数据文件作为数据源,使用一些 LINQ over XML 查询制造文本流,并将它们插入现有客户端清单。

数据结构不必复杂。(可以在本文相应的代码下载中找到完整文件。这里我将使用摘录进行说明。)数据文件以一个 Tracks 元素开始,然后包含两个 ContentTrack 元素。每个 ContentTrack 条目最后都将在客户端清单中生成一个不同的文本流。第一个 ContentTrack 元素用于字幕:

<ContentTrack Name="ClosedCaptions" Subtype="CAPT">

第二个用于动画:

<ContentTrack Name="Animations" Subtype="DATA">

每个 ContentTrack 都包含多个 Event 元素,这些元素使用时间属性指定这些文本事件应在视频的时间线中发生的时间点。Event 元素又在 CDATA 部分中包含以 XML(对于动画为 XAML)定义的实际字幕事件:

<Event time="00:00:10"> 
  <![CDATA[<Caption Id="{DE90FACD-BC01-43f2-A4EC-6A01A49BAFBB}" 
    Action="ADD">
    Test Caption 1
  </Caption>] ]> 
</Event>
<Event time="00:00:15"> 
  <![CDATA[<Caption Id="{DE90FACD-BC01-43f2-A4EC-6A01A49BAFBB}" 
    Action="REMOVE"/>] ]> 
</Event>

请注意,对于添加的每个隐藏字幕事件,都有一个相应事件指示应移除上次所加字幕的时间点。隐藏字幕事件的 CDATA 部分包含的 Caption 元素定义一个值为 Add 或 Remove 的 Action 属性,指示相应的操作。

我的 LINQ over XML 代码将 XML 数据转换到客户端清单的相应条目中,并将它们插入一个现有客户端清单文件。在本文相应的代码下载部分可以找到一个示例,但请注意该示例所示数据格式不是平滑流式处理播放器开发工具包或平滑流式处理规范的一部分,也不具说明性。可以定义满足您应用程序需要的任意数据结构,只要可以将它转换为平滑流式处理客户端清单规范所需的适当格式即可,这包括将 CDATA 部分的文本内容编码为 base64 格式。

执行转换后,所生成的客户端清单文件将包含如图 6 所示的文本流。

图 6 包含文本内容流的客户端清单摘录

<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" 
  Duration="1456860000">
  <StreamIndex Type="video" Chunks="73" QualityLevels="8" 
    MaxWidth="1280" MaxHeight="720" 
    DisplayWidth="1280" DisplayHeight="720"
    Url="QualityLevels({bitrate})/Fragments(video={start time})">
    <QualityLevel Index="0" Bitrate="2962000" FourCC="WVC1" 
      MaxWidth="1280" MaxHeight="720"
      CodecPrivateData="250000010FD37E27F1678A27F859E80490825A645A64400000010E5A67F840" />
    <QualityLevel Index="1" Bitrate="2056000" FourCC="WVC1" 
      MaxWidth="992" MaxHeight="560" 
      CodecPrivateData="250000010FD37E1EF1178A1EF845E8049081BEBE7D7CC00000010E5A67F840" />
    ...
    <c n="0" d="20020000" />
    <c n="1" d="20020000" />
    ...
    <c n="71" d="20020000" />
    <c n="72" d="15010001" />
  </StreamIndex>
  <StreamIndex Type="audio" Index="0" FourCC="WMAP" 
    Chunks="73" QualityLevels="1" 
    Url="QualityLevels({bitrate})/Fragments(audio={start time})">
    <QualityLevel Bitrate="64000" SamplingRate="44100" Channels="2" 
      BitsPerSample="16" PacketSize="2973" AudioTag="354" 
      CodecPrivateData="1000030000000000000000000000E00042C0" />
    <c n="0" d="21246187" />
    <c n="1" d="19620819" />
    ...
    <c n="71" d="19504762" />
    <c n="72" d="14900906" />
  </StreamIndex>
  <StreamIndex Type="text" Name="ClosedCaptions" Subtype="CAPT" 
    TimeScale="10000000" ParentStreamIndex="video" 
    ManifestOutput="TRUE" QualityLevels="1" Chunks="2" 
    Url="QualityLevels({bitrate},{CustomAttributes})/Fragments(ClosedCaptions={start time})">
    <QualityLevel Index="0" Bitrate="1000" 
      CodecPrivateData="" FourCC=""/> 
    <c n="0" t="100000000">
      <f>...</f> 
    </c>
    <c n="1" t="150000000">
      <f>...</f>
    </c>
  </StreamIndex>
  ...
</SmoothStreamingMedia>

图 6 所示的客户端清单中已经存在视频和音频流,我添加了两个名称分别为 ClosedCaptions 和 Animations 的文本流。请注意,每个流都将该视频流用作其父级,并将 ManifestOutput 设置为 true。前一种设置是因为文本流实际上是稀疏流,将它们的父级设为视频流可确保每个文本内容条目(c 元素)在视频流时间线上正确定时。后一种设置是为了确保 SSME 从清单本身读取实际数据(f 元素内的 base64 编码字符串)。

TimelineEvent 和 TimelineMarker

现在,我们看看如何在 SSME 中使用其他文本内容。SSME 将其他文本属性在 AvailableStreams 属性中公开为 StreamInfo 实例,每个 StreamInfo 都以 TrackInfo 实例的形式包含轨道数据。TrackInfo.TrackData 集合属性将包含与每个文本轨道中的文本事件数量相同的 TimelineEvent 类型实例。TimelineEvent.EventData 属性公开一个表示字符串内容的字节数组(从其 base64 编码格式解码),而 TimelineEvent.EventTime 属性公开此事件应发生的时间点。

开始播放影片时,只要到达这些事件,SSME 就会引发 TimelineEventReached 事件。图 7 显示的示例对图 6 中添加到客户端清单中的隐藏字幕和动画轨道进行处理。

图 7 处理 TimelineEventReached 事件

ssme.TimelineEventReached += 
  new EventHandler<TimelineEventArgs>((s, e) => { 
  //if closed caption event
  if (e.Track.ParentStream.Name == "ClosedCaptions" && 
    e.Track.ParentStream.Subtype == "CAPT") {

    //base64 decode the content and load the XML fragment
    XElement xElem = XElement.Parse(
      Encoding.UTF8.GetString(e.Event.EventData,
      0, e.Event.EventData.Length));

    //if we are adding a caption
    if (xElem.Attribute("Action") != null && 
      xElem.Attribute("Action").Value == "ADD") {

      //remove the text block if it exists
      UIElement captionTextBlock = MediaElementContainer.Children.
      Where((uie) => uie is FrameworkElement && 
        (uie as FrameworkElement).Name == (xElem.Attribute("Id").Value)).
        FirstOrDefault() as UIElement;
        if(captionTextBlock != null)
          MediaElementContainer.Children.Remove(captionTextBlock);

      //add a TextBlock 
      MediaElementContainer.Children.Add(new TextBlock() {
        Name = xElem.Attribute("Id").Value,
        Text = xElem.Value,
        HorizontalAlignment = HorizontalAlignment.Center,
        VerticalAlignment = VerticalAlignment.Bottom,
        Margin = new Thickness(0, 0, 0, 20),
        Foreground = new SolidColorBrush(Colors.White),
        FontSize = 22
      });
    }
    //if we are removing a caption
    else if (xElem.Attribute("Action") != null && 
      xElem.Attribute("Action").Value == "REMOVE") {

      //remove the TextBlock
      MediaElementContainer.Children.Remove(
        MediaElementContainer.Children.Where(
        (uie) => uie is FrameworkElement && 
        (uie as FrameworkElement).Name == 
        (xElem.Attribute("Id").Value)).FirstOrDefault() 
        as UIElement);
    }
  }

  //Logic for animation event
  ...
});

处理每个 TimelineEvent 时,或者是在 UI 中插入一个 TextBlock 来显示字幕,或者是加载动画 XAML 字符串并启动动画(有关动画处理逻辑的详细信息,请参见可下载代码)。

请注意,因为文本内容是使用 base-64 编码的,所以解码为其原始状态。另请注意,代码检查 Caption 元素的 Action 属性来决定是向 UI 添加字幕还是移除现有字幕。对于动画事件,可以依赖动画自身的完成处理程序将其从 UI 中移除。

图 8 显示了一张屏幕快照,其中显示一条字幕以及在播放的视频上叠加的动画椭圆。虽然这种方法运行良好,但在使用之前仍有一个问题需要考虑。当前版本的 SSME 在两秒的边界上处理 TimlineEvent。为更好地理解这一点,我们假设您有一条定在视频时间线的 15.5 秒时显示的隐藏字幕。SSME 将在距该时间最近的为 2 的倍数的前一时间点为此隐藏字幕引发 TimelineEventReached 事件,即在大约 14 秒时。


图 8 使用文本内容流和 TimelineEvent 叠加内容

如果您需要更高的准确度并且您无法将内容区块放在两秒边界附近,则可能不适合使用 TimelineEventReached 处理内容轨道。但您可以使用 TimelineMarker 类(如在标准 MediaElement 类型中使用一样)向时间线添加标记,让这些标记能够以所需任意间隔引发 MarkerReached 事件。本文相应的代码下载中包含对 AddAndHandleMarkers 方法的概述,该方法为每个内容事件添加 TimelineMarker 并在 MarkerReached 事件处理程序中对它们进行响应。

合并外部清单

前面有一个示例是向客户端清单添加其他内容流。如果能够访问客户端清单,则那种方法很有效,但您有时可能无法通过直接访问客户端清单进行必要的添加。有时其他内容流还可能有条件地依赖于其他因素(例如,不同区域设置的不同语言的隐藏字幕)。将针对所有可能情况的数据都添加到客户端清单会导致 SSME 花费更多时间分析和加载清单。

SSME 通过允许您在运行时将外部清单文件合并到原始客户端清单解决这一问题,使您能够引入其他数据流并如前面所示对数据进行操作,而不必修改原始客户端清单。

下面是一个清单合并示例:

ssme.ManifestMerge += new 
  SmoothStreamingMediaElement.ManifestMergeHandler((sender) => {
  object ParsedExternalManifest = null;
  //URI of the right external manifest based on current locale
  //for example expands to 
  string UriString = 
    string.Format(
    "http://localhost/SmoothStreaming/Media/FighterPilot/{0}/CC.xml", 
    CultureInfo.CurrentCulture.Name);
  //parse the external manifest - timeout in 3 secs
  ssme.ParseExternalManifest(new Uri(UriString), 3000, 
    out ParsedExternalManifest);
  //merge the external manifest
  ssme.MergeExternalManifest(ParsedExternalManifest); 
});

此代码段检查当前区域设置并使用相应的外部清单文件(名为 CC.xml,存储在以该区域设置的语言标识符命名的文件夹中),该文件中包含该区域设置相应语言的隐藏字幕。ParseExternalManifest 方法接受一个指向外部清单位置的 URI,并通过该方法的第三个输出参数以对象的形式返回解析后的清单。该方法的第二个参数接受一个超时值,可用于避免在网络调用上阻塞太长时间。

MergeExternalManifest 方法接受从上一调用返回的解析后的清单对象并执行实际合并。这样,来自任意合并的外部清单的流和轨道都在您的播放器代码中以 StreamInfo 和 TrackInfo 实例的形式随处可用,并且可以进行如前所示的操作。

需要特别注意的是,只能在 ManifestMerge 事件处理程序中调用 ParseExternalManifest 和 MergeExternalManifest。在此事件处理程序范围外对这些方法进行的所有调用都会引发 InvalidOperationException。

请记住,外部清单的扩展名须关联一个 MIME 类型,该类型向其来源 Web 服务器进行了注册。使用如 .xml 这样的常用扩展名是个不错的主意,因为内容无论如何都是 XML。如果外部清单文件是从作为平滑流式处理服务器的同一 Web 服务器提供的,则应避免使用 .ismc 扩展名,因为 IIS Media Services 处理程序会阻止直接访问 .ismc 文件,并且 ParseExternalManifest 将无法下载外部清单。

就结构而言,外部清单要与常规客户端清单相同:一个顶层 SmoothStreamingMedia 元素,使用适当的 StreamIndex 子元素表示数据。

剪辑规划

您可能会需要在影片的特定时间点插入其他视频剪辑。示例包括影片中的广告视频、重大新闻或补白剪辑等多种内容。这个问题可以分为两部分来看。第一,获取必需的内容数据并确定它在时间线上的插入位置。第二,实际规划和播放剪辑。SSME 中引入的功能可使这两项任务都轻松实现。

可以继续使用在客户端清单中插入文本流的方法(如前面部分所示)将剪辑数据提供给代码使用。下面是剪辑规划信息所使用的示例数据源:

<ContentTrack Name="AdClips" Subtype="DATA">
  <Event time="00:00:04">
    <![CDATA[<Clip Id="{89F92331-8501-41ac-B78A-F83F6DD4CB40}" 
    Uri="http://localhost/SmoothStreaming/Media/Robotica/Robotica_1080.ism/manifest" 
    ClickThruUri="https://msdn.microsoft.com/en-us/robotics/default.aspx" 
    Duration="00:00:20" />] ]>
  </Event>
  <Event time="00:00:10">
    <![CDATA[<Clip Id="{3E5169F0-A08A-4c31-BBAD-5ED51C2BAD21}" 
    Uri="http://localhost/ProgDownload/Amazon_1080.wmv" 
    ClickThruUri="http://en.wikipedia.org/wiki/Amazon_Rainforest" 
    Duration="00:00:25"/>] ]>
  </Event>     
</ContentTrack>

对于每个要规划的剪辑,都有一个内容 URI、一个用户可以通过在剪辑上点击访问的网页 URI 以及一个剪辑播放持续时间。Event 元素的时间属性指定时间线上规划剪辑的位置。

通过使用上一节所述的相同的 LINQ to XML 查询方法,可以转换此数据并将相应的文本流添加到客户端清单中。与先前一样,文本流以 StreamInfo 实例的形式向代码公开。然后可以使用 SSME 的剪辑规划 API 利用此信息规划这些剪辑。图 9 显示了一种根据此信息规划剪辑的方法。

图 9 规划剪辑

private void ScheduleClips() {
  //get the clip data stream
  StreamInfo siAdClips = ssme.AvailableStreams.Where(
    si => si.Name == "AdClips").FirstOrDefault();

  //if we have tracks
  if (siAdClips != null && siAdClips.AvailableTracks.Count > 0) {

    //for each event in that track
    foreach (TimelineEvent te in 
      siAdClips.AvailableTracks[0].TrackData) {

      //parse the inner XML fragment
      XElement xeClipData = XElement.Parse(
        Encoding.UTF8.GetString(te.EventData, 0, 
        te.EventData.Length));

      //schedule the clip
      ssme.ScheduleClip(new ClipInformation {
        ClickThroughUrl = new Uri(
        xeClipData.Attribute("ClickThruUri").Value),
        ClipUrl = new Uri(xeClipData.Attribute("Uri").Value),
        IsSmoothStreamingSource = 
        xeClipData.Attribute("Uri").Value.ToUpper().Contains("ism"), 
        Duration = TimeSpan.Parse(xeClipData.Attribute("Duration").Value)
        },
        te.EventTime, true, //pause the timeline
        null);
    }
    //set the Clip MediaElement style
    ssme.ClipMediaElementStyle = 
      this.Resources["ClipStyle"] as Style;
  }
}

SSME 的 ScheduleClip 方法执行实际规划。 对于要规划的每段剪辑,都会使用从剪辑数据派生的相应属性在规划中插入 ClipInformation 类型的一个新实例。

请注意,剪辑可以是平滑流式处理源或 Silverlight MediaElement 支持的其他源。正确设置 ClipInformation.IsSmoothStreamingSource 属性非常重要,这可以确保使用正确的播放器组件播放剪辑。

ScheduleClip 的第二个参数是要播放剪辑的时间。第三个参数用于指示是否要在播放剪辑时使时间线停止前进。最后一个参数用于传入将提供给各种剪辑相关事件处理程序使用的任何用户数据。

有时需要顺序规划剪辑,即只对剪辑序列中的第一个剪辑应用开始时间信息,而后续剪辑链接起来以使规划的所有剪辑连续播放。ScheduleClip 方法也有利于实现此功能,如图 10 所示。

图 10 使用 ClipContext 链接规划的剪辑

private void ScheduleClips() {
  StreamInfo siAdClips = ssme.AvailableStreams.Where(
  si => si.Name == "AdClips").FirstOrDefault();

  if (siAdClips != null && siAdClips.AvailableTracks.Count > 0) {
    ClipContext clipCtx = null;
    foreach (
      TimelineEvent te in siAdClips.AvailableTracks[0].TrackData) {
      XElement xeClipData = 
        XElement.Parse(Encoding.UTF8.GetString(te.EventData, 0,
        te.EventData.Length));

      //if this is the first clip to be scheduled
      if (clipCtx == null) {
        clipCtx = ssme.ScheduleClip(new ClipInformation {
          ClickThroughUrl = new Uri(
          xeClipData.Attribute("ClickThruUri").Value),
          ClipUrl = new Uri(xeClipData.Attribute("Uri").Value),
          IsSmoothStreamingSource = 
          xeClipData.Attribute("Uri").Value.ToUpper().Contains("ism"), 
          Duration = TimeSpan.Parse(
          xeClipData.Attribute("Duration").Value)
        },
        te.EventTime, //pass in the start time for the clip
        true, null);
      }
      else { //subsequent clips
        clipCtx = ssme.ScheduleClip(new ClipInformation {
          ClickThroughUrl = new Uri(
          xeClipData.Attribute("ClickThruUri").Value),
          ClipUrl = new Uri(xeClipData.Attribute("Uri").Value),
          IsSmoothStreamingSource = 
          xeClipData.Attribute("Uri").Value.ToUpper().Contains("ism"),
          Duration = TimeSpan.Parse(
          xeClipData.Attribute("Duration").Value)
        },
        clipCtx, //clip context for the previous clip to chain
        true, null);
      }
    }
    ssme.ClipMediaElementStyle = 
      this.Resources["ClipStyle"] as Style;
  }
}

我只用一个绝对时间规划第一个剪辑,此时没有 ClipContext(换言之,clipCtx 变量为 null)。对 ScheduleClip 的每个后续调用都返回一个表示剪辑的规划状态的 ClipContext 实例。ScheduleClip 方法有一个重载方法,该重载方法接受 ClipContext 实例而不是剪辑规划开始时间,并将剪辑规划为在上一个规划的剪辑(由传入的 ClipContext 表示)之后立刻开始。 

播放规划的剪辑时,SSME 隐藏主视频并引入 MediaElement 播放规划的剪辑。如果要自定义此 MediaElement,可以将 SSME 的 ClipMediaElementStyle 属性设置为所需的 XAML 样式。

在播放规划的剪辑时,SSME 还会引发几个相关的事件。可以处理 ClipProgressUpdate 事件跟踪剪辑进度。ClipPlaybackEventArgs.Progress 属于枚举类型 ClipProgress,它以四分位数表示剪辑进度。ClipProgressUpdate 事件只在剪辑的开始和结束以及表示剪辑持续时间的 25%、50% 和 75% 的时间点引发。请注意,ClipContext.HasQuartileEvents 布尔属性指示是否对剪辑引发四分位事件。在某些情况下,如剪辑持续时间未知时,不会引发四分位进度事件。

当查看者在查看时单击剪辑时会引发 ClipClickThrough 事件。如果要为此剪辑使用点击目标,ClipEventArgs.ClipContext.ClipInformation.ClickThroughUrl 会将其公开,您可以选择使用某种方法(如与浏览器交互打开弹出窗口)打开点击 URL 所指向的 Web 资源。

还可以分别使用 ClipError 事件和 ClipStateChanged 事件处理剪辑的任何错误情况和状态更改。 

播放速度和方向

SSME 允许以不同的速度和方向播放内容。SmoothStreamingMediaElement.SupportedPlaybackRates 属性以双精度值返回支持的播放速度的列表,其中 1.0 表示默认播放速度。在当前公测版中,此列表还另外包含 0.5、4.0、8.0、-4.0 和 -8.0 这几个值。正值能以半速、4 倍和 8 倍速度播放,负值能以 4 倍和 8 倍速度反向播放(后退)。

可以在播放期间的任意点调用 SmoothStreamingMediaElement.SetPlaybackRate 方法设置播放速度。SetPlaybackRate 以其唯一参数的形式接受所需播放速度。

请注意,控制播放速度只对平滑流式处理内容有效。因此如果使用 SSME 播放渐进下载或使用某些其他方法流式处理的内容,SetPlaybackRate 将引发异常。

使用复合清单进行平滑流编辑

有时,您可能需要将来自多个平滑流式处理影片的各个部分组合到一个复合影片中。最常见的方案是使用如粗切割编辑器这样的工具,这些编辑器允许用户在主源生产剪辑上指定入时间点和出时间点,然后使几个可能来自不同主源的此类剪辑像一个影片那样以线性方式播放。

使用 SSME 的复合清单功能,可以通过创建一个包含剪辑片段的独立清单文档完成此任务,在该文档中每个剪辑片段定义由剪辑的开始和结束时间点界定的完整影片的一部分。使用此方法的最大优点是能够对现有影片进行不同的编辑,而不需要对源材料进行代码转换。

复合清单始终以扩展名 .csm 结尾。若要使用这样的清单,只需将 SmoothStreamingSource 属性设置为一个指向复合清单文件的有效 URL:

ssme.SmoothStreamingSource = new Uri("http://localhost/SmoothStreaming/Media/MyCompositeSample.csm");

图 11 显示了一个复合清单的一段摘录。(完整文件包含在本文的代码下载中。)

图 11 示例复合清单

<?xml version="1.0" encoding="utf-8"?>
<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" Duration="269000000">
<Clip Url="http://localhost/SmoothStreaming/Media/AmazingCaves/Amazing_Caves_1080.ism/manifest" 
  ClipBegin="81000000" ClipEnd="250000000">
<StreamIndex Type="video" Chunks="9" QualityLevels="3"
  MaxWidth="992" MaxHeight="560"
  DisplayWidth="992" DisplayHeight="560"
  Url="QualityLevels({bitrate})/Fragments(video={start time})">
  <QualityLevel Index="0" Bitrate="2056000" FourCC="WVC1"
    MaxWidth="992" MaxHeight="560"
    CodecPrivateData="250000010FD37E1EF1178A1EF845E8049081BEBE7D7CC00000010E5A67F840" 
  />
  <QualityLevel Index="1" Bitrate="1427000" FourCC="WVC1"
    MaxWidth="768" MaxHeight="432"
    CodecPrivateData="250000010FCB6C17F0D78A17F835E8049081AB8BD718400000010E5A67F840" 
  />
  <QualityLevel Index="2" Bitrate="991000" FourCC="WVC1"
    MaxWidth="592" MaxHeight="332"
    CodecPrivateData="250000010FCB5E1270A58A127829680490811E3DF8F8400000010E5A67F840" 
  />
  <c t="80130000" />
  <c t="100150000" />
  <c t="120170000" />
  <c t="140190000" />
  <c t="160210000" />
  <c t="180230000" />
  <c t="200250000" />
  <c t="220270000" />
  <c t="240290000" d="20020000" />
</StreamIndex>
<StreamIndex Type="audio" Index="0" FourCC="WMAP"
  Chunks="10" QualityLevels="1" 
  Url="QualityLevels({bitrate})/Fragments(audio={start time})">
  <QualityLevel Bitrate="64000" SamplingRate="44100"
    Channels="2" BitsPerSample="16" PacketSize="2973"
    AudioTag="354" CodecPrivateData="1000030000000000000000000000E00042C0" />
  <c t="63506576" />
  <c t="81734240" />
  <c t="102632199" />
  <c t="121672562" />
  <c t="142106122" />
  <c t="162075283" />
  <c t="181580045" />
  <c t="202478004" />
  <c t="222447165" />
  <c t="241313378" d="20143311" />
</StreamIndex>
</Clip>
<Clip Url="http://localhost/SmoothStreaming/Media/CoralReef/Coral_Reef_Adventure_1080.ism/manifest" 
  ClipBegin="102000000" ClipEnd="202000000">
<StreamIndex Type="video" Chunks="6" QualityLevels="3"
  MaxWidth="992" MaxHeight="560"
  DisplayWidth="992" DisplayHeight="560"
  Url="QualityLevels({bitrate})/Fragments(video={start time})">
...
</Clip>
</SmoothStreamingMedia>

此清单包含两个 Clip 元素,每个元素定义一个来自现有平滑流式处理影片的剪辑(也称为编辑)。URL 属性指向现有平滑流式处理影片,ClipBegin 和 ClipEnd 属性包含提供剪辑边界的开始和结束时间值。顶层 SmoothStreamingMedia 元素的 Duration 属性应恰好为清单中每个剪辑的持续时间的总和:可以对每个 Clip 条目的 ClipEnd 与 ClipBegin 之差求和得到清单的总持续时间。

每个 Clip 元素都包含视频和音频 StreamIndex 及其子 QualityLevel 条目,这些内容对应于源影片的客户端清单 (.ismc) 文件。但是,可以将每个 StreamIndex 条目的区块元数据 (c) 条目限制为满足 ClipBegin 和 ClipEnd 边界所需的那些区块。换言之,ClipBegin 值需要大于或等于流的第一个 c 条目的开始时间(t 属性)值,ClipEnd 值需要小于或等于该开始时间与该流的最后一个 c 条目的持续时间(d 属性)值之和。

请注意,在客户端清单中,区块可能是以索引(n 属性)方式以指定持续时间定义的。但是,对于复合清单,需要使用开始时间(通过对之前区块的持续时间求和很容易计算出)定义区块。另请注意,每个 StreamIndex 条目的 Chunks 属性都需要反映剪辑内的区块数,但所有其他属性都对应于源客户端清单中的条目。 

实时流

SSME 可以播放点播流和实时流。若要使用 SSME 播放实时平滑流式处理视频流,可以将 SSME 的 SmoothStreamingSource 属性设置为一个现场发布点 URL:

ssme.SmoothStreamingSource = "http://localhost/SmoothStreaming/Media/FighterPilotLive.isml/manifest";

若要了解 SSME 是否正在播放实时流,可以检查 IsLive 属性。内容为现场源时该属性的值为 True,否则为 False。

请注意,平滑流式处理实时视频的设置和传输需要专用基础结构。关于设置实时流式处理服务器环境的详细讨论已超出本文的范围。有关如何设置 IIS Media Services 3.0 进行实时流式处理的更多详细信息,可以参考 learn.iis.net/page.aspx/628/live-smooth-streaming/ 上的文章。learn.iis.net/page.aspx/620/live-smooth-streaming-for-iis-70---getting-started/ 上的文章将为您提供有关如何设置实时流式处理模拟环境以进行测试的信息。

总结

IIS 平滑流式处理是 Microsoft 提供的最新的自适应流式处理平台。如您所见,平滑流式处理 PDK(特别是 SmoothStreamingMediaElement 类型)是创建可以使用点播流和实时流的 Silverlight 客户端的关键组成部分。此 PDK 对平滑流的客户端行为提供了全面的控制,允许您编写超越音频/视频流的丰富逼真的体验,让您以有意义的方式将数据流与您的媒体组合起来。

有关平滑流式处理的详细讨论已经超出了本文的范围。建议您在 iis.net/media 查找更多详细信息。有关在 Silverlight 中进行媒体编程以及 Silverlight MediaElement 类型的更多指南,可以访问 silverlight.net/getstarted

 

Jit Ghosh* 是 Microsoft 开发人员推广团队的架构推广者,负责为媒体领域的客户提供构建先进数字媒体解决方案方面的咨询。Ghosh 与他人合著了《Silverlight Recipes》(APress,2009 年)一书。您可以浏览其博客,网址为 blogs.msdn.com/jitghosh。*

衷心感谢以下技术专家对本文的审阅:Vishal Sood