系统支持的计时元数据提示

本文介绍如何利用可以在媒体文件或流中嵌入的多种格式的计时元数据。 每当遇到这些元数据提示时,UWP 应用都可以注册媒体管道在播放期间引发的事件。 使用 DataCue 类,应用可以实现自己的自定义元数据提示,但本文重点介绍媒体管道自动检测到的多个元数据标准,包括:

  • VobSub 格式的基于图像的字幕
  • 语音提示,包括单词边界、句子边界和语音合成标记语言(SSML)书签
  • 章节提示
  • 扩展的 M3U 注释
  • ID3 标记
  • 碎片 mp4 emsg 框

本文建立在文章媒体项、播放列表和轨中讨论的概念之上,这包括使用 MediaSourceMediaPlaybackItemTimedMetadataTrack 类的基础知识以及有关在应用中使用计时元数据的通用指南。

对于本文中所述的所有不同类型的计时元数据,基本实现步骤相同:

  1. 创建 MediaSource,然后为要播放的内容创建 MediaPlaybackItem
  2. 注册 MediaPlaybackItem.TimedMetadataTracksChanged 事件,该事件在媒体管道解析媒体项的子轨道时发生。
  3. 要使用的计时元数据跟踪注册 TimedMetadataTrack.CueEnteredTimedMetadataTrack.CueExited 事件。
  4. CueEntered 事件处理程序中,根据事件参数中传递的元数据更新 UI。 可以再次更新 UI,以删除当前副标题文本,例如,在 CueExited 事件中

在本文中,处理每种类型的元数据显示为不同的方案,但可以使用大多数共享代码处理不同类型的元数据(或忽略)。 可以在进程中的多个点检查 TimedMetadataKind 对象的 TimedMetadataKind 属性 因此,例如,可以选择为具有 TimedMetadataKind.ImageSubtitle 值的元数据跟踪注册 CueEntered 事件,但对于具有 TimedMetadataKind.Speech 值的跟踪,则不能注册该事件。 或者,你可以为所有元数据跟踪类型注册处理程序,然后在 CueEntered 处理程序中检查 TimedMetadataKind 值,以确定响应提示时要执行的操作。

基于图像的字幕

从 Windows 10 版本 1703 开始,UWP 应用可以支持 VobSub 格式的外部基于图像的字幕。 若要使用此功能,请先为显示图像副标题的媒体内容创建 MediaSource 对象。 接下来,通过调用 CreateFromUriWithIndex 或 CreateFromStreamWithIndex 来创建 TimedTextSource 对象,并传入包含副标题图像数据的 .sub 文件的 URI,以及包含副标题计时信息的 .idx 文件。 将 TimedTextSource 添加到 MediaSource,方法是将其添加到源的 ExternalTimedTextSources 集合。 从 MediaSource 创建 MediaPlaybackItem

var contentUri = new Uri("http://contoso.com/content.mp4");
var mediaSource = MediaSource.CreateFromUri(contentUri);

var subUri = new Uri("http://contoso.com/content.sub");
var idxUri = new Uri("http://contoso.com/content.idx");
var timedTextSource = TimedTextSource.CreateFromUriWithIndex(subUri, idxUri);
mediaSource.ExternalTimedTextSources.Add(timedTextSource);

var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用在上一步中创建的 MediaPlaybackItem 对象注册图像副标题元数据事件。 此示例使用 Helper 方法 RegisterMetadataHandlerForImageSubtitles 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此,在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForImageSubtitles

mediaPlaybackItem.TimedMetadataTracksChanged += (MediaPlaybackItem sender, IVectorChangedEventArgs args) =>
{
    if (args.CollectionChange == CollectionChange.ItemInserted)
    {
        RegisterMetadataHandlerForImageSubtitles(sender, (int)args.Index);
    }
    else if (args.CollectionChange == CollectionChange.Reset)
    {
        for (int index = 0; index < sender.TimedMetadataTracks.Count; index++)
        {
            if (sender.TimedMetadataTracks[index].TimedMetadataKind == TimedMetadataKind.ImageSubtitle)
                RegisterMetadataHandlerForImageSubtitles(sender, index);
        }
    }
};

for (int index = 0; index < mediaPlaybackItem.TimedMetadataTracks.Count; index++)
{
    RegisterMetadataHandlerForImageSubtitles(mediaPlaybackItem, index);
}

