시스템에서 지원하는 시간이 제한된 메타데이터 신호

이 문서에서는 미디어 파일 또는 스트림에 포함될 수 있는 시간이 지정된 여러 형식의 메타데이터를 활용하는 방법을 설명합니다. UWP 앱은 이러한 메타데이터 큐가 발견될 때마다 재생하는 동안 미디어 파이프라인에 의해 발생하는 이벤트를 등록할 수 있습니다. DataCue 클래스를 사용하여 앱은 자체 사용자 지정 메타데이터 큐를 구현할 수 있지만 이 문서는 다음과 같은 미디어 파이프라인이 자동으로 검색하는 몇 가지 메타데이터 표준에 초점을 맞춥니다.

  • VobSub 형식의 이미지 기반 자막
  • 단어 범위, 문장 경계 및 SSML(Speech Synthesis Markup Language) 책갈피를 포함한 음성 큐
  • 챕터 큐
  • 확장된 M3U 주석
  • ID3 태그
  • 조각화된 mp4 emsg 상자

이 문서는 MediaSource, MediaPlaybackItemTimedMetadataTrack 클래스를 포함하는 미디어 항목, 재생 목록 및 트랙 문서 및 앱에서 시간이 제한된 메타데이터를 사용하는 일반 가이드에서 설명한 개념을 기반으로 합니다.

기본 구현 단계는 이 문서에서 설명하는 모든 다른 종류의 시간이 제한된 메타데이터에 대해 동일합니다.

  1. MediaSourceMediaPlaybackItem을 만들어 콘텐츠를 재생합니다.
  2. 미디어 파이프라인에 의해 미디어 항목의 하위 트랙이 확인될 때 발생하는 MediaPlaybackItem.TimedMetadataTracksChanged 이벤트를 등록합니다.
  3. 사용하고자 하는 시간이 제한된 메타데이터 트랙에 대한 TimedMetadataTrack.CueEnteredTimedMetadataTrack.CueExited 이벤트를 등록합니다.
  4. CueEntered 이벤트 처리기에서 이벤트 인수에 전달된 메타데이터를 기반으로 UI를 업데이트합니다. 예를 들어, 현재 자막 텍스트를 제거하려면 CueExited 이벤트에서 UI를 다시 업데이트할 수 있습니다.

이 문서에서는 고유한 시나리오로 각 유형의 메타 데이터를 처리하지만 대부분 공유 코드를 사용하여 다른 유형의 메타데이터를 처리(또는 무시)할 수 있습니다. 프로세스의 다중 포인트에서 TimedMetadataTrack 개체의 TimedMetadataKind 속성을 확인할 수 있습니다. 따라서 예를 들어, TimedMetadataKind.ImageSubtitle 값을 지닌 메타 데이터 트랙에 대해 CueEntered 이벤트를 등록하도록 선택할 수 있지만 TimedMetadataKind.Speech 값을 지닌 트랙에 대해서는 불가능합니다. 또는 모든 메타데이터 트랙 형식을 등록한 다음, CueEntered 처리기 내에 있는 TimedMetadataKind 값을 확인하여 큐에 대한 응답으로 수행할 작업을 결정할 수 있습니다.

이미지 기반 자막

Windows 10 버전 1703부터 UWP 앱은 VobSub 형식의 외부 이미지 기반 자막을 지원할 수 있습니다. 이 기능을 사용하려면 먼저 이미지 자막이 표시될 미디어 콘텐츠에 대한 MediaSource 개체를 만듭니다. 다음으로 CreateFromUriWithIndex 또는 CreateFromStreamWithIndex를 호출하여 자막 이미지 데이터가 포함된 .sub 파일 및 자막의 타이밍 정보가 포함된 .idx 파일의 URI를 전달함으로써 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 개체를 사용하여 이미지 자막 메타데이터 이벤트를 등록합니다. 이 예제는 이벤트에 등록하기 위해 RegisterMetadataHandlerForImageSubtitles 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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);
}

이미지 자막 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생을 위해 MediaPlayer에 할당됩니다.

_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 속성에 포함된 데이터 큐를 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(Speech Synthesis Markup Language) 책갈피에 응답하는 이벤트를 수신하도록 등록할 수 있습니다. 이를 사용하여 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 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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);
}

음성 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생을 위해 MediaPlayer에 할당됩니다.

