using System.Collections.Generic; using System.Dynamic; using System.Linq; using CRD.Utils.Parser.Segments; using CRD.Utils.Parser.Utils; namespace CRD.Utils.Parser; public class ToM3u8Class{ public static dynamic ToM3u8(dynamic parsedPlaylists){ List dashPlaylist = ObjectUtilities.GetMemberValue(parsedPlaylists, "dashPlaylist"); dynamic locations = ObjectUtilities.GetMemberValue(parsedPlaylists, "locations"); dynamic contentSteering = ObjectUtilities.GetMemberValue(parsedPlaylists, "contentSteering"); dynamic sidxMapping = ObjectUtilities.GetMemberValue(parsedPlaylists, "sidxMapping"); dynamic previousManifest = ObjectUtilities.GetMemberValue(parsedPlaylists, "previousManifest"); dynamic eventStream = ObjectUtilities.GetMemberValue(parsedPlaylists, "eventStream"); if (dashPlaylist == null || dashPlaylist.Count == 0){ return new{ }; } dynamic attributes = dashPlaylist[0].attributes; dynamic duration = ObjectUtilities.GetMemberValue(attributes, "sourceDuration"); dynamic type = ObjectUtilities.GetMemberValue(attributes, "type"); dynamic suggestedPresentationDelay = ObjectUtilities.GetMemberValue(attributes, "suggestedPresentationDelay"); dynamic minimumUpdatePeriod = ObjectUtilities.GetMemberValue(attributes, "minimumUpdatePeriod"); List videoPlaylists = MergeDiscontiguousPlaylists(dashPlaylist.FindAll(VideoOnly)).Select(FormatVideoPlaylist).ToList(); List audioPlaylists = MergeDiscontiguousPlaylists(dashPlaylist.FindAll(AudioOnly)); List vttPlaylists = MergeDiscontiguousPlaylists(dashPlaylist.FindAll(VttOnly)); List captions = dashPlaylist .Select(playlist => ObjectUtilities.GetMemberValue(playlist.attributes, "captionServices")) .Where(captionService => captionService != null) // Filtering out null values .ToList(); dynamic manifest = new ExpandoObject(); manifest.allowCache = true; manifest.discontinuityStarts = new List(); manifest.segments = new List(); manifest.endList = true; manifest.mediaGroups = new ExpandoObject(); manifest.mediaGroups.AUDIO = new ExpandoObject(); manifest.mediaGroups.VIDEO = new ExpandoObject(); manifest.mediaGroups.SUBTITLES = new ExpandoObject(); manifest.uri = ""; manifest.duration = duration; manifest.playlists = AddSidxSegmentsToPlaylists(videoPlaylists, sidxMapping); var mediaGroupsDict = (IDictionary)manifest.mediaGroups; mediaGroupsDict["CLOSED-CAPTIONS"] = new ExpandoObject(); if (minimumUpdatePeriod != null && minimumUpdatePeriod >= 0){ manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000; } if (locations != null){ manifest.locations = locations; } if (contentSteering != null){ manifest.contentSteering = contentSteering; } if (type != null && type == "dynamic"){ manifest.suggestedPresentationDelay = suggestedPresentationDelay; } if (eventStream != null && eventStream.Count > 0){ manifest.eventStream = eventStream; } var isAudioOnly = ((List)manifest.playlists).Count == 0; var organizedAudioGroup = audioPlaylists.Count > 0 ? OrganizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null; var organizedVttGroup = vttPlaylists.Count > 0 ? OrganizeVttPlaylists(vttPlaylists, sidxMapping) : null; List formattedPlaylists = new List(videoPlaylists); formattedPlaylists.AddRange(FlattenMediaGroupPlaylists(organizedAudioGroup)); formattedPlaylists.AddRange(FlattenMediaGroupPlaylists(organizedVttGroup)); dynamic playlistTimelineStarts = formattedPlaylists.Select(playlist => playlist.timelineStarts).ToList(); List> convertedToList = new List>(); foreach (var item in playlistTimelineStarts){ if (item is List){ convertedToList.Add(item); } } manifest.timelineStarts = PlaylistMerge.GetUniqueTimelineStarts(convertedToList); AddMediaSequenceValues(formattedPlaylists, manifest.timelineStarts); if (organizedAudioGroup != null){ manifest.mediaGroups.AUDIO.audio = organizedAudioGroup; } if (organizedVttGroup != null){ manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup; } if (captions.Count > 0){ dynamic closedCaptions = mediaGroupsDict["CLOSED-CAPTIONS"]; closedCaptions.cc = OrganizeCaptionServices(captions); } if (previousManifest != null){ return PlaylistMerge.PositionManifestOnTimeline(previousManifest, manifest); } return manifest; } public static bool VideoOnly(dynamic item){ var attributes = item.attributes; return ObjectUtilities.GetMemberValue(attributes, "mimeType") == "video/mp4" || ObjectUtilities.GetMemberValue(attributes, "mimeType") == "video/webm" || ObjectUtilities.GetMemberValue(attributes, "contentType") == "video"; } public static bool AudioOnly(dynamic item){ var attributes = item.attributes; return ObjectUtilities.GetMemberValue(attributes, "mimeType") == "audio/mp4" || ObjectUtilities.GetMemberValue(attributes, "mimeType") == "audio/webm" || ObjectUtilities.GetMemberValue(attributes, "contentType") == "audio"; } public static bool VttOnly(dynamic item){ var attributes = item.attributes; return ObjectUtilities.GetMemberValue(attributes, "mimeType") == "text/vtt" || ObjectUtilities.GetMemberValue(attributes, "contentType") == "text"; } public static dynamic FormatVideoPlaylist(dynamic item){ dynamic playlist = new ExpandoObject(); playlist.attributes = new ExpandoObject(); playlist.attributes.NAME = item.attributes.id; playlist.attributes.AUDIO = "audio"; playlist.attributes.SUBTITLES = "subs"; playlist.attributes.RESOLUTION = new ExpandoObject(); playlist.attributes.RESOLUTION.width = item.attributes.width; playlist.attributes.RESOLUTION.height = item.attributes.height; playlist.attributes.CODECS = item.attributes.codecs; playlist.attributes.BANDWIDTH = item.attributes.bandwidth; playlist.uri = ""; playlist.endList = item.attributes.type == "static"; playlist.timeline = item.attributes.periodStart; playlist.resolvedUri = item.attributes.baseUrl ?? ""; playlist.targetDuration = item.attributes.duration; playlist.discontinuityStarts = item.discontinuityStarts; playlist.timelineStarts = item.attributes.timelineStarts; playlist.segments = item.segments; var attributesDict = (IDictionary)playlist.attributes; attributesDict["PROGRAM-ID"] = 1; if (ObjectUtilities.GetMemberValue(item.attributes, "frameRate") != null){ attributesDict["FRAME-RATE"] = item.attributes.frameRate; } if (ObjectUtilities.GetMemberValue(item.attributes, "contentProtection") != null){ playlist.contentProtection = item.attributes.contentProtection; } if (ObjectUtilities.GetMemberValue(item.attributes, "serviceLocation") != null){ playlist.attributes.serviceLocation = item.attributes.serviceLocation; } if (ObjectUtilities.GetMemberValue(item, "sidx") != null){ playlist.sidx = item.sidx; } return playlist; } public static dynamic FormatAudioPlaylist(dynamic item, bool isAudioOnly){ dynamic playlist = new ExpandoObject(); playlist.attributes = new ExpandoObject(); playlist.attributes.NAME = item.attributes.id; playlist.attributes.BANDWIDTH = item.attributes.bandwidth; playlist.attributes.CODECS = item.attributes.codecs; playlist.uri = string.Empty; playlist.endList = item.attributes.type == "static"; playlist.timeline = item.attributes.periodStart; playlist.resolvedUri = item.attributes.baseUrl ?? string.Empty; playlist.targetDuration = item.attributes.duration; playlist.discontinuitySequence = ObjectUtilities.GetMemberValue(item, "discontinuitySequence"); playlist.discontinuityStarts = item.discontinuityStarts; playlist.timelineStarts = item.attributes.timelineStarts; playlist.mediaSequence = ObjectUtilities.GetMemberValue(item, "mediaSequence"); playlist.segments = item.segments; var attributesDict = (IDictionary)playlist.attributes; attributesDict["PROGRAM-ID"] = 1; if (ObjectUtilities.GetMemberValue(item.attributes, "contentProtection") != null){ playlist.contentProtection = item.attributes.contentProtection; } if (ObjectUtilities.GetMemberValue(item.attributes, "serviceLocation") != null){ playlist.attributes.serviceLocation = item.attributes.serviceLocation; } if (ObjectUtilities.GetMemberValue(item, "sidx") != null){ playlist.sidx = item.sidx; } if (isAudioOnly){ playlist.attributes.AUDIO = "audio"; playlist.attributes.SUBTITLES = "subs"; } return playlist; } public static dynamic FormatVttPlaylist(dynamic item){ if (ObjectUtilities.GetMemberValue(item,"segments") == null){ // VTT tracks may use a single file in BaseURL var segment = new ExpandoObject() as IDictionary; segment["uri"] = item.attributes.baseUrl; segment["timeline"] = item.attributes.periodStart; segment["resolvedUri"] = item.attributes.baseUrl ?? string.Empty; segment["duration"] = item.attributes.sourceDuration; segment["number"] = 0; item.segments = new List{ segment }; // TargetDuration should be the same duration as the only segment item.attributes.duration = item.attributes.sourceDuration; } var m3u8Attributes = new ExpandoObject() as IDictionary; m3u8Attributes["NAME"] = item.attributes.id; m3u8Attributes["BANDWIDTH"] = item.attributes.bandwidth; m3u8Attributes["PROGRAM-ID"] = 1; if (ObjectUtilities.GetMemberValue(item.attributes,"codecs") != null){ m3u8Attributes["CODECS"] = item.attributes.codecs; } dynamic vttPlaylist = new ExpandoObject(); vttPlaylist.attributes = m3u8Attributes; vttPlaylist.uri = string.Empty; vttPlaylist.endList = item.attributes.type == "static"; vttPlaylist.timeline = item.attributes.periodStart; vttPlaylist.resolvedUri = item.attributes.baseUrl ?? string.Empty; vttPlaylist.targetDuration = item.attributes.duration; vttPlaylist.timelineStarts = item.attributes.timelineStarts; vttPlaylist.discontinuityStarts = item.discontinuityStarts; vttPlaylist.discontinuitySequence = ObjectUtilities.GetMemberValue(item, "discontinuitySequence"); vttPlaylist.mediaSequence = ObjectUtilities.GetMemberValue(item,"mediaSequence"); vttPlaylist.segments = item.segments; if (ObjectUtilities.GetMemberValue(item.attributes, "serviceLocation") != null){ vttPlaylist.attributes.serviceLocation = item.attributes.serviceLocation; } return vttPlaylist; } public static dynamic OrganizeCaptionServices(List captionServices){ var svcObj = new ExpandoObject() as IDictionary; foreach (var svc in captionServices){ if (svc == null) continue; foreach (var service in svc){ string channel = service.channel; string language = service.language; var serviceDetails = new ExpandoObject() as IDictionary; serviceDetails["autoselect"] = false; serviceDetails["default"] = false; serviceDetails["instreamId"] = channel; serviceDetails["language"] = language; // Optionally add properties if they exist if (((IDictionary)service).ContainsKey("aspectRatio")){ serviceDetails["aspectRatio"] = service.aspectRatio; } if (((IDictionary)service).ContainsKey("easyReader")){ serviceDetails["easyReader"] = service.easyReader; } if (((IDictionary)service).ContainsKey("3D")){ serviceDetails["3D"] = service["3D"]; } svcObj[language] = serviceDetails; } } return svcObj; } public static List FlattenMediaGroupPlaylists(dynamic mediaGroupObject){ if (mediaGroupObject == null) return new List(); var result = new List(); foreach (var key in ((IDictionary)mediaGroupObject).Keys){ var labelContents = mediaGroupObject[key]; if (labelContents.playlists != null && labelContents.playlists is List){ result.AddRange(labelContents.playlists); } } return result; } public static List MergeDiscontiguousPlaylists(List playlists){ // Break out playlists into groups based on their baseUrl var playlistsByBaseUrl = playlists.GroupBy( p => p.attributes.baseUrl, p => p, (key, g) => new{ BaseUrl = key, Playlists = g.ToList() }) .ToDictionary(g => g.BaseUrl, g => g.Playlists); var allPlaylists = new List(); foreach (var playlistGroup in playlistsByBaseUrl.Values){ var mergedPlaylists = playlistGroup .GroupBy( p => p.attributes.id + (ObjectUtilities.GetMemberValue(p.attributes, "lang") ?? ""), p => p, (key, g) => new{ Name = key, Playlists = g.ToList() }) .Select(g => { dynamic mergedPlaylist = new ExpandoObject(); mergedPlaylist.attributes = new ExpandoObject(); mergedPlaylist.attributes.timelineStarts = new List(); foreach (var playlist in g.Playlists){ if (ObjectUtilities.GetMemberValue(mergedPlaylist, "segments") == null){ mergedPlaylist = playlist; mergedPlaylist.attributes.timelineStarts = new List(); } else{ if (playlist.segments != null && playlist.segments.Count > 0){ playlist.segments[0].discontinuity = true; foreach (var segment in playlist.segments){ mergedPlaylist.segments.Add(segment); } } if (playlist.attributes.contentProtection != null){ mergedPlaylist.attributes.contentProtection = playlist.attributes.contentProtection; } } mergedPlaylist.attributes.timelineStarts.Add(new{ start = playlist.attributes.periodStart, timeline = playlist.attributes.periodStart }); } return mergedPlaylist; }) .ToList(); allPlaylists.AddRange(mergedPlaylists); } return allPlaylists.Select(playlist => { playlist.discontinuityStarts = FindIndexes((List) ObjectUtilities.GetMemberValue(playlists,"segments") ?? new List(), "discontinuity"); return playlist; }).ToList(); } public static IDictionary OrganizeAudioPlaylists(List playlists, IDictionary? sidxMapping = null, bool isAudioOnly = false){ sidxMapping ??= new Dictionary(); // Ensure sidxMapping is not null dynamic mainPlaylist = null; var formattedPlaylists = playlists.Aggregate(new Dictionary(), (acc, playlist) => { var role = ObjectUtilities.GetMemberValue(playlist.attributes, "role") != null && ObjectUtilities.GetMemberValue(playlist.attributes.role, "value") != null ? playlist.attributes.role.value : string.Empty; var language = ObjectUtilities.GetMemberValue(playlist.attributes, "lang") ?? string.Empty; var label = ObjectUtilities.GetMemberValue(playlist.attributes, "label") ?? "main"; if (!string.IsNullOrEmpty(language) && string.IsNullOrEmpty(playlist.attributes.label)){ var roleLabel = !string.IsNullOrEmpty(role) ? $" ({role})" : string.Empty; label = $"{language}{roleLabel}"; } if (!acc.ContainsKey(label)){ acc[label] = new ExpandoObject(); acc[label].language = language; acc[label].autoselect = true; acc[label].@default = role == "main"; acc[label].playlists = new List(); acc[label].uri = string.Empty; } var formatted = AddSidxSegmentsToPlaylist(FormatAudioPlaylist(playlist, isAudioOnly), sidxMapping); acc[label].playlists.Add(formatted); if (mainPlaylist == null && role == "main"){ mainPlaylist = playlist; mainPlaylist.@default = true; // Use '@' to escape reserved keyword } return acc; }); // If no playlists have role "main", mark the first as main if (mainPlaylist == null && formattedPlaylists.Count > 0){ var firstLabel = formattedPlaylists.Keys.First(); formattedPlaylists[firstLabel].@default = true; // Use '@' to escape reserved keyword } return formattedPlaylists; } public static IDictionary OrganizeVttPlaylists(List playlists, IDictionary? sidxMapping = null){ sidxMapping ??= new Dictionary(); // Ensure sidxMapping is not null var organizedPlaylists = playlists.Aggregate(new Dictionary(), (acc, playlist) => { var label = playlist.attributes.label ?? playlist.attributes.lang ?? "text"; if (!acc.ContainsKey(label)){ dynamic playlistGroup = new ExpandoObject(); playlistGroup.language = label; playlistGroup.@default = false; // '@' is used to escape C# keyword playlistGroup.autoselect = false; playlistGroup.playlists = new List(); playlistGroup.uri = string.Empty; acc[label] = playlistGroup; } acc[label].playlists.Add(AddSidxSegmentsToPlaylist(FormatVttPlaylist(playlist), sidxMapping)); return acc; }); return organizedPlaylists; } public static void AddMediaSequenceValues(List playlists, List timelineStarts){ foreach (var playlist in playlists){ playlist.mediaSequence = 0; playlist.discontinuitySequence = timelineStarts.FindIndex(ts => ts.timeline == playlist.timeline); if (playlist.segments == null) continue; for (int i = 0; i < playlist.segments.Count; i++){ playlist.segments[i].number = i; } } } public static List FindIndexes(List list, string key){ var indexes = new List(); for (int i = 0; i < list.Count; i++){ var expandoDict = list[i] as IDictionary; if (expandoDict != null && expandoDict.ContainsKey(key) && expandoDict[key] != null){ indexes.Add(i); } } return indexes; } public static dynamic AddSidxSegmentsToPlaylist(dynamic playlist, IDictionary sidxMapping){ string sidxKey = GenerateSidxKey(ObjectUtilities.GetMemberValue(playlist, "sidx")); if (!string.IsNullOrEmpty(sidxKey) && sidxMapping.ContainsKey(sidxKey)){ var sidxMatch = sidxMapping[sidxKey]; if (sidxMatch != null){ SegmentBase.AddSidxSegmentsToPlaylist(playlist, sidxMatch.sidx, playlist.sidx.resolvedUri); } } return playlist; } public static List AddSidxSegmentsToPlaylists(List playlists, IDictionary? sidxMapping = null){ sidxMapping ??= new Dictionary(); if (sidxMapping.Count == 0){ return playlists; } for (int i = 0; i < playlists.Count; i++){ playlists[i] = AddSidxSegmentsToPlaylist(playlists[i], sidxMapping); } return playlists; } public static string GenerateSidxKey(dynamic sidx){ return sidx != null ? $"{sidx.uri}-{UrlType.ByteRangeToString(sidx.byterange)}" : null; } }