This project has moved and is read-only. For the latest updates, please go here.

Playing from a stream on WP8?

Oct 18, 2014 at 10:35 AM
Hello!
Your project is great, but the source code looks like too complicated for me.
I want to play from a stream, that I already have (e.g. stream for file).
Could you help me, how can I create a MediaStreamSource from file, so I can use default MediaElement.SetSource() method?
Coordinator
Oct 18, 2014 at 1:13 PM
The source code is a bit too complicated for me too... (I'm working on cleaning it up.)

What kind of stream is it that you wan to play? What do you want to play it on (e.g., Silverlight, WP7, WP8, WP8.1, Win8.1)?
Oct 18, 2014 at 1:16 PM
Edited Oct 18, 2014 at 1:20 PM
I have a regular stream (System.IO.Stream) on Windows Phone 8, that comes from a file.
And I have a default MediaElement.
How can I use my System.IO.Stream to create MediaSourceStream with this library?
I've looked through sample source code and found a place, where IMediaStreamSource is converted to MediaStreamSource (HlsViewWP8 -> MediaElementManager.cs -> public Task SetSourceAsync(IMediaStreamSource source)), but I can't find where IMediaStreamSource is created or how can I create it from a simple stream.
Coordinator
Oct 18, 2014 at 11:55 PM
What kind of file is it? Do you need to do something with the stream before playing it (e.g., decrypt it or remove unrelated junk)? If not, and it is a supported file format, then you should be able to play the file directly. If you do, then this project may be helpful, but you will need to make some changes to play local content.

The "MediaStreamSource" has very little to do with something like System.IO.Stream. It wants to see discrete audio and video samples with timestamps. phonesm takes TS, AAC, AC3 (where the platform supports it), and MP3 streams and feeds them to a MediaStreamSource. It can get at those streams directly or through a playlist (M3U/M3U8 and, to a limited extent, PLS). The current source has a pluggable architecture for handling the low-level stream read (where "stream" in this case is as in "System.IO.Stream"). All the plugins currently focus on HTTP/HTTPS, but it would be pretty easy to extend that to local files. If that makes sense for your application depends on exactly what it is you are trying to play.

Dealing the with the source should not be too difficult. If you grab a copy through Git (I use VS2013's Git support) or download it, then you should be able to build it by running the "Scripts/buildAll.bat" file from a command line. If VS2012 and VS2013 are installed you should wind up with a "Distribution" directory that is equivalent to the .zip file I put on the downloads page. If either VS is missing, you will see a number of errors, but I think you should still get the binaries built by the VS that you do have. Come to think of it, I should probably put an alpha build from the latest code up there; it is long overdue.
Oct 19, 2014 at 10:01 AM
I have a local .TS video file.
I have access to the file using StorageFile.Open().AsStream().

I'm talking about streams because your code takes stream from web.
So, I understand, that if I give a stream, that comes from local file, I'll get the result that I need.

I have already downloaded source, and I can build them.

But my problem is that I can't find how to create suitable MediaSourceStream for MediaElement using your library.
E.g. let's imaging that I have a simple code:
Stream local_stream = my_local_storage_file.Open().AsStream();
?????
here should be some code, that uses your library: talking a stream, reading data, parsing TS file, getting some of your classes and converting them to MediaSourceStream.
?????
MediaSourceStream mss = ???????
myMediaElement.SetSource(mss);

Could you provide a small code example how to do that?
Coordinator
Oct 19, 2014 at 1:53 PM
Well, not really simple... The stuff that does the reading is all focused on the web. It is pluggable, and below an alternate IWebReaderManager implementation that takes a StorageFile and gives a read stream for that file, regardless of the URL.

It shouldn't be too hard to modify StorageFileWebReaderManager to do things like have the pay attention to the URLs instead of having a hard coded file or have a property for setting the StorageFile.

Use it like this (and remember to "await" the InitializeMediaStream(); the normal implementation in HlsView.WP8 is just a void):
        async Task InitializeMediaStream()
        {
            if (null != _mediaStreamFacade)
                return;

            _mediaStreamFacade = MediaStreamFacadeSettings.Parameters.Create();

            var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Media/somefile.ts"));

            var streamReader = new StorageFileWebReaderManager(file, ContentTypes.TransportStream);

            _mediaStreamFacade.Builder.RegisterSingleton<IWebReaderManager>(streamReader);

            _mediaStreamFacade.StateChange += TsMediaManagerOnStateChange;
        }
Here are the guts:
    public sealed class StorageFileWebReaderManager : IWebReaderManager
    {
        readonly Task<ContentType> _contentTypeTask;
        readonly StorageFileWebReader _reader;

        public StorageFileWebReaderManager(StorageFile file, ContentType contentType)
        {
            if (null == file)
                throw new ArgumentNullException("file");
            if (null == contentType)
                throw new ArgumentNullException("contentType");

            _reader = new StorageFileWebReader(file);
            _contentTypeTask = Task.FromResult(contentType);
        }

        #region IWebReaderManager Members

        public IWebReader RootWebReader
        {
            get { return _reader; }
        }

        public IWebReader CreateReader(Uri url, ContentKind contentKind, IWebReader parent = null, ContentType contentType = null)
        {
            return _reader;
        }

        public IWebCache CreateWebCache(Uri url, ContentKind contentKind, IWebReader parent = null, ContentType contentType = null)
        {
            throw new NotImplementedException();
        }

        public Task<ContentType> DetectContentTypeAsync(Uri url, ContentKind contentKind, CancellationToken cancellationToken, IWebReader parent = null)
        {
            return _contentTypeTask;
        }

        #endregion
    }

    public sealed class StreamWebStreamResponse : IWebStreamResponse
    {
        readonly Stream _stream;

        public StreamWebStreamResponse(Stream stream)
        {
            if (null == stream)
                throw new ArgumentNullException("stream");

            _stream = stream;
        }

        #region IWebStreamResponse Members

        public void Dispose()
        {
            _stream.Dispose();
        }

        public bool IsSuccessStatusCode
        {
            get { return true; }
        }

        public Uri ActualUrl { get; private set; }

        public int HttpStatusCode
        {
            get { return 200; }
        }

        public long? ContentLength
        {
            get { return null; }
        }

        public void EnsureSuccessStatusCode()
        { }

        public Task<Stream> GetStreamAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(_stream);
        }

        #endregion
    }

    public class StorageFileWebReader : IWebReader
    {
        readonly StorageFile _file;

        public StorageFileWebReader(StorageFile file)
        {
            if (null == file)
                throw new ArgumentNullException("file");

            _file = file;
        }

        #region IWebReader Members

        public void Dispose()
        { }

        public Uri BaseAddress { get; private set; }
        public Uri RequestUri { get; private set; }
        public ContentType ContentType { get; private set; }
        public IWebReaderManager Manager { get; private set; }

        public async Task<IWebStreamResponse> GetWebStreamAsync(Uri url, bool waitForContent, CancellationToken cancellationToken, Uri referrer = null, long? @from = null, long? to = null, WebResponse response = null)
        {
            var stream = await _file.OpenSequentialReadAsync().AsTask(cancellationToken).ConfigureAwait(false);

            return new StreamWebStreamResponse(stream.AsStreamForRead());
        }

        public Task<byte[]> GetByteArrayAsync(Uri url, CancellationToken cancellationToken, WebResponse webResponse = null)
        {
            throw new NotSupportedException();
        }

        #endregion
    }
Coordinator
Oct 20, 2014 at 2:09 AM
henric wrote:
Come to think of it, I should probably put an alpha build from the latest code up there; it is long overdue.
I built and uploaded phonesm-1.4-alpha, if you prefer to deal with a binary build.
Oct 31, 2014 at 11:08 PM
Thank you for your help, it really works when I modify HlsView.WP8.
But I've got troubles when I try to add reference libraries to .dlls from phonesm-1.4-alpha to my new Windows Phone 8.0 project.
Visual Studio says that "A reference to a higher version or incompatible assembly cannot be added to the project".
I've checked all other refernces - they are the same.
Coordinator
Nov 1, 2014 at 12:27 AM
You might get some hints from comparing your .csproj file with HlsView.WP8.csproj. WinMerge works as can simply opening the two project files as text files and eyeballing them.

Another thing you can try is to build from source and add the dependencies as projects instead of as DLLs (even if that is not how you want to have thing set up going forward, it might give some hits as to what is going wrong).

Come to think of it, are you trying to add any of those reference to DLLs from inside the packages directory? If so, try using NuGet to add those references instead of going to the DLLs directly (right click your app's References folder and select "Manage NuGet packages..."). Take a look at HlsView.WP8's packages.config file to see which ones you need (or use the NuGet package manager there too).
Nov 4, 2014 at 8:14 PM
Thanks! I've downloaded source code, rebuild it and references were added successfuly, video is playing from a local .TS file.
But I've got another trouble - I can't seek to custom position in video file by changing MediaElement.Position property.
Could you help me with it?
Coordinator
Nov 4, 2014 at 11:35 PM
Hmmm... that might be a bit tricky. MediaStreamConfigurator's "CompleteConfigure()" does this:
                var mediaSourceAttributes = new Dictionary<MediaSourceAttributesKeys, string>();

                if (duration.HasValue)
                    mediaSourceAttributes[MediaSourceAttributesKeys.Duration] = duration.Value.Ticks.ToString(CultureInfo.InvariantCulture);

                var canSeek = duration.HasValue;

                mediaSourceAttributes[MediaSourceAttributesKeys.CanSeek] = canSeek.ToString();
Without a duration, the "CanSeek" key is false, so the position setter does nothing (IIRC). The other problem is that the segment manager that handles single URL streams (pretty much anything other than HLS) only knows how to read a stream from the beginning. One would need a new ISegmentManager for seekable streams that does something other than (from SimpleSegmentManagerBase):
        public Task<TimeSpan> SeekAsync(TimeSpan timestamp)
        {
            return TimeSpanZeroTask;
        }
The ISegmentManager is responsible for providing an IAsyncEnumerable<ISegment>. In this case, it contain exactly one ISegment representing your TS file. It looks like this,
    public interface ISegment
    {
        Uri Url { get; }
        Uri ParentUrl { get; }
        long Offset { get; }
        long Length { get; }
        TimeSpan? Duration { get; }
        long? MediaSequence { get; }

        /// <summary>
        ///     Create a new stream to modify the data returned from the web server (e.g.,
        ///     stripping headers from .mp3 or decrypting #EXT-X-KEY AES-128 segments).
        /// </summary>
        /// <param name="stream"></param>
        /// <returns>null if no filter is required</returns>
        Task<Stream> CreateFilterAsync(Stream stream, CancellationToken cancellationToken);
    }
so when reading starts after a seek, there is already a place for an Offset (byte offset into the file), which you should (hopefully) see as the "@from" argument to the above StorageFileWebReader.GetWebStreamAsync(). If you do not want to plow through the file looking for perfect start points, it should be okay to assume that a seek to 21 minutes of a 60 minute file would mean a byte offset of 21 / 60 * fileLength (you may want to round it to the nearest multiple of 188 to save the parser the trouble of having to seek for a TS packet).

To get a duration, one could start at the end of the file and parse backwards until one found the last PTS (presentation timestamp) and, subtract the first PTS in the file, and call the result of that the duration. It would probably make the most sense to do this once for a given file and store it as a metadata somewhere.

One other thing you may want to think about: If your app is the one downloading the TS file to the device, then you might consider looking into using Media Foundation to build an MP4 file from the TS file. In the future, when phonesm no longer actively supports WP7 (at least until WP7's market share drops into the low single digits) , the TS demuxing should probably happen in the Media Foundation pipeline. For the time being, I think you can still use SM.TsParser to demux the stream and feed it into MF. You should then be able to play the MP4 file directly. Note well: I haven't actually tried this. Then again, at worst one would need to find something other than MF to act as the MP4 mux.