Skip to content

Conversation

@ThanhNguyxn
Copy link

@ThanhNguyxn ThanhNguyxn commented Dec 5, 2025

Summary

Implements media playback preview in File Explorer's preview panel, addressing #44094.

This enables users to preview audio and video files directly in File Explorer's preview pane with full playback controls.

Changes

New Projects

  • MediaPreviewHandlerCpp (C++ DLL) - COM IPreviewHandler implementation
  • MediaPreviewHandler (.NET EXE) - WebView2-based HTML5 media player with dark theme

Supported Formats

Type Extensions
Video .mp4, .avi, .mkv, .mov, .webm, .wmv, .m4v, .3gp, .3g2
Audio .mp3, .wav, .flac, .m4a, .aac, .ogg, .wma

Modified Files

  • shared_constants.h - Added MEDIA_PREVIEW_RESIZE_EVENT
  • Constants.h/cpp/idl - Added MediaPreviewResizeEvent()
  • logger_settings.h - Added media preview logger
  • modulesRegistry.h - Added file extension registration
  • PowerToys.slnx - Added projects to solution

TODO (Can be done in follow-up PRs)

1. Settings UI Toggle

Add enable/disable toggle in PowerToys Settings for Media Preview Handler.

Files to modify:

  • src/modules/previewpane/powerpreview/powerpreview.cpp - Add settings handler
  • src/settings-ui/ - Add UI toggle similar to other preview handlers

2. Installer Integration

Add files to WiX installer manifest.

Files to modify:

  • installer/PowerToysSetup/Resources.wxs - Add MediaPreviewHandler files

3. Additional Format Support

Consider adding more formats:

  • Video: .flv, .ts, .m2ts, .vob
  • Audio: .aiff, .opus, .ape

4. Codec Handling

Add detection for missing codecs (e.g., HEVC/H.265) similar to Peek's GetMissingCodecAsync() in VideoPreviewer.cs.

5. Thumbnail Generation

Optionally add a MediaThumbnailProvider for generating video thumbnails (similar to SvgThumbnailProvider).


Checklist

  • Code follows existing patterns (SvgPreviewHandler)
  • Added to solution file
  • Builds and runs locally
  • Tests added
  • Documentation updated

Fixes #44094

@ThanhNguyxn ThanhNguyxn marked this pull request as ready for review December 5, 2025 10:35
@ThanhNguyxn ThanhNguyxn requested a review from a team as a code owner December 5, 2025 10:35
Copilot AI review requested due to automatic review settings December 5, 2025 10:35
@github-actions

This comment has been minimized.

Copilot finished reviewing on behalf of ThanhNguyxn December 5, 2025 10:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a new Media Preview Handler for PowerToys File Explorer, enabling direct preview of audio and video files in the File Explorer preview pane. The implementation follows the established PowerToys preview handler architecture with a C++ COM DLL component that interfaces with File Explorer and a .NET WinForms application using WebView2 to render HTML5 media players.

Key Changes:

  • Adds MediaPreviewHandlerCpp (C++ DLL) implementing IPreviewHandler COM interface for File Explorer integration
  • Adds MediaPreviewHandler (.NET EXE) providing WebView2-based HTML5 player with dark theme support for audio/video playback
  • Registers video formats (.mp4, .avi, .mkv, .mov, .webm, .wmv, .m4v, .3gp, .3g2) and audio formats (.mp3, .wav, .flac, .m4a, .aac, .ogg, .wma) in the Windows registry

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/modules/previewpane/MediaPreviewHandlerCpp/dllmain.cpp COM DLL entry point with class factory registration for CLSID {D3A86E9B-5F4C-4A8D-9E76-2B1F8C7E3A4D}
src/modules/previewpane/MediaPreviewHandlerCpp/MediaPreviewHandler.cpp IPreviewHandler implementation launching .NET EXE via ShellExecuteEx
src/modules/previewpane/MediaPreviewHandlerCpp/ClassFactory.cpp COM class factory for MediaPreviewHandler instantiation
src/modules/previewpane/MediaPreviewHandler/Program.cs Entry point parsing command-line args (file path, HWND, rect) and initializing preview control
src/modules/previewpane/MediaPreviewHandler/MediaPreviewControl.cs WebView2 control generating HTML5 video/audio players with dark theme styling
src/common/interop/shared_constants.h Adds MEDIA_PREVIEW_RESIZE_EVENT constant for IPC between C++ and .NET processes
src/common/interop/Constants.idl/h/cpp Exposes MediaPreviewResizeEvent() method to .NET via WinRT projection
src/common/logger/logger_settings.h Adds MediaPrevHandler logger configuration
src/common/utils/modulesRegistry.h Registers media file extensions with preview handler CLSID in Windows registry
PowerToys.slnx Adds MediaPreviewHandler and MediaPreviewHandlerCpp projects to solution

