系統支援的定時中繼資料提示

本文說明如何利用數種格式的定時中繼資料,這些中繼資料可內嵌在媒體檔案或串流中。 每當遇到這些中繼資料提示時,UWP 應用程式就可以註冊媒體管線在播放期間引發的事件。 應用程式可以使用 DataCue 類別,實作自己的自訂中繼資料提示,但本文會將重心放在媒體管線自動偵測到的數個中繼資料標準,包括:

  • 使用 VobSub 格式的影像式字幕
  • 語音提示,包括字詞邊界、語句邊界和語音合成標記語言 (SSML) 書籤
  • 章節提示
  • 擴充 M3U 註解
  • ID3 標籤
  • 分散式 mp4 emsg 資料盒

本文以媒體項目、播放清單與曲目一文中討論的概念為基礎,其中包括處理 MediaSourceMediaPlaybackItemTimedMetadataTrack 類別的基本概念,以及在應用程式中使用定時中繼資料的一般指導方針。

本文所述的所有定時中繼資料,即使不同類型,但基本實作步驟都相同:

  1. 建立 MediaSource,然後建立所要播放內容的 MediaPlaybackItem
  2. 註冊 MediaPlaybackItem.TimedMetadataTracksChanged 事件,此事件會在媒體管線解析媒體項目的子追蹤 (Sub-track) 時發生。
  3. 針對您想要使用的定時中繼資料追蹤 (Metadata Track),註冊 TimedMetadataTrack.CueEnteredTimedMetadataTrack.CueExited 事件。
  4. CueEntered 事件處理常式中,根據事件引數中傳遞的中繼資料來更新 UI。 您可以再次更新 UI,以移除目前的字幕文字,例如 CueExited 事件中的文字。

在本文中,每種中繼資料類型的處理會以不同的案例呈現,但可以使用大部分共通的程式碼來處理 (或忽略) 不同類型的中繼資料。 您在本程序中,有多個時機可檢查 TimedMetadataKind 物件的 TimedMetadataKind 屬性。 因此,例如,您可以選擇註冊 CueEntered 事件來進行具有 TimedMetadataKind.ImageSubtitle 值的中繼資料追蹤,但不進行具有 TimedMetadataKind.Speech 值的追蹤。 或者,您可以註冊一個處理常式來進行所有類型的中繼資料追蹤,然後檢查 CueEntered 處理常式內的 TimedMetadataKind 值,以判斷回應提示時要採取的動作。

影像式字幕

從 Windows 10 版本 1703 開始,UWP 應用程式可以支援使用 VobSub 格式的外部影像式字幕。 若要使用這項功能,請先為要顯示影像式字幕的媒體內容建立 MediaSource 物件。 接下來,呼叫 CreateFromUriWithIndexCreateFromStreamWithIndex,並傳入包含字幕影像資料的 .sub 檔案 URI,以及包含字幕時間資訊的 .idx 檔案,來建立 TimedTextSource 物件。 將 TimedTextSource 新增至來源的 ExternalTimedTextSources 集合,藉此將它新增至 MediaSource。 從 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 Helper 方法中,藉由在 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,請將 MediaCueEventArgsCue 屬性所包含的資料提示,轉換成 ImageCueImageCueSoftwareBitmap 屬性包含字幕影像的 SoftwareBitmap 表示法。 建立 SoftwareBitmapSource 並呼叫 SetBitmapAsync,將影像指派給 XAML Image 控制項。 ImageCueExtentPosition 屬性,會提供字幕影像大小和位置的相關資訊。

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 物件註冊語音中繼資料事件。 此範例會使用 Helper 方法 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 Helper 方法中,藉由在 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,請將 MediaCueEventArgsCue 屬性所包含的資料提示,轉換成 SpeechCue。 針對語音提示,中繼資料追蹤包含的語音提示類型,將由檢查 Label 屬性來決定。 此屬性的值將是表示字詞邊界的 "SpeechWord"、表示語句邊界的 "SpeechSentence",或是表示 SSML 書籤的 "SpeechBookmark"。 在此範例中,我們會檢查 "SpeechWord" 值,如果找到此值,則會使用 SpeechCueStartPositionInInputEndPositionInInInput 屬性,決定在目前所播放之字詞輸入文字內的位置。 此範例直接將每個字詞輸出至偵錯輸出。

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 Helper 方法中,藉由在 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,請將 MediaCueEventArgsCue 屬性所包含的資料提示,轉換成 ChapterCueChapterCueTitle 屬性包含最近播放到的章節標題。

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;
            });
        }
    }
}

