미디어 컴퍼지션 및 편집

Windows.Media.Editing 네임스페이스의 API를 사용하면 사용자가 오디오 및 비디오 원본 파일에서 미디어 컴퍼지션을 만들 수 있는 앱을 빠르게 개발할 수 있습니다. 프레임워크의 기능에는 프로그래밍 방식으로 여러 비디오 클립을 함께 추가하고, 비디오 및 이미지 오버레이를 추가하고, 배경 오디오를 추가하고, 오디오 및 비디오 효과를 모두 적용하는 기능이 포함됩니다. 만든 후에는 재생 또는 공유를 위해 미디어 컴퍼지션을 플랫 미디어 파일로 렌더링할 수 있지만, 컴퍼지션을 디스크에서 직렬화하고 역직렬화하여 사용자가 이전에 만든 컴퍼지션을 로드하고 수정할 수 있습니다. 이 모든 기능은 사용이 간편한 Windows 런타임 인터페이스에서 제공되므로 하위 수준 Microsoft Media Foundation API와 비교할 때 이러한 작업을 수행하는 데 필요한 코드의 양과 복잡성을 크게 줄일 수 있습니다.

새 미디어 컴퍼지션 만들기

MediaComposition 클래스는 컴퍼지션을 구성하는 모든 미디어 클립의 컨테이너이며 최종 컴퍼지션을 렌더링하고, 컴퍼지션을 디스크에 로드 및 저장하고, 사용자가 UI에서 볼 수 있도록 컴퍼지션의 미리 보기 스트림을 제공합니다. 앱에서 MediaComposition을 사용하려면 필요한 관련 API를 제공하는 Windows.Media.Core 네임스페이스뿐만 아니라 Windows.Media.Editing 네임스페이스를 포함합니다.

using Windows.Media.Editing;
using Windows.Media.Core;
using Windows.Media.Playback;
using System.Threading.Tasks;

MediaComposition 개체는 코드의 여러 지점에서 액세스되므로 일반적으로 저장할 멤버 변수를 선언합니다.

private MediaComposition composition;

MediaComposition의 생성자는 인수를 사용하지 않습니다.

composition = new MediaComposition();

컴퍼지션에 미디어 클립 추가

미디어 컴퍼지션은 일반적으로 하나 이상의 비디오 클립을 포함합니다. FileOpenPicker를 사용하여 사용자가 비디오 파일을 선택할 수 있도록 할 수 있습니다. 파일이 선택되면 MediaClip.CreateFromFileAsync를 호출하여 비디오 클립을 포함할 새 MediaClip 개체를 만듭니다. 그런 다음 MediaComposition 개체의 Clips 목록에 클립을 추가합니다.

private async Task PickFileAndAddClip()
{
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
    picker.FileTypeFilter.Add(".mp4");
    Windows.Storage.StorageFile pickedFile = await picker.PickSingleFileAsync();
    if (pickedFile == null)
    {
        ShowErrorMessage("File picking cancelled");
        return;
    }

    // These files could be picked from a location that we won't have access to later
    var storageItemAccessList = Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList;
    storageItemAccessList.Add(pickedFile);

    var clip = await MediaClip.CreateFromFileAsync(pickedFile);
    composition.Clips.Add(clip);

}
  • 미디어 클립은 Clips 목록에 표시되는 순서와 동일한 순서로 MediaComposition에 표시됩니다.

  • MediaClip은 컴퍼지션에 한 번만 포함될 수 있습니다. 컴퍼지션에서 이미 사용 중인 MediaClip을 추가하려고 하면 오류가 발생합니다. 컴퍼지션에서 비디오 클립을 여러 번 다시 사용하려면 Clone을 호출하여 컴퍼지션에 추가할 수 있는 새 MediaClip 개체를 만듭니다.

  • 유니버설 Windows 앱에는 전체 파일 시스템에 액세스할 수 있는 권한이 없습니다. StorageApplicationPermissions 클래스의 FutureAccessList 속성을 사용하면 앱에서 사용자가 선택한 파일의 레코드를 저장하여 파일에 액세스할 수 있는 권한을 유지할 수 있습니다. FutureAccessList에는 최대 1,000개의 항목이 있으므로 앱이 가득 차지 않도록 목록을 관리해야 합니다. 이는 이전에 만든 컴퍼지션 로드 및 수정을 지원하려는 경우에 특히 중요합니다.

  • MediaComposition은 MP4 형식의 동영상 클립을 지원합니다.

  • 비디오 파일에 포함된 오디오 트랙이 여러 개 있는 경우 SelectedEmbeddedAudioTrackIndex 속성을 설정하여 컴퍼지션에 사용되는 오디오 트랙을 선택할 수 있습니다.

  • CreateFromColor를 호출하고 클립의 색과 기간을 지정하여 전체 프레임을 채우는 단일 색으로 MediaClip을 만듭니다.

  • CreateFromImageFileAsync를 호출하고 이미지 파일 및 클립 기간을 지정하여 이미지 파일에서 MediaClip을 만듭니다.

  • CreateFromSurface를 호출하고 클립에서 표면 및 기간을 지정하여 IDirect3DSurface에서 MediaClip을 만듭니다.

