Skip to content

Commit

Permalink
Merge pull request microsoft#218 from michaelstonis/feature/image-pro…
Browse files Browse the repository at this point in the history
…cessing

Image Processing Service
  • Loading branch information
jamesmontemagno authored Apr 3, 2023
2 parents 1d1cf4f + 0c039ad commit 28ede25
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 100 deletions.
4 changes: 3 additions & 1 deletion src/Mobile/Microsoft.NetConf2021.Maui.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<!-- iOS, Android, MacCatalyst -->
Expand Down Expand Up @@ -65,6 +65,8 @@
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.0.0" />
<PackageReference Include="MonkeyCache.FileStore" Version="2.0.0-beta" />
<PackageReference Include="Refractored.MvvmHelpers" Version="1.6.2" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="System.Threading.RateLimiting" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Mobile/Pages/EpisodeDetailPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
Spacing="8">
<Image Margin="0,4,0,8"
HeightRequest="156"
Source="{Binding Image}"
Source="{Binding Show.CachedImage, TargetNullValue='default_podcast_image.png'}"
SemanticProperties.Description="It is the banner of the episode"
WidthRequest="156" />
<Label Style="{StaticResource H6LabelStyle}"
Expand Down
4 changes: 2 additions & 2 deletions src/Mobile/Pages/ShowDetailPage.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="Microsoft.NetConf2021.Maui.Pages.ShowDetailPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Expand Down Expand Up @@ -100,7 +100,7 @@
Grid.Row="0"
Grid.RowSpan="{OnIdiom Default=3, Phone=2}"
HeightRequest="{OnIdiom Default=230, Phone=156}"
Source="{Binding Show.Image, FallbackValue=default_podcast_image.png}"
Source="{Binding Show.CachedImage, TargetNullValue='default_podcast_image.png'}"
WidthRequest="{OnIdiom Default=230, Phone=156}" />

<Label Grid.Column="1"
Expand Down
94 changes: 94 additions & 0 deletions src/Mobile/Services/ImageProcessingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Threading.RateLimiting;
using Microsoft.Extensions.Logging;
using SkiaSharp;

namespace Microsoft.NetConf2021.Maui.Services;

public class ImageProcessingService
{
private readonly HttpClient httpClient = new HttpClient();

private readonly RateLimiter rateLimiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = 3, QueueProcessingOrder = QueueProcessingOrder.NewestFirst });

private readonly object cacheDirectoryLock = new object();

private readonly string imageCacheDirectory;

public ImageProcessingService()
{
imageCacheDirectory = Path.Combine(FileSystem.CacheDirectory, "remoteimages");
}

public async Task<string> ProcessRemoteImage(Uri imageUri, int maxHeight = 250, int maxWidth = 250, int quality = 75, SKEncodedImageFormat imageFormat = SKEncodedImageFormat.Png)
{
var imageUriHash = $"{Crc64.ComputeHashString(imageUri.ToString())}.{imageFormat}".ToLowerInvariant();

var cachedImagePath = Path.Combine(imageCacheDirectory, imageUriHash);

lock (cacheDirectoryLock)
{
if (!Directory.Exists(imageCacheDirectory))
{
Directory.CreateDirectory(imageCacheDirectory);
}
}

if (File.Exists(cachedImagePath))
{
return cachedImagePath;
}

RateLimitLease lease = rateLimiter.AttemptAcquire();

while (!lease.IsAcquired)
{
lease = await rateLimiter.AcquireAsync().ConfigureAwait(false);
}

SKBitmap bmp = null;

try
{
using var imageStream = await httpClient.GetStreamAsync(imageUri).ConfigureAwait(false);
using var data = SKData.Create(imageStream);
using var codec = SKCodec.Create(data);

var info = codec.Info;

var maxSize = Math.Max(maxHeight, maxWidth);

var supportedScale =
info.Height > info.Width
? (float)maxHeight / info.Height
: (float)maxWidth / info.Width;

var scaledWidth = (int)(info.Width * supportedScale);
var scaledHeight = (int)(info.Height * supportedScale);

// decode the bitmap at the nearest size
var nearest = new SKImageInfo(scaledWidth, scaledHeight);

bmp = SKBitmap.Decode(codec);

SKImageInfo desired = new SKImageInfo(scaledWidth, scaledHeight);
bmp = bmp.Resize(desired, SKFilterQuality.High);

using var image = SKImage.FromBitmap(bmp);
using var encodedImage = image.Encode(imageFormat, quality);
using var encodedImageStream = encodedImage.AsStream();
using var file = File.Create(cachedImagePath);
await encodedImageStream.CopyToAsync(file).ConfigureAwait(false);
}
finally
{
bmp?.Dispose();
bmp = null;

lease.Dispose();
}

return cachedImagePath;
}
}

