系統支援的定時中繼資料提示
本文說明如何利用數種格式的定時中繼資料,這些中繼資料可內嵌在媒體檔案或串流中。 每當遇到這些中繼資料提示時,UWP 應用程式就可以註冊媒體管線在播放期間引發的事件。 應用程式可以使用 DataCue 類別,實作自己的自訂中繼資料提示,但本文會將重心放在媒體管線自動偵測到的數個中繼資料標準,包括:
- 使用 VobSub 格式的影像式字幕
- 語音提示,包括字詞邊界、語句邊界和語音合成標記語言 (SSML) 書籤
- 章節提示
- 擴充 M3U 註解
- ID3 標籤
- 分散式 mp4 emsg 資料盒
本文以媒體項目、播放清單與曲目一文中討論的概念為基礎,其中包括處理 MediaSource、MediaPlaybackItem 和 TimedMetadataTrack 類別的基本概念,以及在應用程式中使用定時中繼資料的一般指導方針。
本文所述的所有定時中繼資料,即使不同類型,但基本實作步驟都相同:
- 建立 MediaSource,然後建立所要播放內容的 MediaPlaybackItem。
- 註冊 MediaPlaybackItem.TimedMetadataTracksChanged 事件,此事件會在媒體管線解析媒體項目的子追蹤 (Sub-track) 時發生。
- 針對您想要使用的定時中繼資料追蹤 (Metadata Track),註冊 TimedMetadataTrack.CueEntered 和 TimedMetadataTrack.CueExited 事件。
- 在 CueEntered 事件處理常式中,根據事件引數中傳遞的中繼資料來更新 UI。 您可以再次更新 UI,以移除目前的字幕文字,例如 CueExited 事件中的文字。
在本文中,每種中繼資料類型的處理會以不同的案例呈現,但可以使用大部分共通的程式碼來處理 (或忽略) 不同類型的中繼資料。 您在本程序中,有多個時機可檢查 TimedMetadataKind 物件的 TimedMetadataKind 屬性。 因此,例如,您可以選擇註冊 CueEntered 事件來進行具有 TimedMetadataKind.ImageSubtitle 值的中繼資料追蹤,但不進行具有 TimedMetadataKind.Speech 值的追蹤。 或者,您可以註冊一個處理常式來進行所有類型的中繼資料追蹤,然後檢查 CueEntered 處理常式內的 TimedMetadataKind 值,以判斷回應提示時要採取的動作。
影像式字幕
從 Windows 10 版本 1703 開始,UWP 應用程式可以支援使用 VobSub 格式的外部影像式字幕。 若要使用這項功能,請先為要顯示影像式字幕的媒體內容建立 MediaSource 物件。 接下來,呼叫 CreateFromUriWithIndex 或 CreateFromStreamWithIndex,並傳入包含字幕影像資料的 .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 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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。 ImageCue 的 SoftwareBitmap 屬性包含字幕影像的 SoftwareBitmap 表示法。 建立 SoftwareBitmapSource 並呼叫 SetBitmapAsync,將影像指派給 XAML Image 控制項。 ImageCue 的 Extent 和 Position 屬性,會提供字幕影像大小和位置的相關資訊。
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 類別的新執行個體。 將合成器的 IncludeWordBoundaryMetadata 和 IncludeSentenceBoundaryMetadata 選項設定為 true,來指定應該將中繼資料包含在所產生的媒體串流中。 呼叫 SynthesizeTextToStreamAsync 來產生串流,其中包含所合成的語音和對應的中繼資料。 從所合成的串流建立 MediaSource 和 MediaPlaybackItem。
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 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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" 值,如果找到此值,則會使用 SpeechCue 的 StartPositionInInput 和 EndPositionInInInput 屬性,決定在目前所播放之字詞輸入文字內的位置。 此範例直接將每個字詞輸出至偵錯輸出。
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 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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。 ChapterCue 的 Title 屬性包含最近播放到的章節標題。
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 集合,檢查在任何追蹤中,是否有 TimedMetadataKind.Chapter 的 TimedMetadataKind 屬性具備 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 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 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 Helper 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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。 檢查以確定提示的 DataCue 和 Data 屬性不是 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 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 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 標籤事件。 此範例會使用 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 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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 事件的處理常式中,將 MediaCueEventArgs 的 Cue 屬性中包含的資料提示轉換成 DataCue。 檢查以確定提示的 DataCue 和 Data 屬性不是 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 來播放媒體內容。 如需更多資訊,請參閱彈性資料流。 透過呼叫 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 資料盒事件。 此範例會使用 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 方法中,藉由在 MediaPlaybackItem 的 TimedMetadataTracks 集合中編製索引,來取得 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 標識碼的清單,並且只處理新訊息。 透過呼叫 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}");
}
}
}
}
相關主題