views:

284

answers:

2

I have a working DNLA device (Xbox360, PSP...) RSS Video feed reader in C#. It parses .opml files to get the feed uri.

Sometimes an RSS feed item will not have a duration value, so I am hard coding a default duration value when it doesn't.

I want to get the true duration of the video file.

My idea is to use httpWebRequest to get a byte stream and seek out the information in the files binary metaData, if available. I'm thinking it could be done, but can find no similar examples.

The process must be fast and does not need to get the whole video file, because the duration value is only needed to build the menu. The files I am expecting to process this way are .flv, .m4v, and .mp4's. the example shown below is for a .flv file:

using System;
using System.IO;
using System.Text;
using System.Net;

namespace myRSSVideoReader
{
    public static class FlvMetadataReader
    {
        private const int BufferLength = 1000;
        /// <summary>
        /// Reads the meta information (if present) in an FLV
        /// </summary>
        /// <param name="uri">The path to the FLV file</returns>
        public static MediaMetadataInfo GetMetadataInfo(string uri)
        {
            bool hasMetaData = false;
            double duration = 0;
            double width = 0;
            double height = 0;
            double videoDataRate = 0;
            double audioDataRate = 0;
            double frameRate = 0;
            DateTime creationDate = DateTime.MinValue;

            WebRequest req = HttpWebRequest.Create(uri);
            WebResponse res = req.GetResponse();

            Stream s = res.GetResponseStream(); //Source
            MemoryStream ms = new MemoryStream((int)(res as HttpWebResponse).ContentLength); //Destination

            byte[] b = new byte[BufferLength]; //Buffer
            int cnt = 0;

            do
            {
                //Read up to 1000 bytes from the response stream
                cnt = s.Read(b, 0, BufferLength);

                //Write the number of bytes actually read
                ms.Write(b, 0, cnt);
            }
            while (cnt > 0);

            try
            {
                // read where "onMetaData"
                byte[] bytes = new byte[10];
                ms.Seek(27, SeekOrigin.Begin);
                int result = ms.Read(bytes, 0, 10);

                // if "onMetaData" exists then proceed to read the attributes
                string onMetaData = ByteArrayToString(bytes);
                if (onMetaData == "onMetaData")
                {
                    hasMetaData = true;
                    // 16 bytes past "onMetaData" is the data for "duration" 
                    duration = GetNextDouble(ms, 16, 8);

                    // 8 bytes past "duration" is the data for "width"
                    width = GetNextDouble(ms, 8, 8);

                    // 9 bytes past "width" is the data for "height"
                    height = GetNextDouble(ms, 9, 8);

                    // 16 bytes past "height" is the data for "videoDataRate"
                    videoDataRate = GetNextDouble(ms, 16, 8);

                    // 16 bytes past "videoDataRate" is the data for "audioDataRate"
                    audioDataRate = GetNextDouble(ms, 16, 8);

                    // 12 bytes past "audioDataRate" is the data for "frameRate"
                    frameRate = GetNextDouble(ms, 12, 8);

                    // read in bytes for creationDate manually
                    ms.Seek(17, SeekOrigin.Current);
                    byte[] seekBytes = new byte[24];
                    result = ms.Read(seekBytes, 0, 24);
                    string dateString = ByteArrayToString(seekBytes);
                    // create .NET readable date string
                    // cut off Day of Week
                    dateString = dateString.Substring(4);
                    // grab 1) month and day, 2) year, 3) time
                    dateString = dateString.Substring(0, 6) + " " + dateString.Substring(16, 4) + " " + dateString.Substring(7, 8);
                    // .NET 2.0 has DateTime.TryParse
                    try
                    {
                        creationDate = Convert.ToDateTime(dateString);
                    }
                    catch(Exception) 
                    {
                        // no error handling
                    }
                }
            }
            catch (Exception)
            {
                // no error handling
            }
            finally
            {
                ms.Close();
                ms.Dispose();
            }

            Uri newUri = new Uri(uri);
            string filename = Path.GetFileName(newUri.AbsoluteUri);

            return new MediaMetadataInfo(hasMetaData, filename, duration, width, height, videoDataRate, audioDataRate, frameRate, creationDate);
        }

        private static Double GetNextDouble(MemoryStream ms, int offset, int length)
        {
            // move the desired number of places in the array
            ms.Seek(offset, SeekOrigin.Current);

            // create byte array
            byte[] bytes = new byte[length];

            // read bytes
            int result = ms.Read(bytes, 0, length);

            // convert to double (all flass values are written in reverse order)
            return ByteArrayToDouble(bytes, true);
        }


        private static String ByteArrayToString(byte[] bytes)
        {
            string byteString = string.Empty;
            foreach (byte b in bytes)
            {
                byteString += Convert.ToChar(b).ToString();
            }
            return byteString;
        }


        private static Double ByteArrayToDouble(byte[] bytes, bool readInReverse)
        {
            if (bytes.Length != 8)
                throw new Exception("bytes must be exactly 8 in Length");
            if (readInReverse)
                Array.Reverse(bytes);
            return BitConverter.ToDouble(bytes, 0);
        }
    }
}

Can this be done? I'm including an .flv uri from abc News RSS feed to use as an example: http://video-cdn.abcnew.go.com/090713_ann_skinnydip.flv Any help would be appreciated.

A: 

Your code looks promising. The hard part will be writing parsers for each of the expected file format. There's also a good chance that the files won't have the metadata you need.