MediaElement에서 컴퍼지션 미리 보기

사용자가 미디어 컴퍼지션을 볼 수 있도록 하려면 UI를 정의하는 XAML 파일에 MediaPlayerElement를 추가합니다.

<MediaPlayerElement x:Name="mediaPlayerElement" AutoPlay="False" Margin="5" HorizontalAlignment="Stretch" AreTransportControlsEnabled="True" />

MediaStreamSource 형식의 멤버 변수를 선언합니다.

private MediaStreamSource mediaStreamSource;

MediaComposition 개체의 GeneratePreviewMediaStreamSource 메서드를 호출하여 컴퍼지션에 대한 MediaStreamSource를 만듭니다. 팩터리 메서드 CreateFromMediaStreamSource를 호출하여 MediaSource 개체를 만들고 MediaPlayerElementSource 속성에 할당합니다. 이제 UI에서 컴퍼지션을 볼 수 있습니다.

public void UpdateMediaElementSource()
{

    mediaStreamSource = composition.GeneratePreviewMediaStreamSource(
        (int)mediaPlayerElement.ActualWidth,
        (int)mediaPlayerElement.ActualHeight);

    mediaPlayerElement.Source = MediaSource.CreateFromMediaStreamSource(mediaStreamSource);

}
  • MediaCompositionGeneratePreviewMediaStreamSource를 호출하기 전에 하나 이상의 미디어 클립을 포함해야 합니다. 그렇지 않으면 반환된 개체가 null이 됩니다.

  • MediaElement 타임라인은 컴퍼지션의 변경 내용을 반영하도록 자동으로 업데이트되지 않습니다. 컴퍼지션을 변경하고 UI를 업데이트할 때마다 GeneratePreviewMediaStreamSource를 호출하고 MediaPlayerElementSource 속성을 설정하는 것이 좋습니다.

연결된 리소스를 해제하기 위해 사용자가 페이지에서 벗어나면 MediaStreamSource 개체 및 MediaPlayerElementSource 속성을 null로 설정하는 것이 좋습니다.

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    mediaPlayerElement.Source = null;
    mediaStreamSource = null;
    base.OnNavigatedFrom(e);

}

비디오 파일에 컴퍼지션 렌더링

미디어 컴퍼지션을 플랫 비디오 파일로 렌더링하여 다른 디바이스에서 공유하고 볼 수 있도록 하려면 Windows.Media.Transcoding 네임스페이스의 API를 사용해야 합니다. 비동기 작업의 진행률에 따라 UI를 업데이트하려면 Windows.UI.Core 네임스페이스의 API도 필요합니다.

using Windows.Media.Transcoding;
using Windows.UI.Core;

사용자가 FileSavePicker를 사용하여 출력 파일을 선택할 수 있도록 허용한 후 MediaComposition 개체의 RenderToFileAsync를 호출하여 선택한 파일에 컴퍼지션을 렌더링합니다. 다음 예제의 나머지 코드는 AsyncOperationWithProgress를 처리하는 패턴을 따릅니다.

