3

I have a Windows Store app (C#/XAML) which communicates with a REST service. At some point, I need to play a video stream provided by this service.

If I just assign the stream URI to the MediaElement.Source property, it doesn't work, because the request needs to be authenticated. I need to customize the request sent by the MediaElement control in order to add cookies, credentials and some other custom headers, but I can't find any method or property to do this.

How can I do it? Is it even possible?

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Check out [MediaElement.SetMediaStreamSource](http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.controls.mediaelement.setmediastreamsource.aspx). What you'll likely have to do is authenticate and create the stream separately using HTTP Client (check out the MSFT HTTP library in Nuget as well), then set the source of the `MediaElement` to that stream. – Nate Diamond Sep 03 '13 at 16:33
  • @NateDiamond, thanks, but I don't have this method... According to the documentation, it's available in Windows 8.0, but it takes a IMediaSource parameter, which is only available in 8.1. I suspect the method exists in the native MediaElement control, but is not surfaced in the .NET API – Thomas Levesque Sep 03 '13 at 17:40
  • Ah, you are correct! Regular [SetSource](http://msdn.microsoft.com/en-us/library/windows/apps/br244338.aspx) accepts a `RandomAccessStream` though. – Nate Diamond Sep 03 '13 at 18:58
  • @NateDiamond, yes, but I have no idea how to implement this IRandomAccessStream... – Thomas Levesque Sep 03 '13 at 21:53
  • Check [this](http://stackoverflow.com/questions/8678080/how-to-convert-a-simple-streamhttp-webresponse-to-bitmapimage-in-c-sharp-windo) answer out. It describes turning a regular stream (such as you would get in an `HttpResponse`, as discussed in the question) and turning it into an `InMemoryRandomAccessStream`. If you want to actually stream the data instead of wait for all of it to be downloaded, there are a few other ways of creating streams that you can research. – Nate Diamond Sep 04 '13 at 01:11
  • @NateDiamond, unfortunately this isn't an option for me, I really need to stream the data. I'm looking into a custom implementation that uses the HTTP Range header to seek to a given position. – Thomas Levesque Sep 04 '13 at 12:26
  • I believe you can still stream the data, you just have to do the authentication first, then retrieve the stream and turn it into an `InputStream`. – Nate Diamond Sep 04 '13 at 16:29
  • 1
    @NateDiamond, I got it working, I'll post my solution as soon as I can – Thomas Levesque Sep 04 '13 at 16:38
  • @NateDiamond, I just posted my solution, if you're interested – Thomas Levesque Sep 05 '13 at 08:00

1 Answers1

7

OK, I got it working. Basically, the solution has 2 parts:

  • make the HTTP request manually (with any required credentials or headers)
  • wrap the response stream in a custom IRandomAccessStream that implements Seek by making another request to the server, using the Range header to specify which part of the stream I need.

Here's the RandomAccessStream implementation:

delegate Task<Stream> AsyncRangeDownloader(ulong start, ulong? end);

class StreamingRandomAccessStream : IRandomAccessStream
{
    private readonly AsyncRangeDownloader _downloader;
    private readonly ulong _size;

    public StreamingRandomAccessStream(Stream startStream, AsyncRangeDownloader downloader, ulong size)
    {
        if (startStream != null)
            _stream = startStream.AsInputStream();
        _downloader = downloader;
        _size = size;
    }

    private IInputStream _stream;
    private ulong _requestedPosition;

    public void Dispose()
    {
        if (_stream != null)
            _stream.Dispose();
    }

    public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
    {
        return AsyncInfo.Run<IBuffer, uint>(async (cancellationToken, progress) =>
        {
            progress.Report(0);
            if (_stream == null)
            {
                var netStream = await _downloader(_requestedPosition, null);
                _stream = netStream.AsInputStream();
            }
            var result = await _stream.ReadAsync(buffer, count, options).AsTask(cancellationToken, progress);
            return result;
        });
    }

    public void Seek(ulong position)
    {
        if (_stream != null)
            _stream.Dispose();
        _requestedPosition = position;
        _stream = null;
    }

    public bool CanRead { get { return true; } }
    public bool CanWrite { get { return false; } }
    public ulong Size { get { return _size; } set { throw new NotSupportedException(); } }

    public IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer) { throw new NotSupportedException(); }
    public IAsyncOperation<bool> FlushAsync() { throw new NotSupportedException(); }
    public IInputStream GetInputStreamAt(ulong position) { throw new NotSupportedException(); }
    public IOutputStream GetOutputStreamAt(ulong position) { throw new NotSupportedException(); }
    public IRandomAccessStream CloneStream() { throw new NotSupportedException(); }
    public ulong Position { get { throw new NotSupportedException(); } }
}

It can be used like this:

private HttpClient _client;
private void InitClient()
{
    _client = new HttpClient();
    // Configure the client as needed with CookieContainer, Credentials, etc
    // ...
}

private async Task StartVideoStreamingAsync(Uri uri)
{
    var request = new HttpRequestMessage(HttpMethod.Get, uri);
    // Add required headers
    // ...

    var response = await _client.SendAsync(request);
    ulong length = (ulong)response.Content.Headers.ContentLength;
    string mimeType = response.Content.Headers.ContentType.MediaType;
    Stream responseStream = await response.Content.ReadAsStreamAsync();

    // Delegate that will fetch a stream for the specified range
    AsyncRangeDownloader downloader = async (start, end) =>
        {
            var request2 = new HttpRequestMessage();
            request2.Headers.Range = new RangeHeaderValue((long?)start, (long?)end);
            // Add other required headers
            // ...
            var response2 = await _client.SendAsync(request2);
            return await response2.Content.ReadAsStreamAsync();
        };

    var videoStream = new StreamingRandomAccessStream(responseStream, downloader, length);
    _mediaElement.SetSource(videoStream, mimeType);
}

The user can seek to an arbitrary position in the video, and the stream will issue another request to get the stream at the specified position.

It's still more complex than I think it should be, but it works...

Note that the server must support the Range header in requests, and must issue the Content-Length header in the initial response.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 1
    If you need an equivalent solution for `Windows.Web.Http.HttpClient`, please give a try to `HttpRandomAccessStream`: https://github.com/kiewic/MediaElementWithHttpClient – kiewic Sep 20 '15 at 23:03
  • @kiewic, nice! But it would be more flexible to be able to pass a HttpRequestMessage, rather than just a client and URI – Thomas Levesque Sep 21 '15 at 00:46
  • I considered that option, but in `Windows.Web.Http`, the `HttpRequestMessage` is not reusable, so a new instance is needed each time the position changes. Moreover, custom headers, authentication, redirection, compression, etc. can de configured in the HttpClient. – kiewic Sep 22 '15 at 01:57
  • @kiewic Awesome!!!! answer my question and get the bounty http://stackoverflow.com/questions/35275946/mediaelement-web-video-doesnt-stop-buffering – Stamos Feb 28 '16 at 17:20