You might also look into using the request range to tell the server that you only need a part of the file. That should speed it up as long as the server supports it.

David
Yeah, I'm finding this out. Lots of media files don't have metadata. So I'm in the same spot I was before I checked it, but for those files that do...
The trick for mp4's to get duration, you need the last time stamp (the first one is 0:00:00) out of the bytes. There is some byte shifting, but it works and is fast. This is just for duration. Say if you want height, width, thats in the header. Also the httpWebRequest with a minus AddRange(-1024), you get data from the length of the file backwards 1024 as your starting point : to the end of the file. Cool.
A: 

got it! For Flash files at least! included a little message for the Admin in the user agent string too!

using System;
using System.IO;
using System.Net;

namespace myRSSVideoReader
{
public static class FlvMetadataReader
{
    static string onMetaData = "";
    static string bytesToFile = "";

    /// <summary>
    /// Reads the meta information (if present) in an FLV
    /// </summary>
    /// <param name="uri">The uri to the FLV file</returns>
    public static MediaMetadataInfo GetMetadataInfo(string uri)
    {
        //needed for the file name only
        Uri newUri = new Uri(uri);

        Stream strm = null;
        StreamReader MyReader = null;

        bool hasMetaData = false;
        double duration = 0;
        double width = 0;
        double height = 0;
        double videoDataRate = 0;
        double audioDataRate = 0;
        double frameRate = 0;
        DateTime creationDate = DateTime.MinValue;
        int range = 800;

        try
        {
            //byte[] result;
            byte[] buffer = new byte[range];
            strm = GetURLStream(uri, range);
            if (strm != null)
            {
                using (MemoryStream fileStream = new MemoryStream())
                {
                    int count = 0;
                    do
                    {
                        count = strm.Read(buffer, 0, buffer.Length);
                        fileStream.Write(buffer, 0, count);
                    }
                    while (count != 0);

                    // read where "onMetaData" in flash, this indicates we've got maetadata
                    byte[] bytes = new byte[1000];
                    fileStream.Seek(27, SeekOrigin.Begin);
                    int result = fileStream.Read(bytes, 0, 1000);

                    // if "onMetaData" exists then proceed to read the attributes
                    bytesToFile = ByteArrayToString(bytes);
                    onMetaData = bytesToFile.Substring(0, 10);
                    if (onMetaData == "onMetaData")
                    {
                        hasMetaData = true;
                        duration = GetNextDouble(bytes, bytesToFile.IndexOf("duration") + 9, 8);
                        duration = Math.Round(duration);

                        width = GetNextDouble(bytes, bytesToFile.IndexOf("width") + 6, 8);

                        height = GetNextDouble(bytes, bytesToFile.IndexOf("height") + 7, 8);

                        videoDataRate = GetNextDouble(bytes, bytesToFile.IndexOf("videodatarate") + 14, 8);

                        audioDataRate = GetNextDouble(bytes, bytesToFile.IndexOf("audiodatarate") + 14, 8);

                        frameRate = GetNextDouble(bytes, bytesToFile.IndexOf("framerate") + 10, 8);
                    }
                    fileStream.Close();
                    fileStream.Dispose();
                }                    
            }
        }
        catch {}
        finally
        {
            // do some cleanup
            if (MyReader != null)                
                MyReader.Close();

            if (strm != null)                
                strm.Close();                
        } 

        string filename = Path.GetFileName(newUri.AbsoluteUri);

        return new MediaMetadataInfo(hasMetaData, filename, duration, width, height, videoDataRate, audioDataRate, frameRate, creationDate);
    }

    /* ------------------------------------------------------------- */

    private static Stream GetURLStream(string strURL, int range)
    {
        WebRequest req;
        WebResponse res = null;
        Stream respStream;

        try
        {
            req = WebRequest.Create(strURL);
            ((HttpWebRequest)req).UserAgent = "myRSSVideoReader/1.0.0.12 (compatible; http://www.myrssvideoreader.com; Your RSS feeds need duration value;)";
            ((HttpWebRequest)req).AddRange(0, range * 2); 

            res = req.GetResponse();
            respStream = res.GetResponseStream();

            return respStream;
        }
        catch (Exception)
        {
            res.Close();
            return null;
        }

    }

    /* ------------------------------------------------------------- */

    private static Double GetNextDouble(Byte[] b, int offset, int length)
    {
        MemoryStream ms = new MemoryStream(b);
        // move the desired number of places in the array
        ms.Seek(offset, SeekOrigin.Current);
        // create byte array
        byte[] bytes = new byte[length];
        // read bytes
        int result = ms.Read(bytes, 0, length);
        // convert to double (all flass values are written in reverse order)
        return ByteArrayToDouble(bytes, true);
    }

    /* ------------------------------------------------------------- */

    private static String ByteArrayToString(byte[] bytes)
    {
        string byteString = string.Empty;
        foreach (byte b in bytes)
        {
            byteString += Convert.ToChar(b).ToString();
        }
        return byteString;
    }

    /* ------------------------------------------------------------- */

    private static Double ByteArrayToDouble(byte[] bytes, bool readInReverse)
    {
        if (bytes.Length != 8)
            throw new Exception("bytes must be exactly 8 in Length");
        if (readInReverse)
            Array.Reverse(bytes);
        return BitConverter.ToDouble(bytes, 0);
    }
}
}