private async Task RenderCompositionToFile()
{
    var picker = new Windows.Storage.Pickers.FileSavePicker();
    picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
    picker.FileTypeChoices.Add("MP4 files", new List<string>() { ".mp4" });
    picker.SuggestedFileName = "RenderedComposition.mp4";

    Windows.Storage.StorageFile file = await picker.PickSaveFileAsync();
    if (file != null)
    {
        // Call RenderToFileAsync
        var saveOperation = composition.RenderToFileAsync(file, MediaTrimmingPreference.Precise);

        saveOperation.Progress = new AsyncOperationProgressHandler<TranscodeFailureReason, double>(async (info, progress) =>
        {
            await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(() =>
            {
                ShowErrorMessage(string.Format("Saving file... Progress: {0:F0}%", progress));
            }));
        });
        saveOperation.Completed = new AsyncOperationWithProgressCompletedHandler<TranscodeFailureReason, double>(async (info, status) =>
        {
            await this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(() =>
            {
                try
                {
                    var results = info.GetResults();
                    if (results != TranscodeFailureReason.None || status != AsyncStatus.Completed)
                    {
                        ShowErrorMessage("Saving was unsuccessful");
                    }
                    else
                    {
                        ShowErrorMessage("Trimmed clip saved to file");
                    }
                }
                finally
                {
                        // Update UI whether the operation succeeded or not
                    }

            }));
        });
    }
    else
    {
        ShowErrorMessage("User cancelled the file selection");
    }
}
  • MediaTrimmingPreference를 사용하면 코드 변환 작업의 속도와 인접한 미디어 클립의 트리밍 정밀도에 우선 순위를 지정할 수 있습니다. 정밀도가 낮은 트리밍을 사용하면 빠른 경우 코드 변환이 빨라지고 정확한 경우 코드 변환 속도가 느려지지만 트리밍이 더 정확합니다.

동영상 클립 다듬기

MediaClip 개체 TrimTimeFromStart 속성, TrimTimeFromEnd 속성 또는 둘 다를 설정하여 컴퍼지션에서 비디오 클립의 기간을 트리밍합니다.

private void TrimClipBeforeCurrentPosition()
{
    var currentClip = composition.Clips.FirstOrDefault(
        mc => mc.StartTimeInComposition <= mediaPlayerElement.MediaPlayer.PlaybackSession.Position &&
        mc.EndTimeInComposition >= mediaPlayerElement.MediaPlayer.PlaybackSession.Position);

    TimeSpan positionFromStart = mediaPlayerElement.MediaPlayer.PlaybackSession.Position - currentClip.StartTimeInComposition;
    currentClip.TrimTimeFromStart = positionFromStart;

}
  • UI를 사용하여 사용자는 시작 및 끝 트리밍 값을 지정할 수 있습니다. 위의 예제에서는 MediaPlayerElement와 연결된 MediaPlaybackSessionPosition 속성을 사용하여 먼저 StartTimeInCompositionEndTimeInComposition을 검사 컴퍼지션의 현재 위치에서 재생 중인 MediaClip을 확인합니다. 그런 다음 PositionStartTimeInComposition 속성을 다시 사용하여 클립의 시작 부분에서 트리밍할 시간을 계산합니다. FirstOrDefault 메서드는 목록에서 항목을 선택하기 위한 코드를 간소화하는 System.Linq 네임스페이스의 확장 메서드입니다.
  • MediaClip 개체의 OriginalDuration 속성을 사용하면 클리핑을 적용하지 않고 미디어 클립의 기간을 알 수 있습니다.
  • TrimmedDuration 속성을 사용하면 트리밍이 적용된 후 미디어 클립의 기간을 알 수 있습니다.
  • 클립의 원래 지속 시간보다 큰 트리밍 값을 지정해도 오류가 발생하지 않습니다. 그러나 컴퍼지션에 단일 클립만 포함되고 큰 트리밍 값을 지정하여 길이가 0으로 잘리는 경우 컴퍼지션에 클립이 없는 것처럼 GeneratePreviewMediaStreamSource에 대한 후속 호출은 null을 반환합니다.