注册图像副标题元数据事件后,MediaItem 将分配到 MediaPlayer,以便在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForImageSubtitles 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForImageSubtitles(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    timedTrack.CueEntered += metadata_ImageSubtitleCueEntered;
    timedTrack.CueExited += metadata_ImageSubtitleCueExited;
    item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);

}

在 CueEntered 事件的处理程序中,可以检查传递给处理程序的 TimedMetadataTrack 对象的 TimedMetadataKind 属性,以查看元数据是否用于图像副标题。 如果对多种类型的元数据使用相同的数据提示事件处理程序,则需要这样做。 如果关联的元数据跟踪的类型为 TimedMetadataKind.ImageSubtitle,请将 MediaCueEventArgs Cue 属性中包含的数据提示转换为 ImageCue。 ImageCueSoftwareBitmap 属性包含副标题图像的 SoftwareBitmap 表示形式。 创建 SoftwareBitmapSource 并调用 SetBitmapAsync 将图像分配给 XAML 图像控件。 ImageCue盘区位置属性提供有关副标题图像的大小和位置的信息。

private async void metadata_ImageSubtitleCueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    // Check in case there are different tracks and the handler was used for more tracks 
    if (timedMetadataTrack.TimedMetadataKind == TimedMetadataKind.ImageSubtitle)
    {
        var cue = args.Cue as ImageCue;
        if (cue != null)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                var source = new SoftwareBitmapSource();
                await source.SetBitmapAsync(cue.SoftwareBitmap);
                SubtitleImage.Source = source;
                SubtitleImage.Width = cue.Extent.Width;
                SubtitleImage.Height = cue.Extent.Height;
                SubtitleImage.SetValue(Canvas.LeftProperty, cue.Position.X);
                SubtitleImage.SetValue(Canvas.TopProperty, cue.Position.Y);
            });
        }
    }
}

语音提示

从 Windows 10 版本 1703 开始,UWP 应用可以在播放媒体中注册以接收事件以响应单词边界、句子边界和语音合成标记语言(SSML)书签。 这样,就可以播放使用 SpeechSynthesizer 类生成的音频流,并根据这些事件更新 UI,例如显示当前播放的单词或句子的文本。

本节中显示的示例使用类成员变量来存储将合成和播放的文本字符串。

string inputText = "In the lake heading for the mountain, the flea swims";

创建 SpeechSynthesizer 类的新实例。 将合成器的 IncludeWordBoundaryMetadataIncludeSentenceBoundaryMetadata 选项设置为 true,以指定应将元数据包含在生成的媒体流中。 调用 SynthesizeTextToStreamAsync 以生成包含合成语音和相应元数据的流。 从合成流创建 MediaSourceMediaPlaybackItem

var synthesizer = new Windows.Media.SpeechSynthesis.SpeechSynthesizer();

// Enable word marker generation (false by default). 
synthesizer.Options.IncludeWordBoundaryMetadata = true;
synthesizer.Options.IncludeSentenceBoundaryMetadata = true;

var stream = await synthesizer.SynthesizeTextToStreamAsync(inputText);
var mediaSource = MediaSource.CreateFromStream(stream, "");
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用 MediaPlaybackItem 对象注册语音元数据事件。 此示例使用帮助程序方法 RegisterMetadataHandlerForSpeech 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此,在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForSpeech

// Since the tracks are added later we will  
// monitor the tracks being added and subscribe to the ones of interest 
mediaPlaybackItem.TimedMetadataTracksChanged += (MediaPlaybackItem sender, IVectorChangedEventArgs args) =>
{
    if (args.CollectionChange == CollectionChange.ItemInserted)
    {
        RegisterMetadataHandlerForSpeech(sender, (int)args.Index);
    }
    else if (args.CollectionChange == CollectionChange.Reset)
    {
        for (int index = 0; index < sender.TimedMetadataTracks.Count; index++)
        {
            RegisterMetadataHandlerForSpeech(sender, index);
        }
    }
};

// If tracks were available at source resolution time, itterate through and register: 
for (int index = 0; index < mediaPlaybackItem.TimedMetadataTracks.Count; index++)
{
    RegisterMetadataHandlerForSpeech(mediaPlaybackItem, index);
}

注册语音元数据事件后,MediaItem分配到 MediaPlayer 以在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForSpeech 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForSpeech(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    timedTrack.CueEntered += metadata_SpeechCueEntered;
    timedTrack.CueExited += metadata_SpeechCueExited;
    item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);

}

