媒體組合和編輯

本文說明如何使用 Windows.Media.Editing 命名空間中的 API 快速開發讓使用者能夠從音訊和視訊來源檔案建立媒體組合的應用程式。 架構的功能包括能夠以程式設計方式將多個視訊短片附加在一起、新增視訊和影像重疊、新增背景音訊,以及同時套用音訊和視訊效果。 一旦建立之後,媒體組合就可以轉譯成一般媒體檔案進行播放或共用,但組合也可以序列化為磁碟並還原序列化,讓使用者能夠載入和修改先前建立的組合。 所有這些功能都在易於使用的 Windows 執行時間介面中提供,與低階 Microsoft Media Foundation 相比,該介面大大減少了執行這些工作所需的程式碼數量和複雜性。

建立新的媒體組合

MediaComposition 類別是組成組合的所有媒體短片的容器,負責轉譯最終組成、將組成載入和儲存到光碟,並提供組合的預覽串流,以便使用者可以在使用者介面。 若要在應用程式中使用 MediaComposition,請包含 Windows.Media.Editing 命名空間以及提供您所需的相關 API 的 Windows.Media.Core 命名空間。

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

}
  • 媒體短片在 MediaComposition 中的顯示順序與它們在 Clips 清單中的顯示順序相同。

  • MediaClip 只能包含在組合中一次。 嘗試新增組合已在使用的 MediaClip 將導致錯誤。 若要在組合中重複使用視訊短片,請呼叫 Clone 以建立新的 MediaClip 物件,然後將其新增至組合中。

  • 通用 Windows 應用程式沒有存取整個檔案系統的權限。 StorageApplicationPermissions 類別的 FutureAccessList 屬性可讓您的應用程式儲存使用者已選取的檔案的記錄,以便您可以保留存取該檔案的權限。 FutureAccessList 最多有 1000 個項目,因此您的應用程式需要管理該清單以確保它不會變滿。 如果您打算支援載入和修改先前建立的組合,這特別重要。

  • MediaComposition 能支援 MP4 格式的視訊短片。

  • 如果視訊檔案包含多個內嵌音訊曲目,您可以藉由設定 SelectedEmbeddedAudioTrackIndex 屬性來選取組合中使用的曲目。

  • 透過呼叫 CreateFromColor 並指定短片的色彩和持續時間,建立一個填滿整個畫面的單一色彩的 MediaClip

  • 透過呼叫 CreateFromImageFileAsync 並指定影像檔和短片的持續時間,從影像檔建立 MediaClip

  • 透過呼叫 CreateFromSurface 並指定短片的表面和持續時間,從 IDirect3DSurface 建立 MediaClip

在 MediaElement 中預覽組合

若要讓使用者能夠檢視媒體組合,請將 MediaPlayerElement 新增至定義 UI 的 XAML 檔案。

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

}
  • 在呼叫 GeneratePreviewMediaStreamSource 之前,MediaComposition 必須至少包含一個媒體短片,否則傳回的物件將為 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 讓您優先考慮轉碼作業的速度與修剪相鄰媒體短片的精確度。 Fast 導致轉碼速度更快,但修剪精確度較低;Precise 導致轉碼速度較慢,但修剪精確度更高。

修剪視訊短片

透過設定 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 屬性可讓您瞭解套用修剪後媒體短片的持續時間。
  • 指定大於短片原始持續時間的修剪值不會擲回錯誤。 但是,如果組合僅包含單一短片,並且透過指定較大的修剪值將其長度修剪為零,則對 GeneratePreviewMediaStreamSource 的後續呼叫將傳回 null,就好像組合沒有剪輯一樣。

將背景曲目新增至組合

若要將背景曲目新增至組合中,請載入音訊檔案,然後透過呼叫處理站方法 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 並將 MediaOverlay 新增至其 Overlays 清單。 最後,將 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");
        }
    }
}