컴퍼지션에 배경 오디오 트랙 추가

컴퍼지션에 백그라운드 트랙을 추가하려면 오디오 파일을 로드한 다음 Factory 메서드 BackgroundAudioTrack.CreateFromFileAsync를 호출하여 BackgroundAudioTrack 개체를 만듭니다. 그런 다음, BackgroundAudioTrack을 컴퍼지션의 BackgroundAudioTracks 속성에 추가합니다.

private async Task AddBackgroundAudioTrack()
{
    // Add background audio
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.MusicLibrary;
    picker.FileTypeFilter.Add(".mp3");
    picker.FileTypeFilter.Add(".wav");
    picker.FileTypeFilter.Add(".flac");
    Windows.Storage.StorageFile audioFile = await picker.PickSingleFileAsync();
    if (audioFile == null)
    {
        ShowErrorMessage("File picking cancelled");
        return;
    }

    // These files could be picked from a location that we won't have access to later
    var storageItemAccessList = Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList;
    storageItemAccessList.Add(audioFile);

    var backgroundTrack = await BackgroundAudioTrack.CreateFromFileAsync(audioFile);

    composition.BackgroundAudioTracks.Add(backgroundTrack);

}
  • MediaComposition은 MP3, WAV, FLAC 형식의 배경 오디오 트랙을 지원합니다.

  • 배경 오디오 트랙

  • 비디오 파일과 마찬가지로 StorageApplicationPermissions 클래스를 사용하여 컴퍼지션의 파일에 대한 액세스를 유지해야 합니다.

  • MediaClip과 마찬가지로 BackgroundAudioTrack은 컴퍼지션에 한 번만 포함될 수 있습니다. 컴퍼지션에서 이미 사용 중인 BackgroundAudioTrack을 추가하려고 하면 오류가 발생합니다. 컴퍼지션에서 오디오 트랙을 여러 번 다시 사용하려면 Clone을 호출하여 컴퍼지션에 추가할 수 있는 새 MediaClip 개체를 만듭니다.

  • 기본적으로 배경 오디오 트랙은 컴퍼지션 시작 시 재생되기 시작합니다. 여러 배경 트랙이 있는 경우 모든 트랙은 컴퍼지션 시작 시 재생되기 시작합니다. 백그라운드 오디오 트랙이 다른 시간에 재생을 시작하도록 하려면 Delay 속성을 원하는 시간 오프셋으로 설정합니다.

컴퍼지션에 오버레이 추가

오버레이를 사용하면 여러 비디오 계층을 컴퍼지션에서 서로 위에 쌓을 수 있습니다. 컴퍼지션에는 여러 오버레이 계층이 포함될 수 있으며, 각 레이어에는 여러 오버레이가 포함될 수 있습니다. MediaClip을 생성자에 전달하여 MediaOverlay 개체를 만듭니다. 오버레이의 위치와 불투명도를 설정한 다음, 새 MediaOverlayLayer를 만들고 MediaOverlayOverlays 목록에 추가합니다. 마지막으로 MediaOverlayLayer를 컴퍼지션의 OverlayLayers 목록에 추가합니다.