在 CueEntered 事件的处理程序中,可以检查传递给处理程序的 TimedMetadataTrack 对象的 TimedMetadataKind 适当性,以查看元数据是否为语音。 如果对多种类型的元数据使用相同的数据提示事件处理程序,则需要这样做。 如果关联的元数据跟踪的类型为 TimedMetadataKind.Speech,请将 MediaCueEventArgs 的 Cue 属性中包含的数据提示转换为 SpeechCue 对于语音提示,元数据轨道中包含的语音提示类型通过检查 Label 属性来确定。 此属性的值将为单词边界的“SpeechWord”、句子边界的“SpeechSentence”或 SSML 书签的“SpeechBookmark”。 在此示例中,我们检查“SpeechWord”值,如果找到此值,则使用 SpeechCueStartPositionInInInputEndPositionInInInput 属性来确定当前正在播放的单词的输入文本内的位置。 此示例只是将每个单词输出输出输出。

private void metadata_SpeechCueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    // Check in case there are different tracks and the handler was used for more tracks 
    if (timedMetadataTrack.TimedMetadataKind == TimedMetadataKind.Speech)
    {
        var cue = args.Cue as SpeechCue;
        if (cue != null)
        {
            if (timedMetadataTrack.Label == "SpeechWord")
            {
                // Do something with the cue 
                System.Diagnostics.Debug.WriteLine($"{cue.StartPositionInInput} - {cue.EndPositionInInput}: {inputText.Substring((int)cue.StartPositionInInput, ((int)cue.EndPositionInInput - (int)cue.StartPositionInInput) + 1)}");
            }
        }
    }
}

章节提示

从 Windows 10 版本 1703 开始,UWP 应用可以注册与媒体项中的章节对应的提示。 若要使用此功能,请为媒体内容创建 MediaSource 对象,然后从 MediaSource 创建 MediaPlaybackItem

var contentUri = new Uri("http://contoso.com/content.mp4");
var mediaSource = MediaSource.CreateFromUri(contentUri);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用在上一步中创建的 MediaPlaybackItem 对象注册章节元数据事件。 此示例使用 Helper 方法 RegisterMetadataHandlerForChapterCues 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForChapterCues

mediaPlaybackItem.TimedMetadataTracksChanged += (MediaPlaybackItem sender, IVectorChangedEventArgs args) =>
{
    if (args.CollectionChange == CollectionChange.ItemInserted)
    {
        RegisterMetadataHandlerForChapterCues(sender, (int)args.Index);
    }
    else if (args.CollectionChange == CollectionChange.Reset)
    {
        for (int index = 0; index < sender.TimedMetadataTracks.Count; index++)
        {
            if (sender.TimedMetadataTracks[index].TimedMetadataKind == TimedMetadataKind.ImageSubtitle)
                RegisterMetadataHandlerForChapterCues(sender, index);
        }
    }
};

for (int index = 0; index < mediaPlaybackItem.TimedMetadataTracks.Count; index++)
{
    RegisterMetadataHandlerForChapterCues(mediaPlaybackItem, index);
}

注册章节元数据事件后,MediaItem分配给 MediaPlayer,以便在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForChapterCues 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForChapterCues(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    timedTrack.CueEntered += metadata_ChapterCueEntered;
    timedTrack.CueExited += metadata_ChapterCueExited;
    item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);
}

在 CueEntered 事件的处理程序中,可以检查传递给处理程序的 TimedMetadataTrack 对象的 TimedMetadataKind 是否正确,以查看元数据是否用于章节提示。如果对多种类型的元数据使用相同的数据提示事件处理程序,则需要这样做。 如果关联的元数据跟踪的类型为 TimedMetadataKind.Chapter,请将 MediaCueEventArgs Cue 属性中包含的数据提示转换为 ChapterCue ChapterCueTitle 属性包含刚刚在播放中到达的章节的标题。

private async void metadata_ChapterCueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    // Check in case there are different tracks and the handler was used for more tracks 
    if (timedMetadataTrack.TimedMetadataKind == TimedMetadataKind.Chapter)
    {
        var cue = args.Cue as ChapterCue;
        if (cue != null)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                ChapterTitleTextBlock.Text = cue.Title;
            });
        }
    }
}

使用章节提示查找下一章

