Skip to content

Commit 3efdec9

Browse files
committed
Add Video Background support
1 parent 78d031b commit 3efdec9

18 files changed

+870
-228
lines changed

BackgroundTest/BackgroundTest.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,9 @@
9696
<PackageReference Include="PhotoSauce.NativeCodecs.Libwebp" Version="*-*" />
9797
</ItemGroup>
9898

99+
<ItemGroup>
100+
<ProjectReference Include="..\Hi3Helper.Win32\Hi3Helper.Win32.csproj" />
101+
<ProjectReference Include="..\Hi3Helper.Win32\WinRT\Hi3Helper.Win32.WinRT.csproj" />
102+
</ItemGroup>
103+
99104
</Project>

BackgroundTest/CustomControl/LayeredBackgroundImage/LayeredBackgroundImage.Events.Loaders.cs

Lines changed: 227 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1-
using Microsoft.UI.Xaml;
1+
using Hi3Helper.Win32.Native.Enums.DXGI;
2+
using Hi3Helper.Win32.Native.Structs.DXGI;
3+
using Hi3Helper.Win32.WinRT.SwapChainPanelHelper;
4+
using Microsoft.Graphics.Canvas;
5+
using Microsoft.Graphics.Canvas.UI.Xaml;
6+
using Microsoft.Graphics.DirectX;
7+
using Microsoft.UI.Xaml;
28
using Microsoft.UI.Xaml.Controls;
39
using Microsoft.UI.Xaml.Data;
4-
using Microsoft.UI.Xaml.Media;
510
using Microsoft.UI.Xaml.Media.Animation;
611
using System;
712
using System.Collections.Generic;
813
using System.Diagnostics.CodeAnalysis;
914
using System.IO;
1015
using System.Linq;
1116
using System.Runtime.CompilerServices;
17+
using System.Runtime.InteropServices;
1218
using System.Threading;
1319
using System.Threading.Tasks;
20+
using Windows.Foundation;
21+
using Windows.Graphics.Capture;
22+
using Windows.Graphics.DirectX;
23+
using Windows.Media.Playback;
24+
using Windows.Storage.Streams;
25+
using WinRT.Interop;
1426

1527
// ReSharper disable StringLiteralTypo
1628
// ReSharper disable CommentTypo
@@ -79,6 +91,13 @@ private enum MediaSourceType
7991

8092
#endregion
8193

94+
#region Fields
95+
96+
private bool _isResizingVideoCanvas;
97+
private bool _isDrawingVideoFrame;
98+
99+
#endregion
100+
82101
#region Loaders
83102

