Skip to content

Commit

Permalink
Single threaded analyzing
Browse files Browse the repository at this point in the history
  • Loading branch information
Rene-Sackers committed Apr 25, 2024
1 parent dbf6b6f commit 54e095e
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 75 deletions.
11 changes: 9 additions & 2 deletions src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
@if (_filteredclips == null)
{
<div class="loading-screen">
Loading...
@if (_state.IsProcessing)
{
@($"{_state.Description ?? "Loading..."} {_state.Percentage}%")
}
else
{
<text>Loading...</text>
}
</div>
}
else
Expand All @@ -25,7 +32,7 @@ else

<MudToolBar Class="py-2">
<MudTooltip Text="Refresh videos/events">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" OnClick="@(() => RefreshEventsAsync(true))"/>
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" OnClick="@(async () => await RefreshEventsAsync(true))"/>
</MudTooltip>
<MudSpacer/>
<div>
Expand Down
39 changes: 34 additions & 5 deletions src/TeslaCamPlayer.BlazorHosted/Client/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public partial class Index : ComponentBase
private bool _showFilter;
private bool _filterChanged;
private EventFilterValues _eventFilter = new();
private State _state = new();

protected override async Task OnInitializedAsync()
{
Expand All @@ -44,7 +45,7 @@ protected override async Task OnInitializedAsync()

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_setDatePickerInitialDate && _filteredclips?.Any() == true && _datePicker != null)
if (!_setDatePickerInitialDate && _filteredclips?.Length > 0 && _datePicker != null)
{
_setDatePickerInitialDate = true;
var latestClip = _filteredclips.MaxBy(c => c.EndDate)!;
Expand All @@ -57,15 +58,43 @@ private async Task RefreshEventsAsync(bool refreshCache)
{
_filteredclips = null;
_clips = null;
_setDatePickerInitialDate = false;
await Task.Delay(10);
await InvokeAsync(StateHasChanged);

_setDatePickerInitialDate = false;
_clips = await HttpClient.GetFromNewtonsoftJsonAsync<Clip[]>("Api/GetClips?refreshCache=" + refreshCache);
if (!refreshCache)
{
_clips = await HttpClient.GetFromNewtonsoftJsonAsync<Clip[]>("Api/GetClips.json");
if (_clips != null)
return;
}

if (refreshCache)
{
_state = new("Refreshing...", 1, 0);
await HttpClient.PostAsync("Api/RefreshClips", null);
}

await WaitForProcessing();
_clips = await HttpClient.GetFromNewtonsoftJsonAsync<Clip[]>("Api/GetClips.json");

FilterClips();
}

private async Task WaitForProcessing()
{
while (true)
{
_state = await HttpClient.GetFromNewtonsoftJsonAsync<State>("Api/GetState.json");
if (!_state.IsProcessing)
break;

_ = InvokeAsync(StateHasChanged);

await Task.Delay(1000);
}
}

private void FilterClips()
{
_filteredclips = (_clips ??= Array.Empty<Clip>())
Expand Down Expand Up @@ -114,7 +143,7 @@ private static string[] GetClipIcons(Clip clip)
};

if (clip.Type == ClipType.Recent || clip.Type == ClipType.Unknown || clip.Event == null)
return new[] { baseIcon };
return [baseIcon];

var secondIcon = clip.Event.Reason switch
{
Expand All @@ -128,7 +157,7 @@ private static string[] GetClipIcons(Clip clip)
if (clip.Event.Reason.StartsWith(CamEvents.SentryAwareAccelerationPrefix))
secondIcon = Icons.Material.Filled.OpenWith;

return secondIcon == null ? new [] { baseIcon } : new[] { baseIcon, secondIcon };
return secondIcon == null ? [baseIcon] : [baseIcon, secondIcon];
}

private class ScrollToOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.11.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.4" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.19.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace TeslaCamPlayer.BlazorHosted.Server.Controllers;

[ApiController]
[Route("Api/[action]")]
[Route("Api")]
public class ApiController : ControllerBase
{
private readonly IClipsService _clipsService;
Expand All @@ -19,18 +19,29 @@ public ApiController(ISettingsProvider settingsProvider, IClipsService clipsServ
_clipsService = clipsService;
}

[HttpGet]
public async Task<Clip[]> GetClips(bool refreshCache = false)
=> await _clipsService.GetClipsAsync(refreshCache);
[HttpGet("GetClips.json")]
public async Task<Clip[]> GetClips()
=> await _clipsService.GetClipsAsync();

[HttpPost("RefreshClips")]
public IActionResult RefreshClips()
{
_clipsService.RefreshClips();
return Ok();
}

[HttpGet("GetState.json")]
public State GetState()
=> _clipsService.State;

private bool IsUnderRootPath(string path)
=> path.StartsWith(_rootFullPath);

[HttpGet("{path}.mp4")]
[HttpGet("Video/{path}.mp4")]
public IActionResult Video(string path)
=> ServeFile(path, ".mp4", "video/mp4", true);

[HttpGet("{path}.png")]
[HttpGet("Thumbnail/{path}.png")]
public IActionResult Thumbnail(string path)
=> ServeFile(path, ".png", "image/png");

Expand Down
7 changes: 4 additions & 3 deletions src/TeslaCamPlayer.BlazorHosted/Server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews().AddNewtonsoftJson();
builder.Services

Check warning on line 15 in src/TeslaCamPlayer.BlazorHosted/Server/Program.cs

View workflow job for this annotation

GitHub Actions / windows-selfcontained

Using member 'Microsoft.Extensions.DependencyInjection.MvcServiceCollectionExtensions.AddControllers(IServiceCollection)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. MVC does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming
.AddControllers()
.AddNewtonsoftJson();
builder.Services.AddRazorPages();

Check warning on line 18 in src/TeslaCamPlayer.BlazorHosted/Server/Program.cs

View workflow job for this annotation

GitHub Actions / windows-selfcontained

Using member 'Microsoft.Extensions.DependencyInjection.MvcServiceCollectionExtensions.AddRazorPages(IServiceCollection)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Razor Pages does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming
builder.Services.AddSingleton<ISettingsProvider, SettingsProvider>();
builder.Services.AddTransient<IClipsService, ClipsService>();
builder.Services.AddSingleton<IClipsService, ClipsService>();
#if WINDOWS
builder.Services.AddTransient<IFfProbeService, FfProbeServiceWindows>();
#elif DOCKER
Expand Down Expand Up @@ -55,7 +57,6 @@

app.UseRouting();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"https": {
"commandName": "Project",
"launchBrowser": false,
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
Expand Down
132 changes: 92 additions & 40 deletions src/TeslaCamPlayer.BlazorHosted/Server/Services/ClipsService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Serilog;
using TeslaCamPlayer.BlazorHosted.Server.Providers.Interfaces;
Expand All @@ -18,63 +13,120 @@ public partial class ClipsService : IClipsService

private static readonly string CacheFilePath = Path.Combine(AppContext.BaseDirectory, "clips.json");
private static readonly Regex FileNameRegex = FileNameRegexGenerated();
private static Clip[] _cache;
private static readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
private static Clip[] _clips;

private readonly ISettingsProvider _settingsProvider;
private readonly IFfProbeService _ffProbeService;

public State State { get; private set; } = new();

public ClipsService(ISettingsProvider settingsProvider, IFfProbeService ffProbeService)
{
_settingsProvider = settingsProvider;
_ffProbeService = ffProbeService;
}

private async Task<Clip[]> GetCachedAsync()
=> File.Exists(CacheFilePath)
? JsonConvert.DeserializeObject<Clip[]>(await File.ReadAllTextAsync(CacheFilePath))
: null;
private static async Task<Clip[]> GetCachedAsync()
{
try
{
return File.Exists(CacheFilePath)
? JsonConvert.DeserializeObject<Clip[]>(await File.ReadAllTextAsync(CacheFilePath))
: null;
}
catch (Exception e)
{
Log.Error(e, "Failed to read cached clips data from {Path}", CacheFilePath);
return null;
}
}

public async Task<Clip[]> GetClipsAsync(bool refreshCache = false)
public async Task<Clip[]> GetClipsAsync()
{
_cache ??= await GetCachedAsync();
_clips ??= await GetCachedAsync();

if (!refreshCache && _cache != null)
return _cache;
if (_clips != null)
return _clips;

if (!State.IsProcessing)
State = new("", 1, 1);

_ = Task.Run(RefreshClips);
return null;
}

public async void RefreshClips()
{
var isRefreshing = RefreshSemaphore.CurrentCount == 0;
await RefreshSemaphore.WaitAsync();
if (isRefreshing)
return;

try
{
await RefreshClipsInternal();
}
catch (Exception e)
{
Log.Error(e, "Failed to refresh clips");
}
finally
{
RefreshSemaphore.Release();
}
}

_cache ??= [];
private async Task RefreshClipsInternal()
{
var knownClips = _clips ?? [];

var knownVideoFiles = _cache
var knownVideoFiles = knownClips
.SelectMany(c => c.Segments.SelectMany(s => s.VideoFiles))
.Where(f => f != null)
.ToDictionary(v => v.FilePath, v => v);

var videoFiles = (await Task.WhenAll(Directory
var files = Directory
.GetFiles(_settingsProvider.Settings.ClipsRootPath, "*.mp4", SearchOption.AllDirectories)
.AsParallel()
.Select(path => new { Path = path, RegexMatch = FileNameRegex.Match(path) })
.Where(f => f.RegexMatch.Success)
.ToList()
.Select(async f => knownVideoFiles.TryGetValue(f.Path, out var knownVideo) ? knownVideo : await TryParseVideoFileAsync(f.Path, f.RegexMatch))))
.AsParallel()
.Where(vfi => vfi != null)
.ToList();
.ToArray();

var recentClips = GetRecentClips(videoFiles
.Where(vfi => vfi.ClipType == ClipType.Recent).ToList());

var clips = videoFiles
var videoFiles = new List<VideoFile>(files.Length);
for (var i = 0; i < files.Length; i++)
{
State = new("Analyzing video files...", files.Length, i + 1);

var file = files[i];

if (knownVideoFiles.TryGetValue(file.Path, out var knownVideo))
videoFiles.Add(knownVideo);

var parsedFile = await TryParseVideoFileAsync(file.Path, file.RegexMatch);
if (parsedFile != null)
videoFiles.Add(parsedFile);
}

var recentClips = GetRecentClips(videoFiles.Where(vfi => vfi.ClipType == ClipType.Recent).ToList());

var distinctClips = videoFiles
.Select(vfi => vfi.EventFolderName)
.Where(f => !string.IsNullOrWhiteSpace(f))
.Distinct()
.AsParallel()
.Where(e => !string.IsNullOrWhiteSpace(e))
.Select(e => ParseClip(e, videoFiles))
.Concat(recentClips.AsParallel())
.OrderByDescending(c => c.StartDate)
.ToArray();
var clips = new List<Clip>();
for (var i = 0; i < distinctClips.Length; i++)
{
clips.Add(await ParseClip(distinctClips[i], videoFiles));
State = new("Reading event data...", distinctClips.Length, i + 1);
}

_cache = clips;
await File.WriteAllTextAsync(CacheFilePath, JsonConvert.SerializeObject(clips));
return _cache;
_clips = clips
.Concat(recentClips)
.OrderByDescending(c => c.StartDate)
.ToArray();

await File.WriteAllTextAsync(CacheFilePath, JsonConvert.SerializeObject(_clips));
}

private static IEnumerable<Clip> GetRecentClips(List<VideoFile> recentVideoFiles)
Expand Down Expand Up @@ -190,7 +242,7 @@ private async Task<VideoFile> ParseVideoFileAsync(string path, Match regexMatch)
};
}

private static Clip ParseClip(string eventFolderName, IEnumerable<VideoFile> videoFiles)
private static async Task<Clip> ParseClip(string eventFolderName, IEnumerable<VideoFile> videoFiles)
{
var eventVideoFiles = videoFiles
.AsParallel()
Expand All @@ -213,7 +265,7 @@ private static Clip ParseClip(string eventFolderName, IEnumerable<VideoFile> vid

var eventFolderPath = Path.GetDirectoryName(eventVideoFiles.First().FilePath)!;
var expectedEventJsonPath = Path.Combine(eventFolderPath, "event.json");
var eventInfo = TryReadEvent(expectedEventJsonPath);
var eventInfo = await TryReadEvent(expectedEventJsonPath);

var expectedEventThumbnailPath = Path.Combine(eventFolderPath, "thumb.png");
var thumbnailUrl = File.Exists(expectedEventThumbnailPath)
Expand All @@ -227,14 +279,14 @@ private static Clip ParseClip(string eventFolderName, IEnumerable<VideoFile> vid
};
}

private static Event TryReadEvent(string path)
private static async Task<Event> TryReadEvent(string path)
{
try
{
if (!File.Exists(path))
return null;

var json = File.ReadAllText(path);
var json = await File.ReadAllTextAsync(path);
return JsonConvert.DeserializeObject<Event>(json);
}
catch (Exception e)
Expand Down
Loading

0 comments on commit 54e095e

Please sign in to comment.