除了在当前章节更改播放项目时接收通知外,还可以使用章节提示来查找播放项中的下一章。 下面显示的示例方法采用 MediaPlayer表示当前播放媒体项的 MediaPlaybackItem 的参数。 搜索 TimedMetadataTracks 集合,以查看是否有任一曲目具有 TimedMetadataTrack 值 TimedMetadataTrack 值的 TimedMetadataKind。Chapter。 如果找到章节曲目,该方法将循环访问曲目提示集合中的每个提示,以查找 StartTime 大于媒体播放器播放会话当前位置的第一个提示。 找到正确的提示后,播放会话的位置将更新,并在 UI 中更新章节标题。

private void GoToNextChapter(MediaPlayer player, MediaPlaybackItem item)
{
    // Find the chapters track if one exists
    TimedMetadataTrack chapterTrack = item.TimedMetadataTracks.FirstOrDefault(track => track.TimedMetadataKind == TimedMetadataKind.Chapter);
    if (chapterTrack == null)
    {
        return;
    }

    // Find the first chapter that starts after current playback position
    TimeSpan currentPosition = player.PlaybackSession.Position;
    foreach (ChapterCue cue in chapterTrack.Cues)
    {
        if (cue.StartTime > currentPosition)
        {
            // Change player position to chapter start time
            player.PlaybackSession.Position = cue.StartTime;

            // Display chapter name
            ChapterTitleTextBlock.Text = cue.Title;
            break;
        }
    }
}

扩展的 M3U 注释

从 Windows 10 版本 1703 开始,UWP 应用可以注册与扩展 M3U 清单文件中的注释对应的提示。 此示例使用 AdaptiveMediaSource 播放媒体内容。 有关详细信息,请参阅 自适应流式处理通过调用 CreateFromUriAsync 或 CreateFromStreamAsync 为内容创建 AdaptiveMediaSource。 通过调用 CreateFromAdaptiveMediaSource 来创建 MediaSource 对象,然后从 MediaSource 创建 MediaPlaybackItem

AdaptiveMediaSourceCreationResult result =
    await AdaptiveMediaSource.CreateFromUriAsync(new Uri("http://contoso.com/playlist.m3u"));

if (result.Status != AdaptiveMediaSourceCreationStatus.Success)
{
    // TODO: Handle adaptive media source creation errors.
    return;
}
var mediaSource = MediaSource.CreateFromAdaptiveMediaSource(result.MediaSource);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用在上一步中创建的 MediaPlaybackItem 对象注册 M3U 元数据事件。 此示例使用 Helper 方法 RegisterMetadataHandlerForEXTM3UCues 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForEXTM3UCues

mediaPlaybackItem.TimedMetadataTracksChanged += (MediaPlaybackItem sender, IVectorChangedEventArgs args) =>
{
    if (args.CollectionChange == CollectionChange.ItemInserted)
    {
        RegisterMetadataHandlerForEXTM3UCues(sender, (int)args.Index);
    }
    else if (args.CollectionChange == CollectionChange.Reset)
    {
        for (int index = 0; index < sender.TimedMetadataTracks.Count; index++)
        {
            if (sender.TimedMetadataTracks[index].TimedMetadataKind == TimedMetadataKind.ImageSubtitle)
                RegisterMetadataHandlerForEXTM3UCues(sender, index);
        }
    }
};

for (int index = 0; index < mediaPlaybackItem.TimedMetadataTracks.Count; index++)
{
    RegisterMetadataHandlerForEXTM3UCues(mediaPlaybackItem, index);
}

注册 M3U 元数据事件后,MediaItem分配到 MediaPlayer 以在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForEXTM3UCues 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 检查元数据跟踪的 DispatchType 属性,如果跟踪表示 M3U 注释,该跟踪的值将为“EXTM3U”。 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForEXTM3UCues(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    var dispatchType = timedTrack.DispatchType;

    if (String.Equals(dispatchType, "EXTM3U", StringComparison.OrdinalIgnoreCase))
    {
        timedTrack.Label = "EXTM3U comments";
        timedTrack.CueEntered += metadata_EXTM3UCueEntered;
        timedTrack.CueExited += metadata_EXTM3UCueExited;
        item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);
    }
}

