/// <summary> /// This function is used to intialize a heuristics module. It must be implemented by all derived classes. /// </summary> /// <param name="manifestInfo">The manifest for the stream we are reading</param> /// <param name="playbackInfo">The playback info for the media element</param> /// <param name="adInsertPoints">Optional expected points for advertising. Can be null</param> public abstract void Initialize(ManifestInfo manifestInfo, PlaybackInfo playbackInfo, AdInsertionPoint[] adInsertPoints);
/// <summary> /// This function reads the manfiest file from the given stream /// </summary> /// <param name="manifestStream">The manifest file to read</param> private void DoOpen(Stream manifestStream) { // Check for errors if (null == manifestStream) { RaiseError(string.Format(CultureInfo.InvariantCulture, Errors.ManifestFailureError, m_manifestSourceUrl)); return; } // Parse the manifest file using our current parser m_manifestInfo = ManifestParser.ParseManifest(manifestStream, m_manifestAbsoluteUrl); manifestStream.Close(); // Heuristics modules can tell us to either request a new download, or to pause // all of our current downloads. Either way, we need to listen for those events. m_heuristics.RequestDownload += new EventHandler<Heuristics.RequestDownloadEventArgs>(StartChunkDownload); m_heuristics.DownloadsPaused += new EventHandler<Heuristics.DownloadsPausedEventArgs>(FireDownloadsPaused); // Initialize our heuristics module. Convert our ad points into an array AdInsertionPoint[] adArray = m_adInsertionPoints.ToArray(); m_heuristics.Initialize(m_manifestInfo, m_playbackInfo, adArray); // Set our state to opened m_state = State.Opened; // Report back to our media element that we have completed opening the media ReportOpenMediaCompleted(m_manifestInfo.MediaAttributes, m_manifestInfo.MediaStreamDescriptions); // Attach our playback info to the media element if (m_playbackInfo != null) { m_playbackInfo.Attach(); } }
/// <summary> /// Implements Heuristics.Initialize(). Called to initialize this heuristics module with /// the given manifest file and optional adverting insertion points /// </summary> /// <param name="manifestInfo">The manifest of the stream we are using</param> /// <param name="playbackInfo">The playback info for the media element</param> /// <param name="adInsertPoints">Optional ad insertion points. Can be null</param> public override void Initialize(ManifestInfo manifestInfo, PlaybackInfo playbackInfo, AdInsertionPoint[] adInsertPoints) { Tracer.Trace(TraceChannel.MSS, "MediaSource instance {0}, sessionID: {1}", InstanceId.ToString("B"), sm_sessionId.ToString("B")); // Store our manifest info m_manifestInfo = manifestInfo; m_playbackInfo = playbackInfo; // We only support 1 audio and 1 video stream right now, so let's throw an exception // if someone tries to create us with more than that int numStreams = m_manifestInfo.NumberOfStreams; if (numStreams > 2) { throw new AdaptiveStreamingException("We only support 1 audio and 1 video stream at this time"); } // Copy the add insertion points over m_adInsertPoints = new AdInsertionPoints(); foreach (AdInsertionPoint ad in adInsertPoints) { m_adInsertPoints.Add(ad); } // Allocate some internal variables m_heuristicsMode = new HeuristicsMode[numStreams]; m_fixedBitrateIndex = new int[numStreams]; m_mediaStreamTypes = new MediaStreamType[numStreams]; m_maxDownloadsPerStream = new int[numStreams]; m_numDownloadsInProgress = new int[numStreams]; m_nextChunkId = new int[numStreams]; // Go through each stream in our manifest and do per-stream configuration int numVideoStreams = 0, numAudioStreams = 0; for (int i = 0; i < numStreams; i++) { StreamInfo streamInfo = m_manifestInfo.GetStreamInfoForStream(i); // Initialize some of the arrays we just created m_mediaStreamTypes[i] = streamInfo.MediaType; m_fixedBitrateIndex[i] = 0; m_maxDownloadsPerStream[i] = GlobalMaxDownloadsPerStream; m_numDownloadsInProgress[i] = 0; m_nextChunkId[i] = 0; // If we are a video stream, then we need to set our heuristics mode // to Full switch (streamInfo.MediaType) { case MediaStreamType.Audio: m_heuristicsMode[i] = HeuristicsMode.FixedRate; ++numAudioStreams; break; case MediaStreamType.Video: m_heuristicsMode[i] = HeuristicsMode.Full; ++numVideoStreams; break; default: throw new AdaptiveStreamingException(Errors.NonVideoOrAudioStreamsNotSupportedError); } // Configure this stream InitializeHeuristicsForStream( streamInfo.StreamId, (double)m_maxBufferSizeInMs / 1000, m_manifestInfo.GetStreamInfoForStream(i).Bitrates.Count, m_manifestInfo.GetStreamInfoForStream(i).GetBitrateArray()); } // Make sure we found only 1 video and 1 audio stream if (numVideoStreams != 1) { throw new AdaptiveStreamingException(Errors.IncorrectNumberOfVideoStreamsError); } // Start scheduling our downloads ScheduleDownloads(); }
/// <summary> /// Default implementation of the ParseManifest function. /// </summary> /// <param name="manifestStream">The manifest stream to parse</param> /// <param name="manifestUrl">The url of the stream we are parsing</param> /// <returns>A ManifestInfo representing this manifest</returns> public override ManifestInfo ParseManifest(Stream manifestStream, Uri manifestUrl) { ManifestInfo info = new ManifestInfo(); try { int streamId = 0; XmlReader manifest = XmlReader.Create(manifestStream); if (!manifest.Read()) { throw new AdaptiveStreamingException("Manifest does not have a single parsable element."); } // Check to make sure the first element is <MediaIndex> if (!manifest.IsStartElement(ManifestRootElement)) { throw new AdaptiveStreamingException("Manifest root element must be <" +ManifestRootElement + ">"); } // Get manifest version, do not overwrite -1 if not present string major = manifest.GetAttribute(ManifestMajorVersionAttribute); string minor = manifest.GetAttribute(ManifestMinorVersionAttribute); if (major != null) { info.ManifestMajorVersion = Convert.ToInt32(major, CultureInfo.InvariantCulture); } if (major != null) { info.ManifestMinorVersion = Convert.ToInt32(minor, CultureInfo.InvariantCulture); } // Pull the duration out of the maifest string duration = manifest.GetAttribute(ManifestDurationAttribute); if (duration != null) { info.MediaAttributes.Add(MediaSourceAttributesKeys.Duration, duration.Trim()); } else { throw new AdaptiveStreamingException("Manifest root must contain a duration attribute"); } // Base URL of the manifest for chunks, we basically cut the manifest file name, e.g. "mbr/manifest.elive" will become "mbr/" string manifestBaseUrl = string.Empty; int cut1 = manifestUrl.AbsoluteUri.LastIndexOf('/'); int cut2 = manifestUrl.AbsoluteUri.LastIndexOf('\\'); if (cut1 < cut2) { cut1 = cut2; } if (cut1 >= 0) { manifestBaseUrl = manifestUrl.AbsoluteUri.Substring(0, cut1 + 1); } while (manifest.Read()) { if (manifest.IsStartElement(StreamIndexElement)) { StreamInfo si = ParseStreamInfo(manifest, streamId, manifestBaseUrl, info); if (si != null) { info.AddStream(si); streamId++; } } // We explicitly ignore content that we don't understand, as well as whitespace, comments etc. } if (streamId < 1) { throw new AdaptiveStreamingException("At least one media stream is needed, none are declared in the manifest."); } info.Valid = true; } catch (AdaptiveStreamingException) { info.Valid = false; throw; } finally { manifestStream.Close(); } return info; }
/// <summary> /// Parse our text streams /// </summary> /// <param name="manifest">the xml we are parsing</param> /// <param name="manifestInfo">the manifest info object we are returning</param> private static void ParseTextStream(XmlReader manifest, ManifestInfo manifestInfo) { // Parse all of the entries while (manifest.Read()) { if (manifest.Name.Equals("Marker")) { string markerTime = manifest.GetAttribute("Time"); string markerValue = manifest.GetAttribute("Value"); TimelineMarker marker = new TimelineMarker(); marker.Type = MarkerType; marker.Time = new TimeSpan((long)Convert.ToUInt64(markerTime, CultureInfo.InvariantCulture)); marker.Text = markerValue; manifestInfo.Markers.Add(marker); } else if (manifest.Name.Equals("ScriptCommand")) { string markerTime = manifest.GetAttribute("Time"); string markerType = manifest.GetAttribute("Type"); string markerCommand = manifest.GetAttribute("Command"); TimelineMarker marker = new TimelineMarker(); marker.Type = markerType; marker.Time = new TimeSpan((long)Convert.ToUInt64(markerTime, CultureInfo.InvariantCulture)); marker.Text = markerCommand; manifestInfo.Markers.Add(marker); } else if (manifest.Name.Equals("StreamIndex")) { break; } } }
/// <summary> /// Parse the stream section of the manifest /// </summary> /// <param name="manifest">The XML dom of the stream section in the manifest</param> /// <param name="streamId">The Id of the stream we are parsing</param> /// <param name="manifestBaseUrl">the url of the manifest we are parsing</param> /// <param name="manifestInfo">the manifest we are parsing</param> /// <returns>A StreamInfo describing the stream at streamId, or null if one was not found</returns> private static StreamInfo ParseStreamInfo(XmlReader manifest, int streamId, string manifestBaseUrl, ManifestInfo manifestInfo) { string mediaTypeStr = manifest.GetAttribute(StreamIndexTypeAttribute); bool bHaveFirstBitrate = false; // Pick out text types since we handle those separately if (mediaTypeStr.ToUpper(CultureInfo.InvariantCulture).Equals("TEXT")) { // Parse the text stream and return null ParseTextStream(manifest, manifestInfo); return null; } string baseUrl = manifest.GetAttribute(StreamIndexUrlAttribute); int numberOfChunks = Convert.ToInt32(manifest.GetAttribute(StreamIndexChunksAttribute), CultureInfo.InvariantCulture); if (mediaTypeStr == null || baseUrl == null || numberOfChunks < 1) { throw new AdaptiveStreamingException("Stream description in the manifest " + streamId.ToString(CultureInfo.InvariantCulture) + " is missing mandatory attributes (media type, subtype, base URL or number of chunks)"); } MediaStreamType mediaType = mediaTypeStr.ToUpper(CultureInfo.InvariantCulture).Equals("VIDEO") ? MediaStreamType.Video : mediaTypeStr.ToUpper(CultureInfo.InvariantCulture).Equals("AUDIO") ? MediaStreamType.Audio : MediaStreamType.Script; if (mediaType == MediaStreamType.Script) { throw new AdaptiveStreamingException("Stream media type in manifest may be 'audio' or 'video' only"); } if (!baseUrl.ToUpper(CultureInfo.InvariantCulture).StartsWith("HTTP://", StringComparison.OrdinalIgnoreCase)) { baseUrl = manifestBaseUrl + baseUrl; } // Get the language attribute string language = manifest.GetAttribute(StreamIndexLanguageAttribute); if (language == null) { language = string.Empty; } StreamInfo info = new StreamInfo(baseUrl, language, numberOfChunks, mediaType, streamId); ulong maxBitrate = 0; int displayAspectRatioWidth = 0; int displayAspectRatioHeight = 0; int maxBitrateWidth = 0; int maxBitrateHeight = 0; bool bIsVideoStream = true; while (manifest.Read()) { // Get the available bitrates if (manifest.IsStartElement(QualityLevelElement)) { // Missing or malformed attribute kbps will result in failure, which is what we want ulong bitrate = Convert.ToUInt64(manifest.GetAttribute(QualityLevelBitrateAttribute), CultureInfo.InvariantCulture); Dictionary<MediaStreamAttributeKeys, string> attributes = new Dictionary<MediaStreamAttributeKeys, string>(4); // Get the FourCC for this quality level string fourCC = manifest.GetAttribute(QualityLevelFourCCAttribute); if (fourCC != null) { attributes.Add(MediaStreamAttributeKeys.VideoFourCC, fourCC); } // Get the width of this stream string width = manifest.GetAttribute(QualityLevelWidthAttribute); if (width != null) { attributes.Add(MediaStreamAttributeKeys.Width, width); } // Get the height of this stream string height = manifest.GetAttribute(QualityLevelHeightAttribute); if (height != null) { attributes.Add(MediaStreamAttributeKeys.Height, height); } // Get the video codec data string codecPrivateData = manifest.GetAttribute(QualityLevelCodecPrivateDataAttribute); if (codecPrivateData != null) { attributes.Add(MediaStreamAttributeKeys.CodecPrivateData, codecPrivateData); } // Get the wave format ex. Note we will only have one (codec private data) or the other // (wave format ex) string waveFormatEx = manifest.GetAttribute(QualityLevelWaveFormatExAttribute); if (waveFormatEx != null) { if (codecPrivateData != null) { throw new AdaptiveStreamingException("Cannot have both a CodecPrivateData and a WaveFormatEx attribute in the same QualityLevel element."); } bIsVideoStream = false; attributes.Add(MediaStreamAttributeKeys.CodecPrivateData, waveFormatEx); } if (!bHaveFirstBitrate) { bHaveFirstBitrate = true; if (bIsVideoStream) { displayAspectRatioHeight = int.Parse(height, CultureInfo.InvariantCulture); displayAspectRatioWidth = int.Parse(width, CultureInfo.InvariantCulture); } } // Add this bitrate and these attributes to the stream info info.AddBitrate(bitrate, attributes); if (bitrate > maxBitrate) { maxBitrate = bitrate; if (bIsVideoStream) { maxBitrateHeight = int.Parse(height, CultureInfo.InvariantCulture); maxBitrateWidth = int.Parse(width, CultureInfo.InvariantCulture); } } } else if (manifest.IsStartElement("c")) { // Getting chunk information int id = 0; try { // Missing or malformed attributes n or d will result in failure, which is what we want id = Convert.ToInt32(manifest.GetAttribute("n"), CultureInfo.InvariantCulture); // Ignore out-of-range chunk id's to simplify experimental manifest tinkering (truncation for test purposes). if (id < info.NumberOfChunksInStream) { // Add a new media chunk to our stream info ulong chunkDuration = Convert.ToUInt64(manifest.GetAttribute("d"), CultureInfo.InvariantCulture); info.AddMediaChunk(id, chunkDuration); } } catch (ArgumentOutOfRangeException e) { throw new AdaptiveStreamingException(String.Format(CultureInfo.InvariantCulture, "Bad manifest format: chunk ID {0} is out of range.", id), e); } } else if (manifest.Name.Equals("StreamIndex")) { break; } // We explicitly ignore content that we don't understand, as well as whitespace, comments etc. } // Let's fix up the aspect ratio of the highest bitrate stream. We need to find the // combination that gives us the largest buffer size. IDictionary<MediaStreamAttributeKeys, string> mediaAttributes = info.GetAttributesForBitrate(maxBitrate); if(bIsVideoStream) { // First try the width int testWidth = displayAspectRatioWidth * maxBitrateHeight; testWidth = (int)((double)(testWidth) / (double)displayAspectRatioHeight); // Now round it up to the nearest four testWidth += 3; testWidth -= testWidth % 4; // Now try the height int testHeight = displayAspectRatioHeight * maxBitrateWidth; testHeight = (int)((double)(testHeight) / (double)displayAspectRatioWidth); // Now round it up to the nearest four testHeight+= 3; testHeight -= testHeight % 4; // Calculate the buffer sizes int bufferSizeOriginal = maxBitrateWidth * maxBitrateHeight; int bufferSizeWidth = testWidth * maxBitrateHeight; int bufferSizeHeight = testHeight * maxBitrateWidth; if (bufferSizeWidth >= bufferSizeHeight && bufferSizeWidth >= bufferSizeOriginal) { maxBitrateWidth = testWidth; } else if (bufferSizeHeight >= bufferSizeWidth && bufferSizeHeight >= bufferSizeOriginal) { maxBitrateHeight = testHeight; } mediaAttributes.Remove(MediaStreamAttributeKeys.Width); mediaAttributes.Remove(MediaStreamAttributeKeys.Height); mediaAttributes.Add(MediaStreamAttributeKeys.Width, maxBitrateWidth.ToString(CultureInfo.InvariantCulture)); mediaAttributes.Add(MediaStreamAttributeKeys.Height, maxBitrateHeight.ToString(CultureInfo.InvariantCulture)); } // Set the description to be the highest bitrate item. info.Description = new MediaStreamDescription(info.MediaType, mediaAttributes); info.Valid = true; return info; }