使用章節提示尋找下一章

除了在播放項目中的目前章節變更時收到通知之外,您也可以使用章節提示來搜尋播放項目內的下一章。 下面顯示的範例方法,將 MediaPlayerMediaPlaybackItem (代表目前所播放媒體項目) 當成引數。 系統會搜尋 TimedMetadataTracks 集合,檢查在任何追蹤中,是否有 TimedMetadataKind.ChapterTimedMetadataKind 屬性具備 TimedMetadataTrack 值。 如果找到章節追蹤,方法會對追蹤的 Cues 提示集合循環處理每個提示,以尋找 StartTime 大於 Position (媒體播放器播放工作階段的目前位置) 的第一個提示。 找到正確的提示之後,播放工作階段的位置就會更新,並在 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 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 CreateFromUriAsyncCreateFromStreamAsync 來建立內容的 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 Helper 方法中,藉由在 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 事件的處理常式中,將 MediaCueEventArgsCue 屬性中包含的資料提示轉換成 DataCue。 檢查以確定提示的 DataCueData 屬性不是 Null。 擴充 EMU 註解是以 UTF-16、Little Endian、Null 結尾的字串來構成。 透過呼叫 DataReader.FromBuffer,建立新的 DataReader 來讀取提示資料。 將讀取器的 UnicodeEncoding 屬性設定為 Utf16LE,以正確的格式來讀取資料。 呼叫 ReadString 以讀取資料,並指定 Data 欄位的一半長度 (因為每個字元的大小都是兩個位元組),接著減去一 (1) 以移除結尾的 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 即時串流 (HLS) 內容當中的 ID3 標籤。 此範例會使用 AdaptiveMediaSource 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 CreateFromUriAsyncCreateFromStreamAsync 來建立內容的 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 標籤事件。 此範例會使用 Helper 方法 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 Helper 方法中,藉由在 MediaPlaybackItemTimedMetadataTracks 集合中編製索引,來取得 TimedMetadataTrack 類別的執行個體。 檢查中繼資料追蹤的 DispatchType 屬性,如果追蹤代表 ID3 標籤,則會有包含 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 事件的處理常式中,將 MediaCueEventArgsCue 屬性中包含的資料提示轉換成 DataCue。 檢查以確定提示的 DataCueData 屬性不是 Null。 擴充 EMU 註解是以傳輸串流中原始位元組的形式提供 (請參閱 ID3)。 透過呼叫 DataReader.FromBuffer,建立新的 DataReader 來讀取提示資料。 在此範例中,系統會從提示資料讀取 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 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 CreateFromUriAsyncCreateFromStreamAsync 來建立內容的 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 資料盒事件。 此範例會使用 Helper 方法 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 資料盒中繼資料事件之後,會將 MediaItem 指派給 MediaPlayer,以便在 MediaPlayerElement 內播放。

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

RegisterMetadataHandlerForEmsgCues Helper 方法中,藉由在 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 事件的處理常式中,將 MediaCueEventArgsCue 屬性中包含的資料提示轉換成 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 標識碼的清單,並且只處理新訊息。 透過呼叫 DataReader.FromBuffer 來建立讀取提示資料的新 DataReader,並透過設定 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}");
            }
        }
    }
}