Skip to content

Commit

Permalink
Fix FramebufferHeader.ToBitmap
Browse files Browse the repository at this point in the history
  • Loading branch information
wherewhere committed Dec 27, 2023
1 parent 1fbf06f commit 3bc57e8
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 40 deletions.
2 changes: 1 addition & 1 deletion AdvancedSharpAdbClient.Tests/AdbClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ public void ExecuteRemoteCommandUnresponsiveTest()
[Fact]
public void CreateRefreshableFramebufferTest()
{
Framebuffer framebuffer = TestClient.CreateRefreshableFramebuffer(Device);
Framebuffer framebuffer = TestClient.CreateFramebuffer(Device);
Assert.NotNull(framebuffer);
Assert.Equal(Device, framebuffer.Device);
}
Expand Down
2 changes: 1 addition & 1 deletion AdvancedSharpAdbClient.Tests/Dummys/DummyAdbClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public Task ExecuteServerCommandAsync(string target, string command, IAdbSocket

Task<int> IAdbClient.CreateForwardAsync(DeviceData device, string local, string remote, bool allowRebind, CancellationToken cancellationToken) => throw new NotImplementedException();

Framebuffer IAdbClient.CreateRefreshableFramebuffer(DeviceData device) => throw new NotImplementedException();
Framebuffer IAdbClient.CreateFramebuffer(DeviceData device) => throw new NotImplementedException();

int IAdbClient.CreateReverseForward(DeviceData device, string remote, string local, bool allowRebind) => throw new NotImplementedException();

Expand Down
2 changes: 1 addition & 1 deletion AdvancedSharpAdbClient/AdbClient.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public virtual async Task<Framebuffer> GetFrameBufferAsync(DeviceData device, Ca
{
EnsureDevice(device);

Framebuffer framebuffer = CreateRefreshableFramebuffer(device);
Framebuffer framebuffer = CreateFramebuffer(device);
await framebuffer.RefreshAsync(true, cancellationToken).ConfigureAwait(false);

// Convert the framebuffer to an image, and return that.
Expand Down
4 changes: 2 additions & 2 deletions AdvancedSharpAdbClient/AdbClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ public virtual void ExecuteRemoteCommand(string command, DeviceData device, IShe
}

/// <inheritdoc/>
public Framebuffer CreateRefreshableFramebuffer(DeviceData device)
public Framebuffer CreateFramebuffer(DeviceData device)
{
EnsureDevice(device);
return new Framebuffer(device, this, AdbSocketFactory);
Expand All @@ -421,7 +421,7 @@ public Framebuffer GetFrameBuffer(DeviceData device)
{
EnsureDevice(device);

Framebuffer framebuffer = CreateRefreshableFramebuffer(device);
Framebuffer framebuffer = CreateFramebuffer(device);
framebuffer.Refresh(true);

// Convert the framebuffer to an image, and return that.
Expand Down
4 changes: 2 additions & 2 deletions AdvancedSharpAdbClient/Interfaces/IAdbClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,12 @@ public partial interface IAdbClient
void ExecuteRemoteCommand(string command, DeviceData device, IShellOutputReceiver receiver, Encoding encoding);

/// <summary>
/// Gets a <see cref="Framebuffer"/> which contains the framebuffer data for this device. The framebuffer data can be refreshed,
/// Gets a <see cref="Framebuffer"/> which contains the framebuffer data for this device. The framebuffer data is not refreshed,
/// giving you high performance access to the device's framebuffer.
/// </summary>
/// <param name="device">The device for which to get the framebuffer.</param>
/// <returns>A <see cref="Framebuffer"/> object which can be used to get the framebuffer of the device.</returns>
Framebuffer CreateRefreshableFramebuffer(DeviceData device);
Framebuffer CreateFramebuffer(DeviceData device);

/// <summary>
/// Gets the frame buffer from the specified end point.
Expand Down
12 changes: 6 additions & 6 deletions AdvancedSharpAdbClient/Models/Framebuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,10 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous task.</param>
/// <returns>An <see cref="WriteableBitmap"/> which represents the framebuffer data.</returns>
public virtual Task<WriteableBitmap?> ToBitmap(CancellationToken cancellationToken = default)
public virtual Task<WriteableBitmap?> ToBitmapAsync(CancellationToken cancellationToken = default)
{
EnsureNotDisposed();
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, cancellationToken);
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmapAsync(Data, cancellationToken);
}

/// <summary>
Expand All @@ -242,10 +242,10 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can
/// <param name="dispatcher">The target <see cref="CoreDispatcher"/> to invoke the code on.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous task.</param>
/// <returns>An <see cref="WriteableBitmap"/> which represents the framebuffer data.</returns>
public virtual Task<WriteableBitmap?> ToBitmap(CoreDispatcher dispatcher, CancellationToken cancellationToken = default)
public virtual Task<WriteableBitmap?> ToBitmapAsync(CoreDispatcher dispatcher, CancellationToken cancellationToken = default)
{
EnsureNotDisposed();
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, dispatcher, cancellationToken);
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmapAsync(Data, dispatcher, cancellationToken);
}

/// <summary>
Expand All @@ -254,10 +254,10 @@ public virtual async Task RefreshAsync(bool reset = false, CancellationToken can
/// <param name="dispatcher">The target <see cref="DispatcherQueue"/> to invoke the code on.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous task.</param>
/// <returns>An <see cref="WriteableBitmap"/> which represents the framebuffer data.</returns>
public virtual Task<WriteableBitmap?> ToBitmap(DispatcherQueue dispatcher, CancellationToken cancellationToken = default)
public virtual Task<WriteableBitmap?> ToBitmapAsync(DispatcherQueue dispatcher, CancellationToken cancellationToken = default)
{
EnsureNotDisposed();
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmap(Data, dispatcher, cancellationToken);
return Data == null ? throw new InvalidOperationException($"Call {nameof(RefreshAsync)} first") : Header.ToBitmapAsync(Data, dispatcher, cancellationToken);
}
#endif

Expand Down
95 changes: 68 additions & 27 deletions AdvancedSharpAdbClient/Models/FramebufferHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)

// By far, the most common format is a 32-bit pixel format, which is either
// RGB or RGBA, where each color has 1 byte.
if (Bpp == 32)
if (Bpp == 8 * 4)
{
// Require at least RGB to be present; and require them to be exactly one byte (8 bits) long.
if (Red.Length != 8 || Blue.Length != 8 || Green.Length != 8)
Expand Down Expand Up @@ -391,7 +391,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
// Returns RGB or RGBA, function of the presence of an alpha channel.
return Alpha.Length == 0 ? PixelFormat.Format32bppRgb : PixelFormat.Format32bppArgb;
}
else if (Bpp == 24)
else if (Bpp == 8 * 3)
{
// For 24-bit image depths, we only support RGB.
if (Red.Offset == 0
Expand All @@ -406,7 +406,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
return PixelFormat.Format24bppRgb;
}
}
else if (Bpp == 16
else if (Bpp == 5 + 6 + 5
&& Red.Offset == 11
&& Red.Length == 5
&& Green.Offset == 5
Expand Down Expand Up @@ -434,11 +434,11 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous task.</param>
/// <returns>A <see cref="WriteableBitmap"/> that represents the image contained in the frame buffer, or <see langword="null"/>
/// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device.</returns>
public Task<WriteableBitmap?> ToBitmap(byte[] buffer, CoreDispatcher dispatcher, CancellationToken cancellationToken = default)
public Task<WriteableBitmap?> ToBitmapAsync(byte[] buffer, CoreDispatcher dispatcher, CancellationToken cancellationToken = default)
{
if (dispatcher.HasThreadAccess)
{
return ToBitmap(buffer, cancellationToken);
return ToBitmapAsync(buffer, cancellationToken);
}
else
{
Expand All @@ -450,7 +450,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
{
try
{
WriteableBitmap? result = await self.ToBitmap(buffer, cancellationToken).ConfigureAwait(false);
WriteableBitmap? result = await self.ToBitmapAsync(buffer, cancellationToken).ConfigureAwait(false);
taskCompletionSource.SetResult(result);
}
catch (Exception e)
Expand All @@ -472,11 +472,11 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
/// <returns>A <see cref="WriteableBitmap"/> that represents the image contained in the frame buffer, or <see langword="null"/>
/// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device.</returns>
[ContractVersion(typeof(UniversalApiContract), 327680u)]
public Task<WriteableBitmap?> ToBitmap(byte[] buffer, DispatcherQueue dispatcher, CancellationToken cancellationToken = default)
public Task<WriteableBitmap?> ToBitmapAsync(byte[] buffer, DispatcherQueue dispatcher, CancellationToken cancellationToken = default)
{
if (ApiInformation.IsMethodPresent("Windows.System.DispatcherQueue", "HasThreadAccess") && dispatcher.HasThreadAccess)
{
return ToBitmap(buffer, cancellationToken);
return ToBitmapAsync(buffer, cancellationToken);
}
else
{
Expand All @@ -488,7 +488,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
{
try
{
WriteableBitmap? result = await self.ToBitmap(buffer, cancellationToken).ConfigureAwait(false);
WriteableBitmap? result = await self.ToBitmapAsync(buffer, cancellationToken).ConfigureAwait(false);
taskCompletionSource.SetResult(result);
}
catch (Exception e)
Expand All @@ -511,7 +511,7 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous task.</param>
/// <returns>A <see cref="WriteableBitmap"/> that represents the image contained in the frame buffer, or <see langword="null"/>
/// if the framebuffer does not contain any data. This can happen when DRM is enabled on the device.</returns>
public async Task<WriteableBitmap?> ToBitmap(byte[] buffer, CancellationToken cancellationToken = default)
public async Task<WriteableBitmap?> ToBitmapAsync(byte[] buffer, CancellationToken cancellationToken = default)
{
if (buffer == null)
{
Expand All @@ -525,26 +525,67 @@ private PixelFormat StandardizePixelFormat(byte[] buffer)
return null;
}

using MemoryStream stream = new(buffer);
using IRandomAccessStream randomAccessStream = stream.AsRandomAccessStream();
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(randomAccessStream).AsTask(cancellationToken);
SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync().AsTask(cancellationToken);
try
// The pixel format of the framebuffer may not be one that WinRT recognizes, so we need to fix that
BitmapPixelFormat bitmapPixelFormat = StandardizePixelFormat(buffer, out BitmapAlphaMode alphaMode);

using InMemoryRandomAccessStream random = new();
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, random).AsTask(cancellationToken);
encoder.SetPixelData(bitmapPixelFormat, alphaMode, Width, Height, 960, 960, buffer);
await encoder.FlushAsync().AsTask(cancellationToken);
WriteableBitmap WriteableImage = new((int)Width, (int)Height);
await WriteableImage.SetSourceAsync(random).AsTask(cancellationToken).ConfigureAwait(false);

return WriteableImage;
}

/// <summary>
/// Returns the <see cref="BitmapPixelFormat"/> that describes pixel format of an image that is stored according to the information
/// present in this <see cref="FramebufferHeader"/>. Because the <see cref="BitmapPixelFormat"/> enumeration does not allow for all
/// formats supported by Android, this method also takes a <paramref name="buffer"/> and reorganizes the bytes in the buffer to
/// match the return value of this function.
/// </summary>
/// <param name="buffer">A byte array in which the images are stored according to this <see cref="FramebufferHeader"/>.</param>
/// <param name="alphaMode">A <see cref="BitmapAlphaMode"/> which describes how the alpha channel is stored.</param>
/// <returns>A <see cref="BitmapPixelFormat"/> that describes how the image data is represented in this <paramref name="buffer"/>.</returns>
private BitmapPixelFormat StandardizePixelFormat(byte[] buffer, out BitmapAlphaMode alphaMode)
{
// Initial parameter validation.
ExceptionExtensions.ThrowIfNull(buffer);

if (buffer.Length < Width * Height * (Bpp / 8))
{
throw new ArgumentOutOfRangeException(nameof(buffer), $"The buffer length {buffer.Length} is less than expected buffer " +
$"length ({Width * Height * (Bpp / 8)}) for a picture of width {Width}, height {Height} and pixel depth {Bpp}");
}

if (Width == 0 || Height == 0 || Bpp == 0)
{
WriteableBitmap WriteableImage = new((int)decoder.PixelWidth, (int)decoder.PixelHeight);
await WriteableImage.SetSourceAsync(randomAccessStream).AsTask(cancellationToken);
return WriteableImage;
throw new InvalidOperationException("Cannot cannulate the pixel format of an empty framebuffer");
}
catch when (!cancellationToken.IsCancellationRequested)
{
using InMemoryRandomAccessStream random = new();
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, random).AsTask(cancellationToken);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync().AsTask(cancellationToken);
WriteableBitmap WriteableImage = new((int)decoder.PixelWidth, (int)decoder.PixelHeight);
await WriteableImage.SetSourceAsync(random).AsTask(cancellationToken);
return WriteableImage;

// By far, the most common format is a 32-bit pixel format, which is either
// RGB or RGBA, where each color has 1 byte.
if (Bpp == 8 * 4)
{
// Require at least RGB to be present; and require them to be exactly one byte (8 bits) long.
if (Red.Length != 8 || Blue.Length != 8 || Green.Length != 8)
{
throw new ArgumentOutOfRangeException($"The pixel format with with RGB lengths of {Red.Length}:{Blue.Length}:{Green.Length} is not supported");
}

// Alpha can be present or absent, but must be 8 bytes long
alphaMode = Alpha.Length switch
{
0 => BitmapAlphaMode.Premultiplied,
8 => BitmapAlphaMode.Ignore,
_ => throw new ArgumentOutOfRangeException($"The alpha length {Alpha.Length} is not supported"),
};

return BitmapPixelFormat.Rgba8;
}

// If not caught by any of the statements before, the format is not supported.
throw new NotSupportedException($"Pixel depths of {Bpp} are not supported");
}
#endif

Expand Down

0 comments on commit 3bc57e8

Please sign in to comment.