2014年9月21日 星期日

利用 AudioEncodingProperties 產生 MediaStreamSource

曾經在 Silverlight for Windows Phone 上使用過 BackgroundAudioPlayer 的開發者大概會知道,這個播放器除了餵一般的 Uri 之外,若要播放串流就得實作該音檔格式的 MediaStreamSource 來供播放器讀取,而這個實作過程相當的辛苦,除了要讀音檔格式的 Spec 之外,背景播放的測試也是很耗時間的,幸好這個過程在撰寫 Store Apps 時不需再經歷一次。

在 Store Apps 中的 MediaElement 也是同樣除了 Uri 之外還可以指定 MediaStreamSource 來播放自訂的串流,但是 MediaStreamSource 的 Constructor 可以指定 MediaStreamDescriptor,這就是用來描述音檔格式的方法,目前可以利用 AudioEncodingProperties 建立  AAC、AAC-ADTS、MP3、PCM、WMA 這幾種格式的串流,如果正好你需要播放這些格式,那麼 Store Apps 的框架很貼心的幫你節省了上千行程式碼的撰寫時間。

其中 AAC 指的是 MP4 或 M4A 這種包裝的方式,與 ADTS 格式的 Frame 排列規則略有不同,有興趣的可以參考 AAC 的 Wiki,有需要播放 AAC 串流的開發者別忘了看看副檔名確認是哪一種 AAC。

以下我用很簡短的 Code 示範如何播放 MP3 的串流,串流來源是放在 Storage 底下的 Audio.mp3 這個檔案,首先在 XAML 中加入一個 MediaElement 元件及一顆播放按鍵。

<Page
    x:Class="App1.PlayerPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <MediaElement x:Name="Player" AreTransportControlsEnabled="True" AudioCategory="BackgroundCapableMedia" />
        <Button Content="Play" Click="OnPlayButtonClick"/>
    </Grid>
</Page>

接著在 PlayerPage.xaml.cs 中利用 MediaStreamSource 將 Audio.mp3 檔案讀出來播放,這裡為了提供連貫的閱讀性,將流程全部寫在 Page 內,但理想上將各種格式的 MediaStreamSource 各別封裝成獨立的 Class 將可以得到更簡潔的播放流程程式碼,例如建立 MP3MediaStreamSource 或 AACMediaStreamSource 之類的來使用。

public sealed partial class PlayerPage : Page
{
    private StorageFile inputMP3File;
    private MediaStreamSource MSS = null;
    private IRandomAccessStream mssStream;
    private IInputStream inputStream;
    private UInt64 byteOffset;
    private TimeSpan timeOffset;
    private const UInt32 sampleSize = 1152;
    private TimeSpan sampleDuration = new TimeSpan(0, 0, 0, 0, 70);

    public PlayerPage()
    {
        this.InitializeComponent();
    }

    private async void OnPlayButtonClick(object sender, RoutedEventArgs e)
    {
        inputMP3File = await ApplicationData.Current.LocalFolder.GetFileAsync("temp0.mp3");
        byteOffset = 0;

        uint sampleRate = 44100;
        uint channelCount = 2;
        uint bitRate = 128000;

        MusicProperties mp3FileProperties = await inputMP3File.Properties.GetMusicPropertiesAsync();
        TimeSpan songDuration = mp3FileProperties.Duration;
        Debug.WriteLine("歌曲總長度 :: " + songDuration.TotalSeconds);

        AudioEncodingProperties audioProps = AudioEncodingProperties.CreateMp3(sampleRate, channelCount, bitRate);
        AudioStreamDescriptor audioDescriptor = new AudioStreamDescriptor(audioProps);

        MSS = new MediaStreamSource(audioDescriptor);
        MSS.CanSeek = true;
        MSS.MusicProperties.Title = mp3FileProperties.Title;
        MSS.Duration = songDuration;

        MSS.Starting += OnMSSStarting;
        MSS.SampleRequested += OnMSSSampleRequested;
        MSS.Closed += OnMSSClosed;

        Player.SetMediaStreamSource(MSS);
    }

    async void OnMSSStarting(Windows.Media.Core.MediaStreamSource sender, MediaStreamSourceStartingEventArgs args)
    {
        MediaStreamSourceStartingRequest request = args.Request;

        if ((request.StartPosition != null) && request.StartPosition.Value <= MSS.Duration)
        {
            UInt64 sampleOffset = (UInt64)request.StartPosition.Value.Ticks / (UInt64)sampleDuration.Ticks;
            timeOffset = new TimeSpan((long)sampleOffset * sampleDuration.Ticks);
            byteOffset = sampleOffset * sampleSize;
        }

        if (mssStream == null)
        {
            MediaStreamSourceStartingRequestDeferral deferal = request.GetDeferral();
            try
            {
                mssStream = await inputMP3File.OpenAsync(FileAccessMode.Read);
                request.SetActualStartPosition(timeOffset);
                deferal.Complete();
            }
            catch (Exception)
            {
                MSS.NotifyError(MediaStreamSourceErrorStatus.FailedToOpenFile);
                deferal.Complete();
            }
        }
        else
        {
            request.SetActualStartPosition(timeOffset);
        }
    }

    async void OnMSSSampleRequested(Windows.Media.Core.MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
    {
        if (mssStream != null)
        {
            MediaStreamSourceSampleRequest request = args.Request;

            if (byteOffset + sampleSize <= mssStream.Size)
            {
                MediaStreamSourceSampleRequestDeferral deferal = request.GetDeferral();
                inputStream = mssStream.GetInputStreamAt(byteOffset);

                MediaStreamSample sample = await MediaStreamSample.CreateFromStreamAsync(inputStream, sampleSize, timeOffset);
                sample.Duration = sampleDuration;
                sample.KeyFrame = true;

                byteOffset += sampleSize;
                timeOffset = timeOffset.Add(sampleDuration);
                request.Sample = sample;
                deferal.Complete();
            }
        }
    }

    void OnMSSClosed(Windows.Media.Core.MediaStreamSource sender, MediaStreamSourceClosedEventArgs args)
    {
        if (mssStream != null)
        {
            mssStream.Dispose();
            mssStream = null;
        }

        sender.SampleRequested -= OnMSSSampleRequested;
        sender.Starting -= OnMSSStarting;
        sender.Closed -= OnMSSClosed;

        if (sender == MSS)
        {
            MSS = null;
        }
    }
}



沒有留言:

張貼留言