using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; using System.Xml; using Avalonia.Logging; using CRD.Utils.Parser.Utils; namespace CRD.Utils.Parser; public class InheritAttributes{ public static Dictionary KeySystemsMap = new Dictionary{ { "urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b", "org.w3.clearkey" }, { "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed", "com.widevine.alpha" }, { "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95", "com.microsoft.playready" }, { "urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb", "com.adobe.primetime" }, { "urn:mpeg:dash:mp4protection:2011", "mp4protection" } }; public static dynamic GenerateKeySystemInformation(List contentProtectionNodes){ var keySystemInfo = new ExpandoObject() as IDictionary; foreach (var node in contentProtectionNodes){ dynamic attributes = ParseAttribute.ParseAttributes(node); // Assume this returns a dictionary var testAttributes = attributes as IDictionary; if (testAttributes != null && testAttributes.TryGetValue("schemeIdUri", out var attribute)){ string? schemeIdUri = attribute.ToString()?.ToLower(); if (schemeIdUri != null && KeySystemsMap.TryGetValue(schemeIdUri, out var keySystem)){ dynamic info = new ExpandoObject(); info.attributes = attributes; var psshNode = XMLUtils.FindChildren(node, "cenc:pssh").FirstOrDefault(); if (psshNode != null){ string pssh = psshNode.InnerText; // Assume this returns the inner text/content if (!string.IsNullOrEmpty(pssh)){ info.pssh = DecodeB64ToUint8Array(pssh); // Convert base64 string to byte array } } // Instead of using a dictionary key, add the key system directly as a member of the ExpandoObject keySystemInfo[keySystem] = info; } } } return keySystemInfo; } private static byte[] DecodeB64ToUint8Array(string base64String){ return Convert.FromBase64String(base64String); } public static string GetContent(XmlElement element) => element.InnerText.Trim(); public static List BuildBaseUrls(List references, List baseUrlElements){ if (!baseUrlElements.Any()){ return references; } return references.SelectMany(reference => baseUrlElements.Select(baseUrlElement => { var initialBaseUrl = GetContent(baseUrlElement); // var resolvedBaseUrl = ResolveUrl(reference.BaseUrl, initialBaseUrl); // var baseUri = new Uri(reference.baseUrl); // string resolvedBaseUrl = new Uri(baseUri, initialBaseUrl).ToString(); string resolvedBaseUrl = UrlUtils.ResolveUrl(reference.baseUrl, initialBaseUrl); dynamic finalBaseUrl = new ExpandoObject(); finalBaseUrl.baseUrl = resolvedBaseUrl; ObjectUtilities.MergeExpandoObjects(finalBaseUrl, ParseAttribute.ParseAttributes(baseUrlElement)); if (resolvedBaseUrl != initialBaseUrl && finalBaseUrl.serviceLocation == null && reference.serviceLocation != null){ finalBaseUrl.ServiceLocation = reference.ServiceLocation; } return finalBaseUrl; }) ).ToList(); } public static double? GetPeriodStart(dynamic attributes, dynamic? priorPeriodAttributes, string mpdType){ // Summary of period start time calculation from DASH spec section 5.3.2.1 // // A period's start is the first period's start + time elapsed after playing all // prior periods to this one. Periods continue one after the other in time (without // gaps) until the end of the presentation. // // The value of Period@start should be: // 1. if Period@start is present: value of Period@start // 2. if previous period exists and it has @duration: previous Period@start + // previous Period@duration // 3. if this is first period and MPD@type is 'static': 0 // 4. in all other cases, consider the period an "early available period" (note: not // currently supported) var attributesL = attributes as IDictionary; // (1) if (attributesL != null && attributesL.ContainsKey("start") && (attributesL["start"] is double || attributesL["start"] is long || attributesL["start"] is float || attributesL["start"] is int)){ return (double)attributes.start; } var priorPeriodAttributesL = priorPeriodAttributes as IDictionary; // (2) if (priorPeriodAttributesL != null && priorPeriodAttributesL.ContainsKey("start") && priorPeriodAttributesL.ContainsKey("duration") && (priorPeriodAttributesL["start"] is double || priorPeriodAttributesL["start"] is long || priorPeriodAttributesL["start"] is float || priorPeriodAttributesL["start"] is int) && (priorPeriodAttributesL["duration"] is double || priorPeriodAttributesL["duration"] is long || priorPeriodAttributesL["duration"] is float || priorPeriodAttributesL["duration"] is int)){ return (double)priorPeriodAttributes.start + (double)priorPeriodAttributes.duration; } // (3) if (priorPeriodAttributesL == null && string.Equals(mpdType, "static", StringComparison.OrdinalIgnoreCase)){ return 0; } // (4) // There is currently no logic for calculating the Period@start value if there is // no Period@start or prior Period@start and Period@duration available. This is not made // explicit by the DASH interop guidelines or the DASH spec, however, since there's // nothing about any other resolution strategies, it's implied. Thus, this case should // be considered an early available period, or error, and null should suffice for both // of those cases. return null; } public class ContentSteeringInfo{ public string ServerURL{ get; set; } public bool QueryBeforeStart{ get; set; } // Add other properties if needed } public static ContentSteeringInfo GenerateContentSteeringInformation(List contentSteeringNodes){ // If there are more than one ContentSteering tags, throw a warning if (contentSteeringNodes.Count > 1){ Console.WriteLine("The MPD manifest should contain no more than one ContentSteering tag"); } // Return null if there are no ContentSteering tags if (contentSteeringNodes.Count == 0){ return null; } // Extract information from the first ContentSteering tag XmlElement firstContentSteeringNode = contentSteeringNodes[0]; ContentSteeringInfo infoFromContentSteeringTag = new ContentSteeringInfo{ ServerURL = XMLUtils.GetContent(firstContentSteeringNode), // Assuming 'queryBeforeStart' is a boolean attribute QueryBeforeStart = Convert.ToBoolean(firstContentSteeringNode.GetAttribute("queryBeforeStart")) }; return infoFromContentSteeringTag; } private static dynamic CreateExpandoWithTag(string tag){ dynamic expando = new ExpandoObject(); expando.tag = tag; return expando; } public static dynamic GetSegmentInformation(XmlElement adaptationSet){ dynamic segmentInfo = new ExpandoObject(); var segmentTemplate = XMLUtils.FindChildren(adaptationSet, "SegmentTemplate").FirstOrDefault(); var segmentList = XMLUtils.FindChildren(adaptationSet, "SegmentList").FirstOrDefault(); var segmentUrls = segmentList != null ? XMLUtils.FindChildren(segmentList, "SegmentURL").Select(s => ObjectUtilities.MergeExpandoObjects(CreateExpandoWithTag("SegmentURL"), ParseAttribute.ParseAttributes(s))).ToList() : null; var segmentBase = XMLUtils.FindChildren(adaptationSet, "SegmentBase").FirstOrDefault(); var segmentTimelineParentNode = segmentList ?? segmentTemplate; var segmentTimeline = segmentTimelineParentNode != null ? XMLUtils.FindChildren(segmentTimelineParentNode, "SegmentTimeline").FirstOrDefault() : null; var segmentInitializationParentNode = segmentList ?? segmentBase ?? segmentTemplate; var segmentInitialization = segmentInitializationParentNode != null ? XMLUtils.FindChildren(segmentInitializationParentNode, "Initialization").FirstOrDefault() : null; dynamic template = segmentTemplate != null ? ParseAttribute.ParseAttributes(segmentTemplate) : null; if (template != null && segmentInitialization != null){ template.initialization = ParseAttribute.ParseAttributes(segmentInitialization); } else if (template != null && template.initialization != null){ dynamic init = new ExpandoObject(); init.sourceURL = template.initialization; template.initialization = init; } segmentInfo.template = template; segmentInfo.segmentTimeline = segmentTimeline != null ? XMLUtils.FindChildren(segmentTimeline, "S").Select(s => ParseAttribute.ParseAttributes(s)).ToList() : null; segmentInfo.list = segmentList != null ? ObjectUtilities.MergeExpandoObjects(ParseAttribute.ParseAttributes(segmentList), new{ segmentUrls, initialization = ParseAttribute.ParseAttributes(segmentInitialization) }) : null; segmentInfo.baseInfo = segmentBase != null ? ObjectUtilities.MergeExpandoObjects(ParseAttribute.ParseAttributes(segmentBase), new{ initialization = ParseAttribute.ParseAttributes(segmentInitialization) }) : null; // Clean up null entries var dict = (IDictionary)segmentInfo; var keys = dict.Keys.ToList(); foreach (var key in keys){ if (dict[key] == null){ dict.Remove(key); } } return segmentInfo; } public static List ParseCaptionServiceMetadata(dynamic service){ List parsedMetadata = new List(); var tempTestService = service as IDictionary; if (tempTestService == null || !tempTestService.ContainsKey("schemeIdUri")){ return parsedMetadata; } // 608 captions if (service.schemeIdUri == "urn:scte:dash:cc:cea-608:2015"){ var values = service.value is string ? service.value.Split(';') : new string[0]; foreach (var value in values){ dynamic metadata = new ExpandoObject(); string channel = null; string language = value; if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^CC\d=")){ var parts = value.Split('='); channel = parts[0]; language = parts[1]; } else if (System.Text.RegularExpressions.Regex.IsMatch(value, @"^CC\d$")){ channel = value; } metadata.channel = channel; metadata.language = language; parsedMetadata.Add(metadata); } } else if (service.schemeIdUri == "urn:scte:dash:cc:cea-708:2015"){ var values = service.value is string ? service.value.Split(';') : new string[0]; foreach (var value in values){ dynamic metadata = new ExpandoObject(); metadata.channel = default(string); metadata.language = default(string); metadata.aspectRatio = 1; metadata.easyReader = 0; metadata._3D = 0; if (value.Contains("=")){ var parts = value.Split('='); var channel = parts[0]; var opts = parts.Length > 1 ? parts[1] : ""; metadata.channel = "SERVICE" + channel; metadata.language = value; var options = opts.Split(','); foreach (var opt in options){ var optionParts = opt.Split(':'); var name = optionParts[0]; var val = optionParts.Length > 1 ? optionParts[1] : ""; switch (name){ case "lang": metadata.language = val; break; case "er": metadata.easyReader = Convert.ToInt32(val); break; case "war": metadata.aspectRatio = Convert.ToInt32(val); break; case "3D": metadata._3D = Convert.ToInt32(val); break; } } } else{ metadata.language = value; } parsedMetadata.Add(metadata); } } return parsedMetadata; } public static List ToRepresentations(dynamic periodAttributes, dynamic periodBaseUrls, dynamic periodSegmentInfo, XmlElement adaptationSet){ dynamic adaptationSetAttributes = ParseAttribute.ParseAttributes(adaptationSet); var adaptationSetBaseUrls = BuildBaseUrls(periodBaseUrls, XMLUtils.FindChildren(adaptationSet, "BaseURL")); var role = XMLUtils.FindChildren(adaptationSet, "Role").FirstOrDefault(); dynamic roleAttributes = new ExpandoObject(); roleAttributes.role = ParseAttribute.ParseAttributes(role); dynamic attrs = ObjectUtilities.MergeExpandoObjects(periodAttributes, adaptationSetAttributes); attrs = ObjectUtilities.MergeExpandoObjects(attrs, roleAttributes); var accessibility = XMLUtils.FindChildren(adaptationSet, "Accessibility").FirstOrDefault(); var captionServices = ParseCaptionServiceMetadata(ParseAttribute.ParseAttributes(accessibility)); if (captionServices != null){ attrs = ObjectUtilities.MergeExpandoObjects(attrs, new{ captionServices }); } XmlElement label = XMLUtils.FindChildren(adaptationSet, "Label").FirstOrDefault(); if (label != null && label.ChildNodes.Count > 0){ var labelVal = label.ChildNodes[0].ToString().Trim(); attrs = ObjectUtilities.MergeExpandoObjects(attrs, new{ label = labelVal }); } var contentProtection = GenerateKeySystemInformation(XMLUtils.FindChildren(adaptationSet, "ContentProtection")); var tempTestContentProtection = contentProtection as IDictionary; if (tempTestContentProtection != null && tempTestContentProtection.Count > 0){ dynamic contentProt = new ExpandoObject(); contentProt.contentProtection = contentProtection; attrs = ObjectUtilities.MergeExpandoObjects(attrs, contentProt ); } var segmentInfo = GetSegmentInformation(adaptationSet); var representations = XMLUtils.FindChildren(adaptationSet, "Representation"); var adaptationSetSegmentInfo = ObjectUtilities.MergeExpandoObjects(periodSegmentInfo, segmentInfo); List list = new List(); for (int i = 0; i < representations.Count; i++){ List res = InheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo, representations[i]); foreach (dynamic re in res){ list.Add(re); } } // return representations.Select(representation => InheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo, representation)); return list; } public static List InheritBaseUrls(dynamic adaptationSetAttributes, dynamic adaptationSetBaseUrls, dynamic adaptationSetSegmentInfo, XmlElement representation){ var repBaseUrlElements = XMLUtils.FindChildren(representation, "BaseURL"); List repBaseUrls = BuildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); var attributes = ObjectUtilities.MergeExpandoObjects(adaptationSetAttributes, ParseAttribute.ParseAttributes(representation)); var representationSegmentInfo = GetSegmentInformation(representation); return repBaseUrls.Select(baseUrl => { dynamic result = new ExpandoObject(); result.segmentInfo = ObjectUtilities.MergeExpandoObjects(adaptationSetSegmentInfo, representationSegmentInfo); result.attributes = ObjectUtilities.MergeExpandoObjects(attributes, baseUrl); return result; }).ToList(); } private static List ToAdaptationSets(ExpandoObject mpdAttributes, dynamic mpdBaseUrls, dynamic period, int index){ dynamic periodBaseUrls = BuildBaseUrls(mpdBaseUrls, XMLUtils.FindChildren(period.node, "BaseURL")); dynamic start = new ExpandoObject(); start.periodStart = period.attributes.start; dynamic periodAttributes = ObjectUtilities.MergeExpandoObjects(mpdAttributes, start); var tempTestAttributes = period.attributes as IDictionary; if (tempTestAttributes != null && tempTestAttributes.ContainsKey("duration") && (tempTestAttributes["duration"] is double || tempTestAttributes["duration"] is long || tempTestAttributes["duration"] is float || tempTestAttributes["duration"] is int)){ periodAttributes.periodDuration = period.attributes.duration; } List adaptationSets = XMLUtils.FindChildren(period.node, "AdaptationSet"); dynamic periodSegmentInfo = GetSegmentInformation(period.node); List list = new List(); for (int i = 0; i < adaptationSets.Count; i++){ List res = ToRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo, adaptationSets[i]); foreach (dynamic re in res){ list.Add(re); } } return list; // return adaptationSets.Select(adaptationSet => // ToRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo, adaptationSet)); } public static ManifestInfo InheritAttributesFun(XmlElement mpd, Dictionary? options = null){ if (options == null) options = new Dictionary(); string manifestUri = options.ContainsKey("manifestUri") ? (string)options["manifestUri"] : string.Empty; long NOW = options.ContainsKey("NOW") ? (long)options["NOW"] : DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); int clientOffset = options.ContainsKey("clientOffset") ? (int)options["clientOffset"] : 0; Action eventHandler = options.ContainsKey("eventHandler") ? (Action)options["eventHandler"] : () => { }; List periodNodes = XMLUtils.FindChildren(mpd, "Period"); if (periodNodes.Count == 0){ throw new Exception(Errors.INVALID_NUMBER_OF_PERIOD); } List locations = XMLUtils.FindChildren(mpd, "Location"); dynamic mpdAttributes = ParseAttribute.ParseAttributes(mpd); dynamic baseUrl = new ExpandoObject(); baseUrl.baseUrl = manifestUri; dynamic mpdBaseUrls = BuildBaseUrls(new List{ baseUrl }, XMLUtils.FindChildren(mpd, "BaseUrl")); List contentSteeringNodes = XMLUtils.FindChildren(mpd, "ContentSteering"); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'. ObjectUtilities.SetAttributeWithDefault(mpdAttributes, "type", "static"); ObjectUtilities.SetFieldFromOrToDefault(mpdAttributes, "sourceDuration", "mediaPresentationDuration", 0); mpdAttributes.NOW = NOW; mpdAttributes.clientOffset = clientOffset; if (locations.Count > 0){ mpdAttributes.locations = locations.Cast().Select(location => location.InnerText).ToList(); } List periods = new List(); for (int i = 0; i < periodNodes.Count; i++){ XmlElement periodNode = periodNodes[i]; dynamic attributes = ParseAttribute.ParseAttributes(periodNode); int getIndex = i - 1; dynamic? priorPeriod = null; if (getIndex >= 0 && getIndex < periods.Count){ priorPeriod = periods[getIndex]; } attributes.start = GetPeriodStart(attributes, priorPeriod, mpdAttributes.type); dynamic finalPeriod = new ExpandoObject(); finalPeriod.node = periodNode; finalPeriod.attributes = attributes; periods.Add(finalPeriod); } List representationInfo = new List(); for (int i = 0; i < periods.Count; i++){ List result = ToAdaptationSets(mpdAttributes, mpdBaseUrls, periods[i], i); foreach (dynamic re in result){ representationInfo.Add(re); } } return new ManifestInfo{ locations = ObjectUtilities.GetAttributeWithDefault(mpdAttributes, "locations", null), contentSteeringInfo = GenerateContentSteeringInformation(contentSteeringNodes.Cast().ToList()), representationInfo = representationInfo, // eventStream = periods.SelectMany(period => ToEventStream(period)).ToList() }; } }