84103
private static void PlaceholderSource_OnChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -150,6 +169,13 @@ private async void LoadFromSourceAsyncDetached(
150169
{
151170
try
152171
{
172+
if (IsInPreloadGrid())
173+
{
174+
return;
175+
}
176+
177+
DisposeVideoPlayer();
178+
153179
object? source = GetValue(sourceProperty);
154180
if (source is null)
155181
{
@@ -185,16 +211,13 @@ await LoadVideoFromSourceAsync(source,
185211
stretchProperty,
186212
horizontalAlignmentProperty,
187213
verticalAlignmentProperty,
188-
nameof(instance.IsAudioEnabled),
189-
nameof(instance.AudioVolume),
190214
instance,
191215
grid))
192216
{
193217
return;
194218
}
195219

196220
ClearGrid:
197-
DisposeMediaPlayerElements(grid);
198221
ClearMediaGrid(grid);
199222
}
200223
catch
@@ -203,27 +226,6 @@ await LoadVideoFromSourceAsync(source,
203226
}
204227
}
205228

206-
private static async ValueTask<bool> LoadVideoFromSourceAsync(
207-
object? source,
208-
string stretchProperty,
209-
string horizontalAlignmentProperty,
210-
string verticalAlignmentProperty,
211-
string isAudioEnabledProperty,
212-
string audioVolumeProperty,
213-
LayeredBackgroundImage instance,
214-
Grid grid)
215-
{
216-
try
217-
{
218-
return false;
219-
}
220-
catch (Exception e)
221-
{
222-
Console.WriteLine(e);
223-
return false;
224-
}
225-
}
226-
227229
private static async ValueTask<bool> LoadImageFromSourceAsync(
228230
object? source,
229231
string stretchProperty,
@@ -238,12 +240,13 @@ private static async ValueTask<bool> LoadImageFromSourceAsync(
238240
Image image = new();
239241

240242
// Bind property
241-
image.BindProperty(instance, stretchProperty, Image.StretchProperty, BindingMode.OneWay);
243+
image.BindProperty(instance, stretchProperty, Image.StretchProperty, BindingMode.OneWay);
242244
image.BindProperty(instance, horizontalAlignmentProperty, HorizontalAlignmentProperty, BindingMode.OneWay);
243-
image.BindProperty(instance, verticalAlignmentProperty, VerticalAlignmentProperty, BindingMode.OneWay);
245+
image.BindProperty(instance, verticalAlignmentProperty, VerticalAlignmentProperty, BindingMode.OneWay);
244246

245247
image.Transitions.Add(new ContentThemeTransition());
246248
grid.Children.Add(image);
249+
247250
image.Tag = (grid, instance);
248251
image.ImageOpened += Image_ImageOpened;
249252

@@ -276,6 +279,192 @@ private static async ValueTask<bool> LoadImageFromSourceAsync(
276279
}
277280
}
278281

282+
private static async Task<bool> LoadVideoFromSourceAsync(
283+
object? source,
284+
string stretchProperty,
285+
string horizontalAlignmentProperty,
286+
string verticalAlignmentProperty,
287+
LayeredBackgroundImage instance,
288+
Grid grid)
289+
{
290+
// Setting up video player
291+
instance.SetupVideoPlayer();
292+
MediaPlayer player = instance._videoPlayer;
293+
player.MediaOpened += InitializeVideoFrameOnMediaOpened;
294+
295+
if (instance.MediaCacheHandler is { } cacheHandler)
296+
{
297+
MediaCacheResult cacheResult = await cacheHandler.LoadCachedSource(source);
298+
source = cacheResult.CachedSource;
299+
}
300+
301+
Uri? sourceUri = source as Uri;
302+
303+
if (sourceUri == null &&
304+
source is string asStringSource)
305+
{
306+
sourceUri = asStringSource.GetStringAsUri();
307+
}
308+
309+
Stream? sourceStream = null;
310+
if (source is Stream { CanSeek: true, CanRead: true } asSeekableStream)
311+
{
312+
sourceStream = asSeekableStream;
313+
}
314+
315+
if (sourceStream == null &&
316+
sourceUri == null)
317+
{
318+
goto CleanupAndReturnFalse;
319+
}
320+
321+
// Assign media source
322+
if (sourceStream != null)
323+
{
324+
IRandomAccessStream? sourceStreamRandom = sourceStream.AsRandomAccessStream();
325+
player.SetStreamSource(sourceStreamRandom);
326+
}
327+
else if (sourceUri != null)
328+
{
329+
player.SetUriSource(sourceUri);
330+
}
331+
else
332+
{
333+
goto CleanupAndReturnFalse;
334+
}
335+
336+
return true;
337+
338+
CleanupAndReturnFalse:
339+
player.MediaOpened -= InitializeVideoFrameOnMediaOpened;
340+
return false;
341+
342+
void InitializeVideoFrameOnMediaOpened(MediaPlayer sender, object args)
343+
{
344+
instance.DispatcherQueue.TryEnqueue(() =>
345+
{
346+
// Create instance
347+
Image image = new()
348+
{
349+
Tag = (grid, instance)
350+
};
351+
352+
// Bind property
353+
image.BindProperty(instance,
354+
stretchProperty,
355+
Image.StretchProperty,
356+
BindingMode.OneWay);
357+
image.BindProperty(instance,
358+
horizontalAlignmentProperty,
359+
HorizontalAlignmentProperty,
360+
BindingMode.OneWay);
361+
image.BindProperty(instance,
362+
verticalAlignmentProperty,
363+
VerticalAlignmentProperty,
364+
BindingMode.OneWay);
365+
366+
instance.InitializeCanvasBitmapSource(image, sender.PlaybackSession);
367+
368+
// Register events
369+
image.Loaded += Image_VideoFrameOnLoaded;
370+
image.Unloaded += Image_VideoFrameOnUnloaded;
371+
372+
// Add to children
373+
image.Transitions.Add(new ContentThemeTransition());
374+
grid.Children.Add(image);
375+
instance._videoPlayer.Play();
376+
});
377+
}
378+
}
379+
380+
private unsafe void VideoPlayer_VideoFrameAvailable(MediaPlayer sender, object args)
381+
{
382+
if (_canvasImageSource == null)
383+
{
384+
return;
385+
}
386+
387+
// Use Interlock to ensure thre session is only created one time. But with one downside that
388+
// the first frame will always be skipped due to atomic operation.
389+
// Note:
390+
// _currentCanvasDrawingSession is ensured to expect null first, then create and exit.
391+
// The next frame will be drawn.
392+
CanvasDrawingSession? session = null;
393+
if ((session = Interlocked.Exchange(ref _currentCanvasDrawingSession, null)) == null)
394+
{
395+
_currentCanvasDrawingSession = _canvasImageSource.CreateDrawingSession(default, _currentCanvasRenderSize);
396+
return;
397+
}
398+
399+
try
400+
{
401+
#if USENATIVESWAPCHAIN
402+
DXGI_PRESENT_PARAMETERS parameter = default;
403+
404+
var swapChain = _swapChainContext.DXGISwapChain;
405+
var surface = _swapChainContext.Direct3DSurface;
406+
407+
Rect renderArea = new(0, 0, _canvasWidth, _canvasHeight);
408+
// CanvasDrawingSession? canvasSession = _canvasImageSource.CreateDrawingSession(default);
409+
sender.CopyFrameToVideoSurface(surface, renderArea);
410+
swapChain?.Present1(1, 0, in parameter);
411+
#else
412+
sender.CopyFrameToVideoSurface(_canvasRenderTarget);
413+
session.DrawImage(_canvasRenderTarget);
414+
415+
if (DispatcherQueue.HasThreadAccess)
416+
{
417+
FinalizeFrame();
418+
}
419+
else
420+
{
421+
DispatcherQueue.TryEnqueue(FinalizeFrame);
422+
}
423+
424+
void FinalizeFrame()
425+
{
426+
try
427+
{
428+
session.Dispose();
429+
}
430+
catch { }
431+
finally
432+
{
433+
}
434+
}
435+
#endif
436+
}
437+
catch (Exception ex)
438+
{
439+
Console.WriteLine(ex);
440+
}
441+
finally
442+
{
443+
Interlocked.Exchange(ref _isDrawingVideoFrame, false);
444+
}
445+
}
446+
447+
private static void Image_VideoFrameOnLoaded(object sender, RoutedEventArgs e)
448+
{
449+
if (sender is not Image { Tag: ValueTuple<Grid, LayeredBackgroundImage> parentGrid })
450+
{
451+
return;
452+
}
453+
454+
parentGrid.Item2.Play();
455+
Image_ImageOpened(sender, e);
456+
}
457+
458+
private static void Image_VideoFrameOnUnloaded(object sender, RoutedEventArgs e)
459+
{
460+
if (sender is not Image { Tag: ValueTuple<Grid, LayeredBackgroundImage> parentGrid })
461+
{
462+
return;
463+
}
464+
465+
parentGrid.Item2.Pause();
466+
}
467+
279468
private static void Image_ImageOpened(object sender, RoutedEventArgs e)
280469
{
281470
if (sender is not Image { Tag: ValueTuple<Grid, LayeredBackgroundImage> parentGrid } image)
@@ -293,7 +482,6 @@ private static void Image_ImageOpened(object sender, RoutedEventArgs e)
293482

294483
// HACK: Tells the Grid to temporarily detach all UIElement children
295484
// then re-add the image to the grid
296-
DisposeMediaPlayerElements(parentGrid.Item1);
297485
ClearMediaGrid(parentGrid.Item1, image);
298486

299487
// Remove transition once loaded
@@ -339,14 +527,19 @@ private static bool TryGetMediaPathFromSource(object? source, [NotNullWhen(true)
339527
private static void ClearMediaGrid(Grid grid, UIElement? except = null)
340528
{
341529
List<UIElement> elementExcepted =
342-
grid.Children
343-
.Where(x => x != except)
344-
.ToList();
530+
except == null ? grid.Children.ToList() :
531+
grid.Children.Where(x => x != except)
532+
.ToList();
345533

346534
foreach (Image image in elementExcepted.OfType<Image>())
347535
{
536+
// This one is for Image. The source will always guarantee to call this event.
348537
image.ImageOpened -= Image_ImageOpened;
349-
image.Source = null; // Clears the loaded ImageSource
538+
// This one is for Video since ImageOpened with Canvas source will never triggers this so we use Loaded instead.
539+
image.Loaded -= Image_VideoFrameOnLoaded;
540+
image.Unloaded -= Image_VideoFrameOnUnloaded;
541+
// Clears the loaded ImageSource
542+
image.Source = null;
350543
}
351544

352545
foreach (UIElement element in elementExcepted)
@@ -355,19 +548,5 @@ private static void ClearMediaGrid(Grid grid, UIElement? except = null)
355548
}
356549
}
357550

358-
private static void DisposeMediaPlayerElements(Grid grid)
359-
{
360-
foreach (UIElement element in grid.Children)
361-
{
362-
if (element is not MediaPlayerElement asMediaElement)
363-
{
364-
continue;
365-
}
366-
367-
asMediaElement.MediaPlayer.Pause();
368-
asMediaElement.MediaPlayer.Dispose();
369-
}
370-
}
371-
372-
#endregion
551+
#endregion
373552
}

0 commit comments

Comments
 (0)