在 CueEntered 事件的处理程序中,将 MediaCueEventArgs 的 Cue 属性中包含的数据提示强制转换为 DataCue。 检查以确保 提示的 DataCueData 属性不为 null。 扩展 EMU 注释以 UTF-16、小 endian、null 终止字符串的形式提供。 创建新的 DataReader,通过调用 DataReader.FromBuffer 来读取提示数据。读取器的 UnicodeEncoding 属性设置为 Utf16LE,以正确格式读取数据。 调用 ReadString 读取数据,指定数据字段长度的一半,因为每个字符的大小为两个字节,并减去一个以删除尾随 null 字符。 在此示例中,M3U 注释只是写入调试输出。

private void metadata_EXTM3UCueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    var dataCue = args.Cue as DataCue;
    if (dataCue != null && dataCue.Data != null)
    {
        // The payload is a UTF-16 Little Endian null-terminated string.
        // It is any comment line in a manifest that is not part of the HLS spec.
        var dr = Windows.Storage.Streams.DataReader.FromBuffer(dataCue.Data);
        dr.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf16LE;
        var m3uComment = dr.ReadString(dataCue.Data.Length / 2 - 1);
        System.Diagnostics.Debug.WriteLine(m3uComment);
    }
}

ID3 标记

从 Windows 10 版本 1703 开始,UWP 应用可以注册与 Http Live Streaming (HLS) 内容中的 ID3 标记对应的提示。 此示例使用 AdaptiveMediaSource 播放媒体内容。 有关详细信息,请参阅 自适应流式处理通过调用 CreateFromUriAsync 或 CreateFromStreamAsync 为内容创建 AdaptiveMediaSource。 通过调用 CreateFromAdaptiveMediaSource 来创建 MediaSource 对象,然后从 MediaSource 创建 MediaPlaybackItem

AdaptiveMediaSourceCreationResult result =
    await AdaptiveMediaSource.CreateFromUriAsync(new Uri("http://contoso.com/playlist.m3u"));

if (result.Status != AdaptiveMediaSourceCreationStatus.Success)
{
    // TODO: Handle adaptive media source creation errors.
    return;
}
var mediaSource = MediaSource.CreateFromAdaptiveMediaSource(result.MediaSource);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用在上一步中创建的 MediaPlaybackItem 对象注册 ID3 标记事件。 此示例使用帮助程序方法 RegisterMetadataHandlerForID3Cues 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此,在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForID3Cues

AdaptiveMediaSourceCreationResult result =
    await AdaptiveMediaSource.CreateFromUriAsync(new Uri("http://contoso.com/playlist.m3u"));

if (result.Status != AdaptiveMediaSourceCreationStatus.Success)
{
    // TODO: Handle adaptive media source creation errors.
    return;
}
var mediaSource = MediaSource.CreateFromAdaptiveMediaSource(result.MediaSource);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

注册 ID3 元数据事件后,MediaItem分配到 MediaPlayer,以便在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForID3Cues 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 如果跟踪表示 ID3 标记,请检查元数据轨道的 DispatchType 属性,该属性的值将包含 GUID 字符串“15260DFFFF49443320FF49443320000F”。 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForID3Cues(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    var dispatchType = timedTrack.DispatchType;

    if (String.Equals(dispatchType, "15260DFFFF49443320FF49443320000F", StringComparison.OrdinalIgnoreCase))
    {
        timedTrack.Label = "ID3 tags";
        timedTrack.CueEntered += metadata_ID3CueEntered;
        timedTrack.CueExited += metadata_ID3CueExited;
        item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);
    }
}

在 CueEntered 事件的处理程序中,将 MediaCueEventArgs 的 Cue 属性中包含的数据提示强制转换为 DataCue。 检查以确保 提示的 DataCueData 属性不为 null。 扩展 EMU 注释以传输流中原始字节的形式提供(请参阅 ID3)。 创建新的 DataReader,通过调用 DataReader.FromBuffer 来读取提示数据。 在此示例中,从提示数据读取 ID3 标记的标头值,并写入调试输出。

private void metadata_ID3CueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    var dataCue = args.Cue as DataCue;
    if (dataCue != null && dataCue.Data != null)
    {
        // The payload is the raw ID3 bytes found in a TS stream
        // Ref: http://id3.org/id3v2.4.0-structure
        var dr = Windows.Storage.Streams.DataReader.FromBuffer(dataCue.Data);
        var header_ID3 = dr.ReadString(3);
        var header_version_major = dr.ReadByte();
        var header_version_minor = dr.ReadByte();
        var header_flags = dr.ReadByte();
        var header_tagSize = dr.ReadUInt32();

        System.Diagnostics.Debug.WriteLine($"ID3 tag data: major {header_version_major}, minor: {header_version_minor}");
    }
}