private void AddOverlay(MediaClip overlayMediaClip, double scale, double left, double top, double opacity)
{
    Windows.Media.MediaProperties.VideoEncodingProperties encodingProperties =
        overlayMediaClip.GetVideoEncodingProperties();

    Rect overlayPosition;

    overlayPosition.Width = (double)encodingProperties.Width * scale;
    overlayPosition.Height = (double)encodingProperties.Height * scale;
    overlayPosition.X = left;
    overlayPosition.Y = top;

    MediaOverlay mediaOverlay = new MediaOverlay(overlayMediaClip);
    mediaOverlay.Position = overlayPosition;
    mediaOverlay.Opacity = opacity;

    MediaOverlayLayer mediaOverlayLayer = new MediaOverlayLayer();
    mediaOverlayLayer.Overlays.Add(mediaOverlay);

    composition.OverlayLayers.Add(mediaOverlayLayer);
}
  • 레이어 내의 오버레이는 포함된 레이어의 Overlays 목록에서 순서에 따라 z 순서가 지정됩니다. 목록 내의 높은 인덱스는 낮은 인덱스 위에 렌더링됩니다. 컴퍼지션 내의 오버레이 레이어도 마찬가지입니다. 컴퍼지션의 OverlayLayers 목록에서 인덱스가 더 높은 레이어는 낮은 인덱스 위에 렌더링됩니다.

  • 오버레이는 순차적으로 재생되지 않고 서로 위에 쌓이기 때문에 모든 오버레이는 기본적으로 컴퍼지션의 시작 부분에서 재생을 시작합니다. 오버레이가 다른 시간에 재생을 시작하도록 하려면 Delay 속성을 원하는 시간 오프셋으로 설정합니다.

미디어 클립에 효과 추가

컴퍼지션의 각 MediaClip에는 여러 효과를 추가할 수 있는 오디오 및 비디오 효과 목록이 있습니다. 효과는 각각 IAudioEffectDefinitionIVideoEffectDefinition을 구현해야 합니다. 다음 예제에서는 현재 MediaPlayerElement 위치를 사용하여 현재 표시된 MediaClip을 선택한 다음 VideoStabilizationEffectDefinition의 새 인스턴스를 만들고 미디어 클립의 VideoEffectDefinitions 목록에 추가합니다.

private void AddVideoEffect()
{
    var currentClip = composition.Clips.FirstOrDefault(
        mc => mc.StartTimeInComposition <= mediaPlayerElement.MediaPlayer.PlaybackSession.Position &&
        mc.EndTimeInComposition >= mediaPlayerElement.MediaPlayer.PlaybackSession.Position);

    VideoStabilizationEffectDefinition videoEffect = new VideoStabilizationEffectDefinition();
    currentClip.VideoEffectDefinitions.Add(videoEffect);
}

컴포지션을 파일에 저장

미디어 컴포지션을 파일로 직렬화하여 나중에 수정할 수 있습니다. 출력 파일을 선택한 다음 MediaComposition 메서드 SaveAsync를 호출하여 컴퍼지션을 저장합니다.

private async Task SaveComposition()
{
    var picker = new Windows.Storage.Pickers.FileSavePicker();
    picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
    picker.FileTypeChoices.Add("Composition files", new List<string>() { ".cmp" });
    picker.SuggestedFileName = "SavedComposition";

    Windows.Storage.StorageFile compositionFile = await picker.PickSaveFileAsync();
    if (compositionFile == null)
    {
        ShowErrorMessage("User cancelled the file selection");
    }
    else
    {
        var action = composition.SaveAsync(compositionFile);
        action.Completed = (info, status) =>
        {
            if (status != AsyncStatus.Completed)
            {
                ShowErrorMessage("Error saving composition");
            }

        };
    }
}

파일에서 컴포지션 불러오기

사용자가 컴퍼지션을 보고 수정할 수 있도록 파일에서 미디어 컴퍼지션을 역직렬화할 수 있습니다. 컴퍼지션 파일을 선택한 다음 MediaComposition 메서드 LoadAsync를 호출하여 컴퍼지션을 로드합니다.

private async Task OpenComposition()
{
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
    picker.FileTypeFilter.Add(".cmp");

    Windows.Storage.StorageFile compositionFile = await picker.PickSingleFileAsync();
    if (compositionFile == null)
    {
        ShowErrorMessage("File picking cancelled");
    }
    else
    {
        composition = null;
        composition = await MediaComposition.LoadAsync(compositionFile);

        if (composition != null)
        {
            UpdateMediaElementSource();

        }
        else
        {
            ShowErrorMessage("Unable to open composition");
        }
    }
}
  • 컴퍼지션의 미디어 파일이 앱에서 액세스할 수 있는 위치에 있지 않고 앱에 대한 StorageApplicationPermissions 클래스의 FutureAccessList 속성에 없는 경우 컴퍼지션을 로드할 때 오류가 throw됩니다.