<source src=""{fileUrl}"" type=""{mimeType}"">
Your browser does not support the audio tag.
</audio>
<div class=""file-name"">{Path.GetFileName(filePath)}</div>
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential XSS vulnerability: The file name is inserted directly into the HTML without HTML encoding on line 162. If a malicious file name contains HTML/JavaScript (e.g., <script>alert('xss')</script>.mp3), it could be executed in the WebView2 context. Consider HTML-encoding the file name:

<div class=""file-name"">{System.Web.HttpUtility.HtmlEncode(Path.GetFileName(filePath))}</div>

Or use System.Net.WebUtility.HtmlEncode() if System.Web is not available.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +115
// Disable external navigation
_webView2Control.CoreWebView2.Settings.IsScriptEnabled = true;
_webView2Control.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
_webView2Control.CoreWebView2.Settings.AreDevToolsEnabled = false;

// Generate and navigate to HTML content
var htmlContent = GenerateMediaHtml(filePath);
_webView2Control.CoreWebView2.NavigateToString(htmlContent);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing navigation protection: While script is enabled for the player controls, there's no handler to prevent navigation away from the media content. If the HTML or media file contains links or redirects, users could potentially navigate to external content. Consider adding a NavigationStarting event handler to block navigation:

_webView2Control.CoreWebView2.NavigationStarting += (s, e) =>
{
    if (!e.IsUserInitiated || e.Uri != "about:blank")
    {
        e.Cancel = true;
    }
};

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +43
string filePath = args[0];
IntPtr hwnd = IntPtr.Parse(args[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture);

int left = Convert.ToInt32(args[2], 10);
int right = Convert.ToInt32(args[3], 10);
int top = Convert.ToInt32(args[4], 10);
int bottom = Convert.ToInt32(args[5], 10);
Rectangle s = new Rectangle(left, top, right - left, bottom - top);

_previewHandlerControl = new MediaPreviewControl();

Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for argument parsing. If any of the arguments contain invalid values, IntPtr.Parse() or Convert.ToInt32() will throw exceptions that aren't caught. Consider wrapping the argument parsing in a try-catch block to handle format exceptions gracefully:

try
{
    string filePath = args[0];
    IntPtr hwnd = IntPtr.Parse(args[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
    // ... rest of parsing
}
catch (FormatException ex)
{
    MessageBox.Show($"Invalid argument format: {ex.Message}");
    return;
}
Suggested change
string filePath = args[0];
IntPtr hwnd = IntPtr.Parse(args[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int left = Convert.ToInt32(args[2], 10);
int right = Convert.ToInt32(args[3], 10);
int top = Convert.ToInt32(args[4], 10);
int bottom = Convert.ToInt32(args[5], 10);
Rectangle s = new Rectangle(left, top, right - left, bottom - top);
_previewHandlerControl = new MediaPreviewControl();
try
{
string filePath = args[0];
IntPtr hwnd = IntPtr.Parse(args[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture);
int left = Convert.ToInt32(args[2], 10);
int right = Convert.ToInt32(args[3], 10);
int top = Convert.ToInt32(args[4], 10);
int bottom = Convert.ToInt32(args[5], 10);
Rectangle s = new Rectangle(left, top, right - left, bottom - top);
_previewHandlerControl = new MediaPreviewControl();
if (!_previewHandlerControl.SetWindow(hwnd, s))
{
return;
}
_previewHandlerControl.DoPreview(filePath);
NativeEventWaiter.WaitForEventLoop(
Constants.MediaPreviewResizeEvent(),
() =>
{
Rectangle s = default;
if (!_previewHandlerControl.SetRect(s))
{
etwTrace?.Dispose();
// When the parent HWND became invalid, the application won't respond to Application.Exit().
Environment.Exit(0);
}
},
Dispatcher.CurrentDispatcher,
_tokenSource.Token);
etwTrace?.Dispose();
}
catch (FormatException ex)
{
MessageBox.Show($"Invalid argument format: {ex.Message}");
return;
}
catch (OverflowException ex)
{
MessageBox.Show($"Argument value out of range: {ex.Message}");
return;
}
catch (ArgumentNullException ex)
{
MessageBox.Show($"Missing argument: {ex.Message}");
return;
}

Copilot uses AI. Check for mistakes.
{
if (args.Length == 6)
{
ETWTrace etwTrace = new ETWTrace(Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw"));
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path construction issue: Using Environment.GetEnvironmentVariable("USERPROFILE") with string concatenation is error-prone. If the environment variable is not set, this will result in a null reference. Use Path.Combine() with Environment.GetFolderPath() instead:

ETWTrace etwTrace = new ETWTrace(Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    "AppData", "LocalLow", "Microsoft", "PowerToys", "etw"));

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +41
private static readonly HashSet<string> VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts",
".m4v", ".mkv", ".mov", ".mp4v", ".mts", ".wm", ".wmv", ".webm",
};

/// <summary>
/// Supported audio file extensions.
/// </summary>
private static readonly HashSet<string> AudioExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".aac", ".ac3", ".amr", ".flac", ".m4a", ".mp3", ".ogg", ".wav", ".wma",
};
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File extension mismatch: The C# code supports more video/audio extensions (.3gp2, .3gpp, .asf, .m2t, .m2ts, .mp4v, .mts, .wm, .ac3, .amr) than are registered in modulesRegistry.h (lines 23-24). Users will see these files as previewable when they register in File Explorer, but the registry won't have handlers for them. Either add the missing extensions to modulesRegistry.h or remove unsupported extensions from the C# code to ensure consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +158
? $@"<video id=""player"" controls autoplay style=""max-width: 100%; max-height: 100%; object-fit: contain;"">
<source src=""{fileUrl}"" type=""{mimeType}"">
Your browser does not support the video tag.
</video>"
: $@"<div class=""audio-container"">
<div class=""audio-icon"">🎵</div>
<audio id=""player"" controls autoplay style=""width: 100%;"">
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The autoplay attribute is used for both video and audio elements (lines 152, 158). Auto-playing media in a preview pane could be surprising or unwanted behavior for users browsing files. Consider removing autoplay or making it configurable through settings, especially for audio files which might play unexpectedly when users are just browsing through their files in File Explorer.

Suggested change
? $@"<video id=""player"" controls autoplay style=""max-width: 100%; max-height: 100%; object-fit: contain;"">
<source src=""{fileUrl}"" type=""{mimeType}"">
Your browser does not support the video tag.
</video>"
: $@"<div class=""audio-container"">
<div class=""audio-icon"">🎵</div>
<audio id=""player"" controls autoplay style=""width: 100%;"">
? $@"<video id=""player"" controls style=""max-width: 100%; max-height: 100%; object-fit: contain;"">
<source src=""{fileUrl}"" type=""{mimeType}"">
Your browser does not support the video tag.
</video>"
: $@"<div class=""audio-container"">
<div class=""audio-icon"">🎵</div>
<audio id=""player"" controls style=""width: 100%;"">

Copilot uses AI. Check for mistakes.
}

MediaPreviewHandler::~MediaPreviewHandler()
{
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The destructor does not clean up the event handle created in the constructor. The m_resizeEvent handle created with CreateEvent() should be closed with CloseHandle(m_resizeEvent) to avoid a handle leak. Consider adding:

if (m_resizeEvent)
{
    CloseHandle(m_resizeEvent);
    m_resizeEvent = NULL;
}
Suggested change
{
{
if (m_resizeEvent)
{
CloseHandle(m_resizeEvent);
m_resizeEvent = NULL;
}

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +199
ShellExecuteEx(&sei);

// Prevent to leak processes: preview is called multiple times when minimizing and restoring Explorer window
if (m_process)
{
TerminateProcess(m_process, 0);
}

m_process = sei.hProcess;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for ShellExecuteEx(). If the function fails, sei.hProcess will be NULL, but this isn't checked before assigning to m_process. This could lead to attempting to terminate a NULL handle later. Consider checking the return value:

if (!ShellExecuteEx(&sei))
{
    Logger::error(L"Failed to start MediaPreviewHandler.exe. Error code: {}", GetLastError());
    return E_FAIL;
}
if (sei.hProcess == NULL)
{
    Logger::error(L"ShellExecuteEx succeeded but hProcess is NULL");
    return E_FAIL;
}

Copilot uses AI. Check for mistakes.
// Prevent to leak processes: preview is called multiple times when minimizing and restoring Explorer window
if (m_process)
{
TerminateProcess(m_process, 0);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process handle leak: Before calling TerminateProcess(), the old process handle in m_process should be closed with CloseHandle() to avoid leaking handles. Consider:

if (m_process)
{
    TerminateProcess(m_process, 0);
    CloseHandle(m_process);
}
m_process = sei.hProcess;
Suggested change
TerminateProcess(m_process, 0);
TerminateProcess(m_process, 0);
CloseHandle(m_process);

Copilot uses AI. Check for mistakes.

IFACEMETHODIMP MediaPreviewHandler::Unload()
{
TerminateProcess(m_process, 0);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing process handle cleanup and null check. Before calling TerminateProcess(), check if m_process is not NULL, and after termination, close the handle:

if (m_process)
{
    TerminateProcess(m_process, 0);
    CloseHandle(m_process);
    m_process = NULL;
}
Suggested change
TerminateProcess(m_process, 0);
if (m_process)
{
TerminateProcess(m_process, 0);
CloseHandle(m_process);
m_process = NULL;
}

Copilot uses AI. Check for mistakes.
)

- Add MediaPreviewHandlerCpp DLL (COM IPreviewHandler)
- Add MediaPreviewHandler .NET EXE (WebView2 HTML5 player)
- Support video: .mp4, .avi, .mkv, .mov, .webm, .wmv, .m4v, .3gp
- Support audio: .mp3, .wav, .flac, .m4a, .aac, .ogg, .wma
- Add MediaPreviewResizeEvent constant
- Add file extension registration in modulesRegistry.h
@github-actions

This comment has been minimized.

@ThanhNguyxn ThanhNguyxn force-pushed the feature/media-preview-handler branch from 6f9f49e to eee1a6a Compare December 5, 2025 11:07
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Media playback in Preview panel

1 participant