/// <summary>
        /// Accepts <paramref name="cipher"/> as a URI-encoded string in the following form:
        /// <para>sp=sig&amp;s=wggj6zg7m-...&amp;url=https%3A%2F%2Fr5---sn-5hnekn7k.googlevideo.com%2Fvideoplayback...</para>
        /// Extracts its useful encoded parameters and puts them into the specified <paramref name="streamInfo"/>.
        /// </summary>
        protected virtual void UpdateStreamCipherInfo(YouTubeStreamInfo streamInfo, string cipher)
        {
            if (string.IsNullOrEmpty(cipher))
            {
                throw new ArgumentNullException(nameof(cipher));
            }

            var map = YouTubeUtils.ExtractUrlEncodedParamMap(cipher);

            streamInfo.Url       = map.GetOrDefault("url") ?? throw new GrabParseException("Failed to extract URL from cipher.");
            streamInfo.Signature = map.GetOrDefault("s") ?? throw new GrabParseException("Failed to extract signature from cipher.");
        }
        /// <summary>
        /// Appends the specified <paramref name="stream"/> to the specified <paramref name="result"/>.
        /// </summary>
        protected virtual void AppendStreamToResult(GrabResult result, YouTubeStreamInfo stream)
        {
            MediaChannels channels;

            // get iTag info
            var itagInfo = stream.iTag == null ? null : YouTubeTags.For(stream.iTag.Value);

            // extract extension from mime
            var extension = stream.Extension ?? stream.Mime?.Split('/')?.Last();

            // decide according to stream type - adaptive, or muxed
            if (stream is YouTubeMuxedStream muxedStream)
            {
                // Muxed stream
                channels = MediaChannels.Both;
            }
            else if (stream is YouTubeAdaptiveStream adaptiveStream)
            {
                // Adaptive stream
                var hasVideo = itagInfo?.HasVideo ?? stream.Mime.StartsWith("video");
                channels = hasVideo ? MediaChannels.Video : MediaChannels.Audio;
            }
            else
            {
                throw new NotSupportedException($"YouTube stream of type {stream.GetType()} is not implemented in {nameof(YouTubeGrabber)}.{nameof(AppendStreamToResult)}.");
            }

            var format  = new MediaFormat(stream.Mime, extension);
            var grabbed = new GrabbedMedia(new Uri(stream.Url), null, format, channels);

            result.Resources.Add(grabbed);

            // update grabbed media iTag info
            if (itagInfo != null)
            {
                UpdateStreamITagInfo(grabbed, itagInfo.Value);
            }
        }