碎片 mp4 emsg 框

从 Windows 10 版本 1703 开始,UWP 应用可以注册对应于碎片 mp4 流中的 emsg 框的提示。 此类元数据的示例用法是让内容提供程序向客户端应用程序发出在实时传送视频流内容期间播放广告的信号。 此示例使用 AdaptiveMediaSource 播放媒体内容。 有关详细信息,请参阅 自适应流式处理通过调用 CreateFromUriAsync 或 CreateFromStreamAsync 为内容创建 AdaptiveMediaSource。 通过调用 CreateFromAdaptiveMediaSource 来创建 MediaSource 对象,然后从 MediaSource 创建 MediaPlaybackItem

AdaptiveMediaSourceCreationResult result =
    await AdaptiveMediaSource.CreateFromUriAsync(new Uri("http://contoso.com/playlist.m3u"));

if (result.Status != AdaptiveMediaSourceCreationStatus.Success)
{
    // TODO: Handle adaptive media source creation errors.
    return;
}
var mediaSource = MediaSource.CreateFromAdaptiveMediaSource(result.MediaSource);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

使用在上一步中创建的 MediaPlaybackItem 对象注册 emsg box 事件。 此示例使用帮助程序方法 RegisterMetadataHandlerForEmsgCues 注册事件。 lambda 表达式用于实现 TimedMetadataTracksChanged 事件的处理程序,当系统检测到与 MediaPlaybackItem 关联的元数据跟踪中的更改时发生。 在某些情况下,在最初解析播放项时,元数据轨迹可能可用,因此,在 TimedMetadataTracksChanged 处理程序之外,我们还循环访问可用的元数据轨迹并调用 RegisterMetadataHandlerForEmsgCues

AdaptiveMediaSourceCreationResult result =
    await AdaptiveMediaSource.CreateFromUriAsync(new Uri("http://contoso.com/playlist.m3u"));

if (result.Status != AdaptiveMediaSourceCreationStatus.Success)
{
    // TODO: Handle adaptive media source creation errors.
    return;
}
var mediaSource = MediaSource.CreateFromAdaptiveMediaSource(result.MediaSource);
var mediaPlaybackItem = new MediaPlaybackItem(mediaSource);

注册 emsg box 元数据事件后,MediaItem分配到 MediaPlayer,以便在 MediaPlayerElement播放。

_mediaPlayer = new MediaPlayer();
mediaPlayerElement.SetMediaPlayer(_mediaPlayer);
_mediaPlayer.Source = mediaPlaybackItem;
_mediaPlayer.Play();

在 RegisterMetadataHandlerForEmsgCues 帮助程序方法中,通过将索引编制到 MediaPlaybackItemTimedMetadataTracks 集合来获取 TimedMetadataTrack 类的实例 检查元数据跟踪的 DispatchType 属性,如果跟踪表示 emsg 框,该跟踪的值将为“emsg:mp4”。 注册 CueEntered 事件和 CueExited 事件。 然后,必须在播放项的 TimedMetadataTracks 集合上调用 SetPresentationMode,以指示系统应用想要接收此播放项目的元数据提示事件。

private void RegisterMetadataHandlerForEmsgCues(MediaPlaybackItem item, int index)
{
    var timedTrack = item.TimedMetadataTracks[index];
    var dispatchType = timedTrack.DispatchType;

    if (String.Equals(dispatchType, "emsg:mp4", StringComparison.OrdinalIgnoreCase))
    {
        timedTrack.Label = "mp4 Emsg boxes";
        timedTrack.CueEntered += metadata_EmsgCueEntered;
        timedTrack.CueExited += metadata_EmsgCueExited;
        item.TimedMetadataTracks.SetPresentationMode((uint)index, TimedMetadataTrackPresentationMode.ApplicationPresented);
    }
}

在 CueEntered 事件的处理程序中,将 MediaCueEventArgs 的 Cue 属性中包含的数据提示强制转换为 DataCue。 检查以确保 DataCue 对象不为 null。 emsg 框的属性由媒体管道提供,作为 DataCue 对象的 Properties 集合中的自定义属性。 此示例尝试使用 TryGetValue 方法提取多个不同的属性值。 如果此方法返回 null,则表示请求的适当性不在 emsg 框中,因此将改为设置默认值。

