Skip to content

Commit

Permalink
Streamed image work
Browse files Browse the repository at this point in the history
  • Loading branch information
TheArcaneBrony committed Jul 1, 2024
1 parent d63d758 commit 3fb65c1
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseBlazorWebAssembly>true</UseBlazorWebAssembly>
<LangVersion>preview</LangVersion>
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>


Expand All @@ -21,8 +23,4 @@
<ProjectReference Include="..\ArcaneLibs\ArcaneLibs.csproj"/>
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>

</Project>
73 changes: 73 additions & 0 deletions ArcaneLibs.Blazor.Components/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace ArcaneLibs.Blazor.Components;

public static class StreamExtensions {
private static IJSObjectReference? _streamExtensionsModule;

// public static async Task<string> GetBlobUriAsync(this IJSRuntime jsRuntime, Stream stream, bool revokeAfterFinish = true, int revocationDelay = 1000) {
// if (_streamExtensionsModule is null) {
// Console.WriteLine("Importing stream extensions module");
// _streamExtensionsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/ArcaneLibs.Blazor.Components/streamExtensions.js");
// }
//
// Console.WriteLine($"Blob stream {stream.GetHashCode()} position {stream.Position} length {stream.Length}");
// if (revokeAfterFinish) {
// Task.Run(async () => {
// var isDisposed = false;
// long lastPosition = 0;
// while (!isDisposed) {
// try {
// await Task.Delay(revocationDelay);
// Console.WriteLine($"Blob stream {stream.GetHashCode()} position {stream.Position} length {stream.Length}");
// }
// catch (ObjectDisposedException) {
// isDisposed = true;
// }
// }
//
// Console.WriteLine($"Blob stream {stream.GetHashCode()} disposed, revoking after {lastPosition} bytes!");
// await _streamExtensionsModule.InvokeVoidAsync("revokeBlobUri", stream);
// });
// }
//
// using var streamRef = new DotNetStreamReference(stream, true);
// Console.WriteLine("got streamRef");
// return await _streamExtensionsModule.InvokeAsync<string>("getBlobUri", streamRef);
// }


public static async Task<string> streamImage(this IJSRuntime jsRuntime, Stream stream, ElementReference element, bool revokeAfterFinish = false, int revocationDelay = 1000) {
if (_streamExtensionsModule is null) {
Console.WriteLine("Importing stream extensions module");
_streamExtensionsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/ArcaneLibs.Blazor.Components/streamExtensions.js");
Console.WriteLine("Imported stream extensions module");
}

Console.WriteLine($"Blob stream {stream.GetHashCode()}");
if (revokeAfterFinish) {
Task.Run(async () => {

Check warning on line 50 in ArcaneLibs.Blazor.Components/StreamExtensions.cs

View workflow job for this annotation

GitHub Actions / build

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
var isDisposed = false;
long lastPosition = 0;
while (!isDisposed) {
try {
await Task.Delay(revocationDelay);
Console.WriteLine($"Blob stream {stream.GetHashCode()} position {stream.Position} length {stream.Length}");
}
catch (ObjectDisposedException) {
isDisposed = true;
}
}
Console.WriteLine($"Blob stream {stream.GetHashCode()} disposed, revoking after {lastPosition} bytes!");
await _streamExtensionsModule.InvokeVoidAsync("revokeBlobUri", stream);
});
}

using var streamRef = new DotNetStreamReference(stream, true);
Console.WriteLine("got streamRef");
return await _streamExtensionsModule.InvokeAsync<string>("streamImage", streamRef, element);
}

}
67 changes: 67 additions & 0 deletions ArcaneLibs.Blazor.Components/StreamedImage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
<img src="@_finalBlob" srcref="@(_self)" @attributes="@(OtherAttributes!)"/> @* @ref="Self" *@

@code {
private static IJSObjectReference? _streamExtensionsModule;
private static SemaphoreSlim _importSemaphore = new(1, 1);
private static SemaphoreSlim _streamSemaphore = new(16, 16);
private string _self = Guid.NewGuid().ToString();

private Stream? _stream;

// private ElementReference Self {
// get => _self;
// set {
// Console.WriteLine($"StreamedImage: {_self.Id} -> {value.Id}");
// _self = value;
@* } *@
@* } *@

private string? _finalBlob;
private CancellationTokenSource _cts = new();
// private ElementReference _self;
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> OtherAttributes { get; set; } = [];

