private ConsoleBitmapDiffFrame PrepareDiffFrame(ConsoleBitmapRawFrame previous, ConsoleBitmap bitmap) { ConsoleBitmapDiffFrame diff = new ConsoleBitmapDiffFrame(); diff.Diffs = new List <ConsoleBitmapPixelDiff>(); int changes = 0; for (int y = 0; y < GetEffectiveHeight(bitmap); y++) { for (int x = 0; x < GetEffectiveWidth(bitmap); x++) { var pixel = bitmap.GetPixel(GetEffectiveLeft + x, GetEffectiveTop + y); var hasPreviousPixel = previous.Pixels.Length == GetEffectiveWidth(bitmap) && previous.Pixels[0].Length == GetEffectiveHeight(bitmap); var previousPixel = hasPreviousPixel ? previous.Pixels[x][y] : default(ConsoleCharacter); if (pixel.HasChanged || hasPreviousPixel == false || (pixel.Value.HasValue && pixel.Value.Value.Equals(previousPixel) == false)) { changes++; if (pixel.Value.HasValue) { diff.Diffs.Add(new ConsoleBitmapPixelDiff() { X = x, Y = y, Value = pixel.Value.Value }); } } } } return(diff); }
/// <summary> /// Writes the given bitmap image as a frame to the stream. If this is the first image or more than half of the pixels have /// changed then a raw frame will be written. Otherwise, a diff frame will be written. /// /// This method uses the system's wall clock to determine the timestamp for this frame. The timestamp will be /// relative to the wall clock time when the first frame was written. /// </summary> /// <param name="bitmap">the image to write</param> /// <param name="desiredFrameTime">if provided, sstamp the frame with this time, otherwise stamp it with the wall clock delta from the first frame time</param> /// <param name="force">if true, writes the frame even if there are no changes</param> /// <returns>the same bitmap that was passed in</returns> public ConsoleBitmap WriteFrame(ConsoleBitmap bitmap, bool force = false, TimeSpan?desiredFrameTime = null) { if (pausedAt.HasValue) { return(bitmap); } var rawFrame = GetRawFrame(bitmap); var now = DateTime.UtcNow - TotalPauseTime; if (firstFrameTime.HasValue == false) { rawFrame.Timestamp = TimeSpan.Zero; firstFrameTime = now; } else { rawFrame.Timestamp = desiredFrameTime.HasValue ? desiredFrameTime.Value : now - firstFrameTime.Value; } if (lastFrame == null) { StreamHeader(bitmap); writer.Write(serializer.SerializeFrame(rawFrame)); FramesWritten++; } else { if (GetEffectiveWidth(bitmap) != lastFrame.Pixels.Length || GetEffectiveHeight(bitmap) != lastFrame.Pixels[0].Length) { throw new InvalidOperationException("Video frame has changed size"); } var diff = PrepareDiffFrame(lastFrame, bitmap); diff.Timestamp = rawFrame.Timestamp; var numPixels = GetEffectiveWidth(bitmap) * GetEffectiveHeight(bitmap); if (force || diff.Diffs.Count > numPixels / 2) { writer.Write(serializer.SerializeFrame(rawFrame)); FramesWritten++; } else if (diff.Diffs.Count > 0) { writer.Write(serializer.SerializeFrame(diff)); FramesWritten++; } } lastFrame = rawFrame; return(bitmap); }
/// <summary> /// Serializes the given raw frame. /// /// A serialized raw frame is always a single line with this structure: /// /// All data values are surrounded in square brackets like [dataValue] /// /// Segment1 - Timestamp in the format: [$timestampInTicks$] where $timestampInTicks$ represents a 64 bit non-negative integer /// Segment2 - The type of frame, in this case [Raw] /// Segment3 - The raw bitmap data /// /// The first pixel will be preceded by color markers for foreground (e.g. [F=Red]) and background (e.g. [B=Red]) which means that subsequence characters have those color characteristics. /// If the next pixel is a different foreground and/or background color then there will be color markers for those changes in between the pixel data values /// If the next pixel shares the same foreground and background then there will be no color markers in between those pixels. This saves space. /// Each pixel value is surrounded by square brackets like [A] if the pixel value was A. /// Each pixel value is generally a single character, but square brackets are encoded a OB for the opening bracket and CB for the closing bracket /// The pixels are ordered vertically starting at x = 0, y = 0. /// There are no markers for the end of a vertical scan line since you're assumed to know the size from the header. /// </summary> /// <param name="frame">a raw frame</param> /// <returns>a serialized string</returns> public string SerializeFrame(ConsoleBitmapRawFrame frame) { StringBuilder builder = new StringBuilder(); builder.Append($"[{frame.Timestamp.Ticks}]"); builder.Append("[Raw]"); ConsoleColor?lastFg = null; ConsoleColor?lastBg = null; for (var x = 0; x < frame.Pixels.Length; x++) { for (var y = 0; y < frame.Pixels[0].Length; y++) { if (lastFg.HasValue == false || lastFg.Value != frame.Pixels[x][y].ForegroundColor) { lastFg = frame.Pixels[x][y].ForegroundColor; builder.Append($"[F={lastFg}]"); } if (lastBg.HasValue == false || lastBg.Value != frame.Pixels[x][y].BackgroundColor) { lastBg = frame.Pixels[x][y].BackgroundColor; builder.Append($"[B={lastBg}]"); } string appendValue; var pixelCharValue = frame.Pixels[x][y].Value; if (pixelCharValue == '[') { appendValue = "OB"; } else if (pixelCharValue == ']') { appendValue = "CB"; } else { appendValue = pixelCharValue + ""; } builder.Append('[' + appendValue + ']'); } } builder.AppendLine(); var ret = builder.ToString(); return(ret); }
private ConsoleBitmapRawFrame GetRawFrame(ConsoleBitmap bitmap) { var rawFrame = new ConsoleBitmapRawFrame(); rawFrame.Pixels = new ConsoleCharacter[bitmap.Width][]; for (int x = 0; x < bitmap.Width; x++) { rawFrame.Pixels[x] = new ConsoleCharacter[bitmap.Height]; for (int y = 0; y < bitmap.Height; y++) { var pixelValue = bitmap.GetPixel(x, y).Value.HasValue ? bitmap.GetPixel(x, y).Value.Value : new ConsoleCharacter(' '); rawFrame.Pixels[x][y] = pixelValue; } } return(rawFrame); }
private ConsoleBitmapRawFrame GetRawFrame(ConsoleBitmap bitmap) { var rawFrame = new ConsoleBitmapRawFrame(); rawFrame.Pixels = new ConsoleCharacter[GetEffectiveWidth(bitmap)][]; for (int x = 0; x < GetEffectiveWidth(bitmap); x++) { rawFrame.Pixels[x] = new ConsoleCharacter[GetEffectiveHeight(bitmap)]; for (int y = 0; y < GetEffectiveHeight(bitmap); y++) { var pixel = bitmap.GetPixel(GetEffectiveLeft + x, GetEffectiveTop + y); var pixelValue = pixel.Value.HasValue ? pixel.Value.Value : defaultChar; rawFrame.Pixels[x][y] = pixelValue; } } return(rawFrame); }
/// <summary> /// Writes the given bitmap image as a frame to the stream. If this is the first image or more than half of the pixels have /// changed then a raw frame will be written. Otherwise, a diff frame will be written. /// /// This method uses the system's wall clock to determine the timestamp for this frame. The timestamp will be /// relative to the wall clock time when the first frame was written. /// </summary> /// <param name="bitmap">the image to write</param> /// <returns>the same bitmap that was passed in</returns> public ConsoleBitmap WriteFrame(ConsoleBitmap bitmap) { var rawFrame = GetRawFrame(bitmap); var now = DateTime.UtcNow; if (firstFrameTime.HasValue == false) { rawFrame.Timestamp = TimeSpan.Zero; firstFrameTime = now; } else { rawFrame.Timestamp = now - firstFrameTime.Value; } var frameTime = firstFrameTime.HasValue == false ? TimeSpan.Zero : now - firstFrameTime.Value; if (lastFrame == null) { rawFrame.Timestamp = frameTime; StreamHeader(bitmap); writer.Write(serializer.SerializeFrame(rawFrame)); } else { var diff = PrepareDiffFrame(bitmap); diff.Timestamp = frameTime; if (diff.Diffs.Count > bitmap.Width * bitmap.Height / 2) { writer.Write(serializer.SerializeFrame(rawFrame)); } else if (diff.Diffs.Count > 0) { writer.Write(serializer.SerializeFrame(diff)); } } lastFrame = rawFrame; return(bitmap); }
/// <summary> /// Writes the given bitmap image as a frame to the stream. If this is the first image or more than half of the pixels have /// changed then a raw frame will be written. Otherwise, a diff frame will be written. /// /// This method uses the system's wall clock to determine the timestamp for this frame. The timestamp will be /// relative to the wall clock time when the first frame was written. /// </summary> /// <param name="bitmap">the image to write</param> /// <param name="desiredFrameTime">if provided, sstamp the frame with this time, otherwise stamp it with the wall clock delta from the first frame time</param> /// <param name="force">if true, writes the frame even if there are no changes</param> /// <returns>the same bitmap that was passed in</returns> public ConsoleBitmap WriteFrame(ConsoleBitmap bitmap, bool force = false, TimeSpan?desiredFrameTime = null) { var rawFrame = GetRawFrame(bitmap); var now = DateTime.UtcNow; if (firstFrameTime.HasValue == false) { rawFrame.Timestamp = TimeSpan.Zero; firstFrameTime = now; } else { rawFrame.Timestamp = desiredFrameTime.HasValue ? desiredFrameTime.Value : now - firstFrameTime.Value; } if (lastFrame == null) { StreamHeader(bitmap); writer.Write(serializer.SerializeFrame(rawFrame)); FramesWritten++; } else { var diff = PrepareDiffFrame(bitmap); diff.Timestamp = rawFrame.Timestamp; if (force || diff.Diffs.Count > bitmap.Width * bitmap.Height / 2) { writer.Write(serializer.SerializeFrame(rawFrame)); FramesWritten++; } else if (diff.Diffs.Count > 0) { writer.Write(serializer.SerializeFrame(diff)); FramesWritten++; } } lastFrame = rawFrame; return(bitmap); }
/// <summary> /// Deserializes the given frame given a known width and height. /// </summary> /// <param name="serializedFrame">the frame data</param> /// <param name="width">the known width of the frame</param> /// <param name="height">the known height of the frame</param> /// <returns>a deserialized frame that's either a raw frame or a diff frame, depending on what was in the serialized string</returns> public ConsoleBitmapFrame DeserializeFrame(string serializedFrame, int width, int height) { var tokens = tokenizer.Tokenize(serializedFrame); var reader = new TokenReader<Token>(tokens); reader.Expect("["); var timestampToken = reader.Advance(); var timestamp = new TimeSpan(long.Parse(timestampToken.Value)); reader.Expect("]"); reader.Expect("["); reader.Advance(); var isDiff = reader.Current.Value == "Diff"; reader.Expect("]"); if (isDiff) { var diffFrame = new ConsoleBitmapDiffFrame() { Timestamp = timestamp, Diffs = new System.Collections.Generic.List<ConsoleBitmapPixelDiff>() }; var lastBackground = ConsoleString.DefaultBackgroundColor; var lastForeground = ConsoleString.DefaultForegroundColor; while (reader.CanAdvance(skipWhitespace: true)) { reader.Expect("[", skipWhiteSpace: true); if (reader.Peek().Value.StartsWith("F=") || reader.Peek().Value.StartsWith("B=")) { reader.Advance(); var match = ColorSpecifierRegex.Match(reader.Current.Value); if (match.Success == false) throw new FormatException($"Unexpected token {reader.Current.Value} at position {reader.Current.Position} "); var isForeground = match.Groups["ForB"].Value == "F"; if (isForeground) { if (Enum.TryParse(match.Groups["color"].Value, out ConsoleColor c)) { lastForeground = (RGB)c; } else if (RGB.TryParse(match.Groups["color"].Value, out lastForeground) == false) { throw new ArgumentException($"Expected a color @ {reader.Position}"); } } else { if (Enum.TryParse(match.Groups["color"].Value, out ConsoleColor c)) { lastBackground = (RGB)c; } else if (RGB.TryParse(match.Groups["color"].Value, out lastBackground) == false) { throw new ArgumentException($"Expected a color @ {reader.Position}"); } } reader.Expect("]"); } else { var match = PixelDiffRegex.Match(reader.Advance().Value); if (match.Success == false) throw new FormatException("Could not parse pixel diff"); var valGroup = match.Groups["val"].Value; char? nextChar = valGroup.Length == 1 ? valGroup[0] : valGroup == "OB" ? '[' : valGroup == "CB" ? ']' : new char?(); if (nextChar.HasValue == false) throw new FormatException($"Unexpected token {nextChar} @ {reader.Position}"); diffFrame.Diffs.Add(new ConsoleBitmapPixelDiff() { X = int.Parse(match.Groups["x"].Value), Y = int.Parse(match.Groups["y"].Value), Value = new ConsoleCharacter(nextChar.Value, lastForeground, lastBackground), }); reader.Expect("]"); } } return diffFrame; } else { var rawFrame = new ConsoleBitmapRawFrame() { Timestamp = timestamp, Pixels = new ConsoleCharacter[width][] }; for (var i = 0; i < width; i++) { rawFrame.Pixels[i] = new ConsoleCharacter[height]; } var x = 0; var y = 0; var lastFg = ConsoleString.DefaultForegroundColor; var lastBg = ConsoleString.DefaultBackgroundColor; while (reader.CanAdvance(skipWhitespace:true)) { reader.Expect("[", skipWhiteSpace:true); var next = reader.Advance(); var match = ColorSpecifierRegex.Match(next.Value); if (match.Success) { var isForeground = match.Groups["ForB"].Value == "F"; if (isForeground) { if(Enum.TryParse<ConsoleColor>(match.Groups["color"].Value, out ConsoleColor c)) { lastFg = c; } else if(RGB.TryParse(match.Groups["color"].Value, out lastFg) == false) { throw new ArgumentException($"Expected a color @ {reader.Position}"); } } else { if (Enum.TryParse<ConsoleColor>(match.Groups["color"].Value, out ConsoleColor c)) { lastBg = c; } else if (RGB.TryParse(match.Groups["color"].Value, out lastBg) == false) { throw new ArgumentException($"Expected a color @ {reader.Position}"); } } } else { char? nextChar = next.Value.Length == 1 ? next.Value[0] : next.Value == "OB" ? '[' : next.Value == "CB" ? ']' : new char?(); if (nextChar.HasValue == false) throw new FormatException($"Unexpected token {nextChar} @ {next.Position}"); rawFrame.Pixels[x][y++] = new ConsoleCharacter(nextChar.Value, lastFg, lastBg); if (y == height) { y = 0; x++; } } reader.Expect("]"); } return rawFrame; } }