/// <summary>
        /// Translates the given JSON object extracted from YouTube to its managed representation.
        /// </summary>
        protected virtual YouTubeAdaptiveStream TranslateAdaptiveStream(JObject input)
        {
            if (input == null)
            {
                throw new ArgumentNullException(nameof(input));
            }

            var result = new YouTubeAdaptiveStream
            {
                iTag            = input.Value <int?>("itag"),
                Type            = input.Value <string>("mimeType"),
                AudioSampleRate = input.Value <long>("audioSampleRate"),
                ContentLength   = input.Value <long>("contentLength"),
                FPS             = input.Value <int>("fps"),
                Quality         = input.Value <string>("quality"),
                QualityLabel    = input.Value <string>("qualityLabel"),
                Url             = input.Value <string>("url"),
                FrameSize       = input.ContainsKey("width") && input.ContainsKey("height")
                    ? new Size(input.Value <int>("width"), input.Value <int>("height"))
                    : null
            };

            result.Mime = ExtractActualMime(result.Type);

            // get cipher info
            var cipher = input.Value <string>("cipher") ?? input.Value <string>("signatureCipher");

            if (!string.IsNullOrEmpty(cipher))
            {
                UpdateStreamCipherInfo(result, cipher);
            }

            return(result);
        }
        /// <summary>
        /// Gets invoked by <see cref="DownloadMetadata"/> to extract adaptive_fmts.
        /// </summary>
        protected virtual List <YouTubeAdaptiveStream> ExtractAdaptiveFormatsMetadata(List <KeyValuePair <string, string> > adaptiveFmts)
        {
            var list        = new List <YouTubeAdaptiveStream>();
            var propertySet = new HashSet <string>();
            var draft       = new YouTubeAdaptiveStream();

            void CommitDraft()
            {
                if (draft?.iTag != null && draft.Url != null)
                {
                    list.Add(draft);
                }
                propertySet.Clear();
                draft = new YouTubeAdaptiveStream();
            }

            void Feed(string key, string value)
            {
                // check for force commit
                if (key == null)
                {
                    CommitDraft();
                    return;
                }

                // check for rotation
                if (propertySet.Contains(key))
                {
                    CommitDraft();
                }
                propertySet.Add(key);

                // update draft
                switch (key.ToLowerInvariant())
                {
                case "quality_label":
                    draft.QualityLabel = value;
                    break;

                case "itag":
                    draft.iTag = int.Parse(value);
                    break;

                case "fps":
                    draft.FPS = int.Parse(value);
                    break;

                case "bitrate":
                    draft.AudioSampleRate = int.Parse(value) * 1000;
                    break;

                case "type":
                    draft.Type = value;
                    var parts = value.Split(new[] { ';' }, 2, StringSplitOptions.RemoveEmptyEntries);
                    draft.Mime = parts[0];
                    break;

                case "size":
                    var sizeRegex = new Regex(@"([0-9]+)[^0-9]+([0-9]+)");
                    var match     = sizeRegex.Match(value);
                    if (!match.Success)
                    {
                        throw new GrabParseException($"Failed to parse stream size: {value}.");
                    }
                    draft.FrameSize = new Size(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value));
                    break;

                case "url":
                    draft.Url = value;
                    break;

                case "s":
                    draft.Signature = value;
                    break;
                }
            }

            UnmangleEntries(adaptiveFmts, Feed);
            return(list);
        }