_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 속성에 포함된 데이터 큐를 SpeechCue에 캐스팅합니다. 음성 큐의 경우 메타데이터 트랙에 포함된 음성 큐의 유형은 레이블 속성을 통해 확인할 수 있습니다. 이 속성 값은 문장 경계에 대해 "SpeechWord", 문장 경계에 대해 "SpeechSentence", 또는 SSML 책갈피에 대해 "SpeechBookmark"입니다. 이 예제에서는 "SpeechWord" 값을 확인하고 값이 발견되는 경우 SpeechCueStartPositionInInputEndPositionInInput 속성은 현재 재생되는 단어의 입력 텍스트 내 위치를 결정하는 데 사용됩니다. 이 예제는 단순히 각 단어를 디버그 출력으로 출력합니다.

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 개체를 사용하여 챕터 메타데이터 이벤트를 등록합니다. 이 예제는 이벤트에 등록하기 위해 RegisterMetadataHandlerForChapterCues 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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);
}

챕터 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생에 대해 MediaPlayer에 할당됩니다.

_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 속성에 포함된 데이터 큐를 ChapterCue에 캐스팅합니다. ChapterCue제목 속성은 재생에 막 도달한 챕터 제목을 포함합니다.

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을 인수로 나타냅니다. 어떤 트랙이라도 TimedMetadataKind.ChapterTimedMetadataTrack 값의 TimedMetadataKind 속성을 지니는지 확인하기 위해 TimedMetadataTracks 컬렉션이 검색됩니다. 챕터 트랙이 발견되면 메서드는 트랙의 컬렉션을 루핑하여 미디어 플레이어의 재생 세션의 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 메타데이터 이벤트를 등록합니다. 이 예제는 이벤트에 등록하기 위해 RegisterMetadataHandlerForEXTM3UCues 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생에 대해 MediaPlayer에 할당됩니다.

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

RegisterMetadataHandlerForEXTM3UCues 도우미 메서드에서 MediaPlaybackItemTimedMetadataTracks 컬렉션으로 인덱싱하여 TimedMetadataTrack 클래스의 인스턴스를 가져옵니다. 트랙이 M3U 주석을 나타내는 경우 "EXTM3U" 값을 갖는 메타데이터 트랙의 DispatchType 속성을 확인합니다. 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을 호출하여 데이터를 읽고 각 문자의 크기는 2바이트이기 때문에 데이터 필드의 길이를 반으로 지정하고 하나를 빼 후행 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 앱은 HLS(Http 라이브 스트리밍) 콘텐츠 내의 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 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생에 대해 MediaPlayer에 할당됩니다.

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

RegisterMetadataHandlerForID3Cues 도우미 메서드에서 MediaPlaybackItemTimedMetadataTracks 컬렉션으로 인덱싱하여 TimedMetadataTrack 클래스의 인스턴스를 가져옵니다. 트랙이 ID3 태그를 나타내는 경우 "15260DFFFF49443320FF49443320000F" GUID 문자열을 포함하는 값을 지닌 메타데이터 트랙의 DispatchType 속성을 확인합니다. 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를 사용하여 미디어 콘텐츠를 재생합니다. 자세한 내용은 적응 스트리밍을 참조하세요. 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 상자 이벤트를 등록합니다. 이 예제는 이벤트에 등록하기 위해 RegisterMetadataHandlerForEmsgCues 도우미 메서드를 사용합니다. 람다 식은 시스템이 MediaPlaybackItem과 관련된 메타데이터 트랙의 변경을 발견할 때 발생하는 TimedMetadataTracksChanged 이벤트에 대한 처리기를 구현하는 데 사용됩니다. 경우에 따라 메타데이터 트랙은 재생 항목이 처음 지정될 때 사용 가능하기 때문에 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 상자 메타데이터 이벤트를 등록하면 MediaItemMediaPlayerElement 내 재생에 대해 MediaPlayer에 할당됩니다.

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

RegisterMetadataHandlerForEmsgCues 도우미 메서드에서 MediaPlaybackItemTimedMetadataTracks 컬렉션으로 인덱싱하여 TimedMetadataTrack 클래스의 인스턴스를 가져옵니다. 트랙이 emsg 상자를 나타내는 경우 "emsg:mp4" 값을 갖는 메타데이터 트랙의 DispatchType 속성을 확인합니다. 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 개체의 속성 컬렉션 내에 사용자 지정 속성으로 제공됩니다. 이 예제는 TryGetValue 메서드를 사용하여 여러 다른 속성 값을 추출해 봅니다. 이 메서드가 null을 반환하면 요청한 속성이 emsg 상자에 나타나지 않는다는 의미이므로 대신 기본값이 설정됩니다.

이 예제의 다음 부분은 광고 재생이 트리거되는 시나리오를 설명하는데, 이 경우는 이전 단계에서 가져온 scheme_id_uri 속성의 값이 "urn:scte:scte35:2013:xml"입니다. 자세한 내용은 https://dashif.org/identifiers/event_schemes/를 참조하세요. 표준 방법으로 중복성에 대해 이 emsg를 여러 번 전송하는 것이 권장되므로 이 예제는 이미 처리된 emsg ID의 목록을 유지하고 새 메시지만 처리합니다. 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}");
            }
        }
    }
}