2 changes: 2 additions & 0 deletions src/Mobile/Services/ServicesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static MauiAppBuilder ConfigureServices(this MauiAppBuilder builder)
builder.Services.AddScoped<ListenTogetherHubClient>(_ =>
new ListenTogetherHubClient(Config.ListenTogetherUrl));

builder.Services.AddSingleton<ImageProcessingService>();


return builder;
}
Expand Down
10 changes: 6 additions & 4 deletions src/Mobile/ViewModels/CategoryViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
[QueryProperty(nameof(Id), nameof(Id))]
public partial class CategoryViewModel : ViewModelBase
{
private readonly ShowsService showsService;
private readonly SubscriptionsService subscriptionsService;
private readonly ImageProcessingService imageProcessingService;

[ObservableProperty]
string text;
Expand All @@ -15,13 +18,12 @@ public partial class CategoryViewModel : ViewModelBase
[ObservableProperty]
List<ShowViewModel> shows;

readonly ShowsService showsService;
readonly SubscriptionsService subscriptionsService;

public CategoryViewModel(ShowsService shows, SubscriptionsService subs)
public CategoryViewModel(ShowsService shows, SubscriptionsService subs, ImageProcessingService imageProcessing)
{
showsService = shows;
subscriptionsService = subs;
imageProcessingService = imageProcessing;
}


Expand Down Expand Up @@ -63,7 +65,7 @@ List<ShowViewModel> LoadShows(IEnumerable<Show> shows)

foreach (var show in shows)
{
var showVM = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id));
var showVM = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id), imageProcessingService);
showList.Add(showVM);
}

Expand Down
14 changes: 9 additions & 5 deletions src/Mobile/ViewModels/DiscoverViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ namespace Microsoft.NetConf2021.Maui.ViewModels;

public partial class DiscoverViewModel : ViewModelBase
{
readonly ShowsService showsService;
readonly SubscriptionsService subscriptionsService;
IEnumerable<ShowViewModel> shows;
private readonly ShowsService showsService;
private readonly SubscriptionsService subscriptionsService;
private readonly ImageProcessingService imageProcessingService;

private IEnumerable<ShowViewModel> shows;

[ObservableProperty]
CategoriesViewModel categoriesVM;
Expand All @@ -18,10 +20,12 @@ public partial class DiscoverViewModel : ViewModelBase
[ObservableProperty]
ObservableRangeCollection<ShowGroup> podcastsGroup;

public DiscoverViewModel(ShowsService shows, SubscriptionsService subs, CategoriesViewModel categories)
public DiscoverViewModel(ShowsService shows, SubscriptionsService subs, CategoriesViewModel categories, ImageProcessingService imageProcessing)
{
showsService = shows;
subscriptionsService = subs;
imageProcessingService = imageProcessing;

PodcastsGroup = new ObservableRangeCollection<ShowGroup>();
categoriesVM = categories;
}
Expand Down Expand Up @@ -60,7 +64,7 @@ private List<ShowViewModel> ConvertToViewModels(IEnumerable<Show> shows)
var viewmodels = new List<ShowViewModel>();
foreach (var show in shows)
{
var showViewModel = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id));
var showViewModel = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id), imageProcessingService);
viewmodels.Add(showViewModel);
}

Expand Down
36 changes: 20 additions & 16 deletions src/Mobile/ViewModels/EpisodeDetailViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ namespace Microsoft.NetConf2021.Maui.ViewModels;
[QueryProperty(nameof(ShowId), nameof(ShowId))]
public partial class EpisodeDetailViewModel : ViewModelBase
{
private readonly ListenLaterService listenLaterService;
private readonly ShowsService podcastService;
private readonly PlayerService playerService;
private readonly SubscriptionsService subscriptionsService;
private readonly ImageProcessingService imageProcessingService;

public string Id { get; set; }
public string ShowId { get; set; }


[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInListenLater))]
Episode episode;


public bool IsInListenLater
{
get => episode?.IsInListenLater ?? false;
Expand All @@ -29,20 +33,15 @@ public bool IsInListenLater
}

[ObservableProperty]
Uri image;

[ObservableProperty]
private Show show;
private ShowViewModel show;