[Parameter]
public Stream? Stream {

Check warning on line 29 in ArcaneLibs.Blazor.Components/StreamedImage.razor

View workflow job for this annotation

GitHub Actions / build

Component parameter 'ArcaneLibs.Blazor.Components.StreamedImage.Stream' should be auto property
get => _stream;
set {
if (_stream == value) return;
_stream = value;
_cts.Cancel();
_cts = new();
_ = StreamHasChanged(value, _cts.Token);
}
}

protected override async Task OnInitializedAsync() { }

Check warning on line 40 in ArcaneLibs.Blazor.Components/StreamedImage.razor

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 40 in ArcaneLibs.Blazor.Components/StreamedImage.razor

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

private async Task StreamHasChanged(Stream? stream, CancellationToken? cancellationToken) {
if (stream is null) return;
await _importSemaphore.WaitAsync();
if (_streamExtensionsModule is null) {
Console.WriteLine("Importing StreamedImage JS module");
_streamExtensionsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/ArcaneLibs.Blazor.Components/StreamedImage.razor.js");
Console.WriteLine("Successfully imported StreamedImage JS module");
}
_importSemaphore.Release();

StateHasChanged();

using var streamRef = new DotNetStreamReference(stream, true);
// Console.WriteLine($"StreamedImage: got streamRef {streamRef.GetHashCode()} for stream {stream.GetHashCode()}");
await _streamSemaphore.WaitAsync();
Console.WriteLine($"StreamedImage: Attempting to invoke streamImage({streamRef}, {_self})");
var finalBlob = await _streamExtensionsModule.InvokeAsync<string>("streamImage", streamRef, _self);
_streamSemaphore.Release();
// Console.WriteLine("StreamedImage.razor: got finalBlob " + finalBlob);
// var finalBlob = await JSRuntime.streamImage(Stream, Self);
if (cancellationToken is { IsCancellationRequested: true }) return;
_finalBlob = finalBlob;
StateHasChanged();
}

}
43 changes: 43 additions & 0 deletions ArcaneLibs.Blazor.Components/StreamedImage.razor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export async function streamImage(stream, srcRef) {
console.log("streamImage start");
const actualStream = await stream.stream();
const reader = actualStream.getReader();

console.log(`StreamedImage: streamImage started for srcRef: ${srcRef}`);

const chunks = [];
while (true) {
const {done, value} = await reader.read();
chunks.push(value);

if (chunks.length <= 10 || chunks.length % 10 === 0 || done) {
//srcRef attribute on img tag
let imageElement = document.querySelector(`img[srcref="${srcRef}"]`);
console.log('meow', srcRef, imageElement);
// console.log(`StreamedImage: streamImage done: ${done}, value: ${value?.length}, chunks: ${chunks.length}`);
let blob = new Blob(chunks);
const url = window.URL.createObjectURL(blob);
imageElement.src = url;
// console.log(`StreamedImage: streamImage src: ${url} - done: ${done}`)

if (done) {
console.log(`StreamedImage.razor.js: streamImage finished in chunks: ${chunks.length} -> ${url}`);
return url;
} else {
// Dispose old URLs to avoid memory leaks
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 100);
}
}

await new Promise(resolve => setTimeout(resolve, chunks.length * 5));

//yield to other tasks
// if(chunks.length > 100) {
// let delay = Math.min(2000, chunks.length * 10);
// console.warn(`StreamedImage: streamImage waiting ${delay} for chunks: ${chunks.length}, last read length: ${value?.length}`);
// await new Promise(resolve => setTimeout(resolve, delay));
// }
}
}
25 changes: 25 additions & 0 deletions ArcaneLibs.Blazor.Components/wwwroot/streamExtensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export async function streamImage(stream, imageElement) {
const actualStream = await stream.stream();
const reader = actualStream.getReader();

const chunks = [];
let blob = null;
while (true) {
const {done, value} = await reader.read();
console.log(`streamImage done: ${done}, value: ${value?.length}, chunks: ${chunks.length}`);
chunks.push(value);

if (blob !== null) {
window.URL.revokeObjectURL(blob);
}
blob = new Blob(chunks, {type: 'image/jpeg'});
const url = window.URL.createObjectURL(blob);
imageElement.src = url;
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 100);
if (done) {
break;
}
}
}
1 change: 0 additions & 1 deletion ArcaneLibs/Extensions/UriExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public static Uri AddQuery(this Uri uri, string name, string value) {
var newQuery = HttpUtility.ParseQueryString(query ?? "");
newQuery[name] = value;
// Console.WriteLine("OriginalString: " + uri.OriginalString);

return new Uri(location + "?" + newQuery, uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative);
}
}

0 comments on commit 3fb65c1

Please sign in to comment.