本示例的下一部分演示了触发广告播放的情况,即在上一步中获取的 scheme_id_uri 属性具有值为“urn:scte:scte35:2013:xml”的情况。 有关详细信息,请参阅 https://dashif.org/identifiers/event_schemes/。 请注意,标准建议多次发送此 emsg 进行冗余,因此此示例维护已处理且仅处理新消息的 emsg ID 列表。 创建新的 DataReader,通过调用 DataReader.FromBuffer 来读取提示数据,并通过设置 UnicodeEncoding 属性将编码设置为 UTF-8,然后读取数据。 在此示例中,消息有效负载将写入调试输出。 实际应用将使用有效负载数据来计划广告的播放。

private void metadata_EmsgCueEntered(TimedMetadataTrack timedMetadataTrack, MediaCueEventArgs args)
{
    var dataCue = args.Cue as DataCue;
    if (dataCue != null)
    {
        string scheme_id_uri = string.Empty;
        string value = string.Empty;
        UInt32 timescale = (UInt32)TimeSpan.TicksPerSecond;
        UInt32 presentation_time_delta = (UInt32)dataCue.StartTime.Ticks;
        UInt32 event_duration = (UInt32)dataCue.Duration.Ticks;
        UInt32 id = 0;
        Byte[] message_data = null;

        const string scheme_id_uri_key = "emsg:scheme_id_uri";
        object propValue = null;
        dataCue.Properties.TryGetValue(scheme_id_uri_key, out propValue);
        scheme_id_uri = propValue != null ? (string)propValue : "";

        const string value_key = "emsg:value";
        propValue = null;
        dataCue.Properties.TryGetValue(value_key, out propValue);
        value = propValue != null ? (string)propValue : "";

        const string timescale_key = "emsg:timescale";
        propValue = null;
        dataCue.Properties.TryGetValue(timescale_key, out propValue);
        timescale = propValue != null ? (UInt32)propValue : timescale;

        const string presentation_time_delta_key = "emsg:presentation_time_delta";
        propValue = null;
        dataCue.Properties.TryGetValue(presentation_time_delta_key, out propValue);
        presentation_time_delta = propValue != null ? (UInt32)propValue : presentation_time_delta;

        const string event_duration_key = "emsg:event_duration";
        propValue = null;
        dataCue.Properties.TryGetValue(event_duration_key, out propValue);
        event_duration = propValue != null ? (UInt32)propValue : event_duration;

        const string id_key = "emsg:id";
        propValue = null;
        dataCue.Properties.TryGetValue(id_key, out propValue);
        id = propValue != null ? (UInt32)propValue : 0;

        System.Diagnostics.Debug.WriteLine($"Label: {timedMetadataTrack.Label}, Id: {dataCue.Id}, StartTime: {dataCue.StartTime}, Duration: {dataCue.Duration}");
        System.Diagnostics.Debug.WriteLine($"scheme_id_uri: {scheme_id_uri}, value: {value}, timescale: {timescale}, presentation_time_delta: {presentation_time_delta}, event_duration: {event_duration}, id: {id}");

        if (dataCue.Data != null)
        {
            var dr = Windows.Storage.Streams.DataReader.FromBuffer(dataCue.Data);

            // Check if this is a SCTE ad message:
            // Ref:  http://dashif.org/identifiers/event-schemes/
            if (scheme_id_uri.ToLower() == "urn:scte:scte35:2013:xml")
            {
                // SCTE recommends publishing emsg more than once, so we avoid reprocessing the same message id:
                if (!processedAdIds.Contains(id))
                {
                    processedAdIds.Add(id);
                    dr.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf8;
                    var scte35payload = dr.ReadString(dataCue.Data.Length);
                    System.Diagnostics.Debug.WriteLine($", message_data: {scte35payload}");
                    // TODO: ScheduleAdFromScte35Payload(timedMetadataTrack, presentation_time_delta, timescale, event_duration, scte35payload);
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine($"This emsg.Id, {id}, has already been processed.");
                }
            }
            else
            {
                message_data = new byte[dataCue.Data.Length];
                dr.ReadBytes(message_data);
                // TODO: Use the 'emsg' bytes for something useful. 
                System.Diagnostics.Debug.WriteLine($", message_data.Length: {message_data.Length}");
            }
        }
    }
}