private readonly ListenLaterService listenLaterService;
private readonly ShowsService podcastService;
private readonly PlayerService playerService;

public EpisodeDetailViewModel(ListenLaterService listen, ShowsService shows, PlayerService player)
public EpisodeDetailViewModel(ListenLaterService listen, ShowsService shows, PlayerService player, SubscriptionsService subs, ImageProcessingService imageProcessing)
{
listenLaterService = listen;
podcastService = shows;
playerService = player;
subscriptionsService = subs;
imageProcessingService = imageProcessing;
}

internal async Task InitializeAsync()
Expand All @@ -55,7 +54,13 @@ internal async Task InitializeAsync()

async Task FetchAsync()
{
Show = await podcastService.GetShowByIdAsync(new Guid(ShowId));
var show = await podcastService.GetShowByIdAsync(new Guid(ShowId));

var showVM = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id), imageProcessingService);

Show = showVM;
Show.InitializeCommand.Execute(null);

var eId = new Guid(Id);
Episode = Show.Episodes.FirstOrDefault(e => e.Id == eId);

Expand All @@ -69,7 +74,6 @@ await Shell.Current.DisplayAlert(
return;
}

Image = Show.Image;
IsInListenLater = listenLaterService.IsInListenLater(Episode);
}

Expand All @@ -79,21 +83,21 @@ Task ListenLater()
if (listenLaterService.IsInListenLater(episode))
listenLaterService.Remove(episode);
else
listenLaterService.Add(episode, show);
listenLaterService.Add(episode, show.Show);

IsInListenLater = listenLaterService.IsInListenLater(episode);
Show.Episodes.FirstOrDefault(x => x.Id == episode.Id).IsInListenLater = IsInListenLater;
return Task.CompletedTask;
}

[RelayCommand]
Task Play() => playerService.PlayAsync(Episode, Show);
Task Play() => playerService.PlayAsync(Episode, Show.Show);

[RelayCommand]
Task Share() =>
Microsoft.Maui.ApplicationModel.DataTransfer.Share.RequestAsync(new ShareTextRequest
{
Text = $"{Config.BaseWeb}show/{show.Id}",
Text = $"{Config.BaseWeb}show/{show.Show.Id}",
Title = "Share the episode uri"
});
}
24 changes: 13 additions & 11 deletions src/Mobile/ViewModels/ShowDetailViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using Microsoft.NetConf2021.Maui.Resources.Strings;
using MvvmHelpers;
using MvvmHelpers.Interfaces;

namespace Microsoft.NetConf2021.Maui.ViewModels;

[QueryProperty(nameof(Id), nameof(Id))]
public partial class ShowDetailViewModel : ViewModelBase
{
public string Id { get; set; }
Guid showId;
private readonly PlayerService playerService;
private readonly SubscriptionsService subscriptionsService;
private readonly ListenLaterService listenLaterService;
private readonly ShowsService showsService;
private readonly ImageProcessingService imageProcessingService;

private Guid showId;

readonly PlayerService playerService;
readonly SubscriptionsService subscriptionsService;
readonly ListenLaterService listenLaterService;
readonly ShowsService showsService;
public string Id { get; set; }

[ObservableProperty]
ShowViewModel show;
Expand All @@ -24,20 +25,20 @@ public partial class ShowDetailViewModel : ViewModelBase
[ObservableProperty]
ObservableRangeCollection<Episode> episodes;


[ObservableProperty]
bool isPlaying;

[ObservableProperty]
string textToSearch;


public ShowDetailViewModel(ShowsService shows, PlayerService player, SubscriptionsService subs, ListenLaterService later)
public ShowDetailViewModel(ShowsService shows, PlayerService player, SubscriptionsService subs, ListenLaterService later, ImageProcessingService imageProcessing)
{
showsService = shows;
playerService = player;
subscriptionsService = subs;
listenLaterService = later;
imageProcessingService = imageProcessing;

episodes = new ObservableRangeCollection<Episode>();
}

Expand Down Expand Up @@ -65,9 +66,10 @@ await Shell.Current.DisplayAlert(
return;
}

var showVM = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id));
var showVM = new ShowViewModel(show, subscriptionsService.IsSubscribed(show.Id), imageProcessingService);

Show = showVM;
Show.InitializeCommand.Execute(null);
Episodes.ReplaceRange(show.Episodes.ToList());
}

Expand Down
Loading

0 comments on commit 28ede25

Please sign in to comment.