前言
上一篇文章 MAUI Blazor 显示本地图片的新思路 中, 提出了通过webview拦截,从而在前端中显示本地图片的思路。不过当时还不完善,随后也发现了很多问题。比如,
- 不同平台上的url不统一。这对于需要存储图片路径并且多端互通的需求来说,并不友好。至少
FileSystem.AppDataDirectory和FileSystem.CacheDirectory下的文件生成的url应该统一。 - 音频文件和视频文件无法使用。理论上可以用于各种文件,但是音频和视频不能播放,应该是需要相应的处理
- Windows上有限制。大于9~10M的图片不显示
- iOS/ Mac有跨域问题。尤其是调用用于截图的js库,图片会由于跨域不出现在截图中
所以,在这篇文章中,对这个思路进行完善,使之成为一个可行的方案。
例如 <img src='appdata/Image/image1.jpg' > 会显示 FileSystem.AppDataDirectory 文件夹下的 Image 文件夹下的 image1.jpg 这个图片
<video src='cache/Video/video1.mp4' controls > 会播放 FileSystem.CacheDirectory 文件夹下的 Video 文件夹下的 video1.mp4 这个视频
对于其他路径的文件来说,url设为 file/ 加上转义后的完整路径
正文
准备工作
新建一个MAUI Blazor项目
参考 配置基于文件名的多目标 ,更改项目文件(以.csproj结尾的文件),添加以下代码
<!-- Android --> <ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true"> <Compile Remove="*****.Android.cs" /> <None Include="*****.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <!-- Both iOS and Mac Catalyst --> <ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true"> <Compile Remove="*****.MaciOS.cs" /> <None Include="*****.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <!-- iOS --> <ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true"> <Compile Remove="*****.iOS.cs" /> <None Include="*****.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <!-- Mac Catalyst --> <ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-maccatalyst')) != true"> <Compile Remove="*****.MacCatalyst.cs" /> <None Include="*****.MacCatalyst.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup> <!-- Windows --> <ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true"> <Compile Remove="***.Windows.cs" /> <None Include="***.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> </ItemGroup>
添加一个处理ContentType的静态类
用来获取文件的ContentType
没找到什么太好的方法,偶然看到Maui的源码中的一段,还不错,不过是internal修饰的,就直接抄来了
新建Utilities/MimeType文件夹,在里面添加 StaticContentProvider.cs
代码如下:
#nullable disable // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.CodeAnalysis; namespace MauiBlazorLocalMediaFile.Utilities { internal partial class StaticContentProvider { private static readonly FileExtensionContentTypeProvider ContentTypeProvider = new(); internal static string GetResponseContentTypeOrDefault(string path) => ContentTypeProvider.TryGetContentType(path, out var matchedContentType) ? matchedContentType : "application/octet-stream"; internal static IDictionary<string, string> GetResponseHeaders(string contentType) => new Dictionary<string, string>(StringComparer.Ordinal) { { "Content-Type", contentType }, { "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" }, }; internal class FileExtensionContentTypeProvider { // Notes: // - This table was initially copied from IIS and has many legacy entries we will maintain for backwards compatibility. // - We only plan to add new entries where we expect them to be applicable to a majority of developers such as being // used in the project templates. #region Extension mapping table /// <summary> /// Creates a new provider with a set of default mappings. /// </summary> public FileExtensionContentTypeProvider() : this(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { ".323", "text/h323" }, { ".3g2", "video/3gpp2" }, { ".3gp2", "video/3gpp2" }, { ".3gp", "video/3gpp" }, { ".3gpp", "video/3gpp" }, { ".aac", "audio/aac" }, { ".aaf", "application/octet-stream" }, { ".aca", "application/octet-stream" }, { ".accdb", "application/msaccess" }, { ".accde", "application/msaccess" }, { ".accdt", "application/msaccess" }, { ".acx", "application/internet-property-stream" }, { ".adt", "audio/vnd.dlna.adts" }, { ".adts", "audio/vnd.dlna.adts" }, { ".afm", "application/octet-stream" }, { ".ai", "application/postscript" }, { ".aif", "audio/x-aiff" }, { ".aifc", "audio/aiff" }, { ".aiff", "audio/aiff" }, { ".appcache", "text/cache-manifest" }, { ".application", "application/x-ms-application" }, { ".art", "image/x-jg" }, { ".asd", "application/octet-stream" }, { ".asf", "video/x-ms-asf" }, { ".asi", "application/octet-stream" }, { ".asm", "text/plain" }, { ".asr", "video/x-ms-asf" }, { ".asx", "video/x-ms-asf" }, { ".atom", "application/atom+xml" }, { ".au", "audio/basic" }, { ".avi", "video/x-msvideo" }, { ".axs", "application/olescript" }, { ".bas", "text/plain" }, { ".bcpio", "application/x-bcpio" }, { ".bin", "application/octet-stream" }, { ".bmp", "image/bmp" }, { ".c", "text/plain" }, { ".cab", "application/vnd.ms-cab-compressed" }, { ".calx", "application/vnd.ms-office.calx" }, { ".cat", "application/vnd.ms-pki.seccat" }, { ".cdf", "application/x-cdf" }, { ".chm", "application/octet-stream" }, { ".class", "application/x-java-applet" }, { ".clp", "application/x-msclip" }, { ".cmx", "image/x-cmx" }, { ".cnf", "text/plain" }, { ".cod", "image/cis-cod" }, { ".cpio", "application/x-cpio" }, { ".cpp", "text/plain" }, { ".crd", "application/x-mscardfile" }, { ".crl", "application/pkix-crl" }, { ".crt", "application/x-x509-ca-cert" }, { ".csh", "application/x-csh" }, { ".css", "text/css" }, { ".csv", "text/csv" }, // https://tools.ietf.org/html/rfc7111#section-5.1 { ".cur", "application/octet-stream" }, { ".dcr", "application/x-director" }, { ".deploy", "application/octet-stream" }, { ".der", "application/x-x509-ca-cert" }, { ".dib", "image/bmp" }, { ".dir", "application/x-director" }, { ".disco", "text/xml" }, { ".dlm", "text/dlm" }, { ".doc", "application/msword" }, { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, { ".dot", "application/msword" }, { ".dotm", "application/vnd.ms-word.template.macroEnabled.12" }, { ".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, { ".dsp", "application/octet-stream" }, { ".dtd", "text/xml" }, { ".dvi", "application/x-dvi" }, { ".dvr-ms", "video/x-ms-dvr" }, { ".dwf", "drawing/x-dwf" }, { ".dwp", "application/octet-stream" }, { ".dxr", "application/x-director" }, { ".eml", "message/rfc822" }, { ".emz", "application/octet-stream" }, { ".eot", "application/vnd.ms-fontobject" }, { ".eps", "application/postscript" }, { ".etx", "text/x-setext" }, { ".evy", "application/envoy" }, { ".exe", "application/vnd.microsoft.portable-executable" }, // https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable { ".fdf", "application/vnd.fdf" }, { ".fif", "application/fractals" }, { ".fla", "application/octet-stream" }, { ".flr", "x-world/x-vrml" }, { ".flv", "video/x-flv" }, { ".gif", "image/gif" }, { ".gtar", "application/x-gtar" }, { ".gz", "application/x-gzip" }, { ".h", "text/plain" }, { ".hdf", "application/x-hdf" }, { ".hdml", "text/x-hdml" }, { ".hhc", "application/x-oleobject" }, { ".hhk", "application/octet-stream" }, { ".hhp", "application/octet-stream" }, { ".hlp", "application/winhlp" }, { ".hqx", "application/mac-binhex40" }, { ".hta", "application/hta" }, { ".htc", "text/x-component" }, { ".htm", "text/html" }, { ".html", "text/html" }, { ".htt", "text/webviewhtml" }, { ".hxt", "text/html" }, { ".ical", "text/calendar" }, { ".icalendar", "text/calendar" }, { ".ico", "image/x-icon" }, { ".ics", "text/calendar" }, { ".ief", "image/ief" }, { ".ifb", "text/calendar" }, { ".iii", "application/x-iphone" }, { ".inf", "application/octet-stream" }, { ".ins", "application/x-internet-signup" }, { ".isp", "application/x-internet-signup" }, { ".IVF", "video/x-ivf" }, { ".jar", "application/java-archive" }, { ".java", "application/octet-stream" }, { ".jck", "application/liquidmotion" }, { ".jcz", "application/liquidmotion" }, { ".jfif", "image/pjpeg" }, { ".jpb", "application/octet-stream" }, { ".jpe", "image/jpeg" }, { ".jpeg", "image/jpeg" }, { ".jpg", "image/jpeg" }, { ".js", "application/javascript" }, { ".json", "application/json" }, { ".jsx", "text/jscript" }, { ".latex", "application/x-latex" }, { ".lit", "application/x-ms-reader" }, { ".lpk", "application/octet-stream" }, { ".lsf", "video/x-la-asf" }, { ".lsx", "video/x-la-asf" }, { ".lzh", "application/octet-stream" }, { ".m13", "application/x-msmediaview" }, { ".m14", "application/x-msmediaview" }, { ".m1v", "video/mpeg" }, { ".m2ts", "video/vnd.dlna.mpeg-tts" }, { ".m3u", "audio/x-mpegurl" }, { ".m4a", "audio/mp4" }, { ".m4v", "video/mp4" }, { ".man", "application/x-troff-man" }, { ".manifest", "application/x-ms-manifest" }, { ".map", "text/plain" }, { ".markdown", "text/markdown" }, { ".md", "text/markdown" }, { ".mdb", "application/x-msaccess" }, { ".mdp", "application/octet-stream" }, { ".me", "application/x-troff-me" }, { ".mht", "message/rfc822" }, { ".mhtml", "message/rfc822" }, { ".mid", "audio/mid" }, { ".midi", "audio/mid" }, { ".mix", "application/octet-stream" }, { ".mmf", "application/x-smaf" }, { ".mno", "text/xml" }, { ".mny", "application/x-msmoney" }, { ".mov", "video/quicktime" }, { ".movie", "video/x-sgi-movie" }, { ".mp2", "video/mpeg" }, { ".mp3", "audio/mpeg" }, { ".mp4", "video/mp4" }, { ".mp4v", "video/mp4" }, { ".mpa", "video/mpeg" }, { ".mpe", "video/mpeg" }, { ".mpeg", "video/mpeg" }, { ".mpg", "video/mpeg" }, { ".mpp", "application/vnd.ms-project" }, { ".mpv2", "video/mpeg" }, { ".ms", "application/x-troff-ms" }, { ".msi", "application/octet-stream" }, { ".mso", "application/octet-stream" }, { ".mvb", "application/x-msmediaview" }, { ".mvc", "application/x-miva-compiled" }, { ".nc", "application/x-netcdf" }, { ".nsc", "video/x-ms-asf" }, { ".nws", "message/rfc822" }, { ".ocx", "application/octet-stream" }, { ".oda", "application/oda" }, { ".odc", "text/x-ms-odc" }, { ".ods", "application/oleobject" }, { ".oga", "audio/ogg" }, { ".ogg", "video/ogg" }, { ".ogv", "video/ogg" }, { ".ogx", "application/ogg" }, { ".one", "application/onenote" }, { ".onea", "application/onenote" }, { ".onetoc", "application/onenote" }, { ".onetoc2", "application/onenote" }, { ".onetmp", "application/onenote" }, { ".onepkg", "application/onenote" }, { ".osdx", "application/opensearchdescription+xml" }, { ".otf", "font/otf" }, { ".p10", "application/pkcs10" }, { ".p12", "application/x-pkcs12" }, { ".p7b", "application/x-pkcs7-certificates" }, { ".p7c", "application/pkcs7-mime" }, { ".p7m", "application/pkcs7-mime" }, { ".p7r", "application/x-pkcs7-certreqresp" }, { ".p7s", "application/pkcs7-signature" }, { ".pbm", "image/x-portable-bitmap" }, { ".pcx", "application/octet-stream" }, { ".pcz", "application/octet-stream" }, { ".pdf", "application/pdf" }, { ".pfb", "application/octet-stream" }, { ".pfm", "application/octet-stream" }, { ".pfx", "application/x-pkcs12" }, { ".pgm", "image/x-portable-graymap" }, { ".pko", "application/vnd.ms-pki.pko" }, { ".pma", "application/x-perfmon" }, { ".pmc", "application/x-perfmon" }, { ".pml", "application/x-perfmon" }, { ".pmr", "application/x-perfmon" }, { ".pmw", "application/x-perfmon" }, { ".png", "image/png" }, { ".pnm", "image/x-portable-anymap" }, { ".pnz", "image/png" }, { ".pot", "application/vnd.ms-powerpoint" }, { ".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, { ".potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, { ".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, { ".ppm", "image/x-portable-pixmap" }, { ".pps", "application/vnd.ms-powerpoint" }, { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, { ".ppt", "application/vnd.ms-powerpoint" }, { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, { ".prf", "application/pics-rules" }, { ".prm", "application/octet-stream" }, { ".prx", "application/octet-stream" }, { ".ps", "application/postscript" }, { ".psd", "application/octet-stream" }, { ".psm", "application/octet-stream" }, { ".psp", "application/octet-stream" }, { ".pub", "application/x-mspublisher" }, { ".qt", "video/quicktime" }, { ".qtl", "application/x-quicktimeplayer" }, { ".qxd", "application/octet-stream" }, { ".ra", "audio/x-pn-realaudio" }, { ".ram", "audio/x-pn-realaudio" }, { ".rar", "application/octet-stream" }, { ".ras", "image/x-cmu-raster" }, { ".rf", "image/vnd.rn-realflash" }, { ".rgb", "image/x-rgb" }, { ".rm", "application/vnd.rn-realmedia" }, { ".rmi", "audio/mid" }, { ".roff", "application/x-troff" }, { ".rpm", "audio/x-pn-realaudio-plugin" }, { ".rtf", "application/rtf" }, { ".rtx", "text/richtext" }, { ".scd", "application/x-msschedule" }, { ".sct", "text/scriptlet" }, { ".sea", "application/octet-stream" }, { ".setpay", "application/set-payment-initiation" }, { ".setreg", "application/set-registration-initiation" }, { ".sgml", "text/sgml" }, { ".sh", "application/x-sh" }, { ".shar", "application/x-shar" }, { ".sit", "application/x-stuffit" }, { ".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12" }, { ".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide" }, { ".smd", "audio/x-smd" }, { ".smi", "application/octet-stream" }, { ".smx", "audio/x-smd" }, { ".smz", "audio/x-smd" }, { ".snd", "audio/basic" }, { ".snp", "application/octet-stream" }, { ".spc", "application/x-pkcs7-certificates" }, { ".spl", "application/futuresplash" }, { ".spx", "audio/ogg" }, { ".src", "application/x-wais-source" }, { ".ssm", "application/streamingmedia" }, { ".sst", "application/vnd.ms-pki.certstore" }, { ".stl", "application/vnd.ms-pki.stl" }, { ".sv4cpio", "application/x-sv4cpio" }, { ".sv4crc", "application/x-sv4crc" }, { ".svg", "image/svg+xml" }, { ".svgz", "image/svg+xml" }, { ".swf", "application/x-shockwave-flash" }, { ".t", "application/x-troff" }, { ".tar", "application/x-tar" }, { ".tcl", "application/x-tcl" }, { ".tex", "application/x-tex" }, { ".texi", "application/x-texinfo" }, { ".texinfo", "application/x-texinfo" }, { ".tgz", "application/x-compressed" }, { ".thmx", "application/vnd.ms-officetheme" }, { ".thn", "application/octet-stream" }, { ".tif", "image/tiff" }, { ".tiff", "image/tiff" }, { ".toc", "application/octet-stream" }, { ".tr", "application/x-troff" }, { ".trm", "application/x-msterminal" }, { ".ts", "video/vnd.dlna.mpeg-tts" }, { ".tsv", "text/tab-separated-values" }, { ".ttc", "application/x-font-ttf" }, { ".ttf", "application/x-font-ttf" }, { ".tts", "video/vnd.dlna.mpeg-tts" }, { ".txt", "text/plain" }, { ".u32", "application/octet-stream" }, { ".uls", "text/iuls" }, { ".ustar", "application/x-ustar" }, { ".vbs", "text/vbscript" }, { ".vcf", "text/x-vcard" }, { ".vcs", "text/plain" }, { ".vdx", "application/vnd.ms-visio.viewer" }, { ".vml", "text/xml" }, { ".vsd", "application/vnd.visio" }, { ".vss", "application/vnd.visio" }, { ".vst", "application/vnd.visio" }, { ".vsto", "application/x-ms-vsto" }, { ".vsw", "application/vnd.visio" }, { ".vsx", "application/vnd.visio" }, { ".vtx", "application/vnd.visio" }, { ".wasm", "application/wasm" }, { ".wav", "audio/wav" }, { ".wax", "audio/x-ms-wax" }, { ".wbmp", "image/vnd.wap.wbmp" }, { ".wcm", "application/vnd.ms-works" }, { ".wdb", "application/vnd.ms-works" }, { ".webm", "video/webm" }, { ".webmanifest", "application/manifest+json" }, // https://w3c.github.io/manifest/#media-type-registration { ".webp", "image/webp" }, { ".wks", "application/vnd.ms-works" }, { ".wm", "video/x-ms-wm" }, { ".wma", "audio/x-ms-wma" }, { ".wmd", "application/x-ms-wmd" }, { ".wmf", "application/x-msmetafile" }, { ".wml", "text/vnd.wap.wml" }, { ".wmlc", "application/vnd.wap.wmlc" }, { ".wmls", "text/vnd.wap.wmlscript" }, { ".wmlsc", "application/vnd.wap.wmlscriptc" }, { ".wmp", "video/x-ms-wmp" }, { ".wmv", "video/x-ms-wmv" }, { ".wmx", "video/x-ms-wmx" }, { ".wmz", "application/x-ms-wmz" }, { ".woff", "application/font-woff" }, // https://www.w3.org/TR/WOFF/#appendix-b { ".woff2", "font/woff2" }, // https://www.w3.org/TR/WOFF2/#IMT { ".wps", "application/vnd.ms-works" }, { ".wri", "application/x-mswrite" }, { ".wrl", "x-world/x-vrml" }, { ".wrz", "x-world/x-vrml" }, { ".wsdl", "text/xml" }, { ".wtv", "video/x-ms-wtv" }, { ".wvx", "video/x-ms-wvx" }, { ".x", "application/directx" }, { ".xaf", "x-world/x-vrml" }, { ".xaml", "application/xaml+xml" }, { ".xap", "application/x-silverlight-app" }, { ".xbap", "application/x-ms-xbap" }, { ".xbm", "image/x-xbitmap" }, { ".xdr", "text/plain" }, { ".xht", "application/xhtml+xml" }, { ".xhtml", "application/xhtml+xml" }, { ".xla", "application/vnd.ms-excel" }, { ".xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, { ".xlc", "application/vnd.ms-excel" }, { ".xlm", "application/vnd.ms-excel" }, { ".xls", "application/vnd.ms-excel" }, { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { ".xlt", "application/vnd.ms-excel" }, { ".xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, { ".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, { ".xlw", "application/vnd.ms-excel" }, { ".xml", "text/xml" }, { ".xof", "x-world/x-vrml" }, { ".xpm", "image/x-xpixmap" }, { ".xps", "application/vnd.ms-xpsdocument" }, { ".xsd", "text/xml" }, { ".xsf", "text/xml" }, { ".xsl", "text/xml" }, { ".xslt", "text/xml" }, { ".xsn", "application/octet-stream" }, { ".xtp", "application/octet-stream" }, { ".xwd", "image/x-xwindowdump" }, { ".z", "application/x-compress" }, { ".zip", "application/x-zip-compressed" }, }) { } #endregion /// <summary> /// Creates a lookup engine using the provided mapping. /// It is recommended that the IDictionary instance use StringComparer.OrdinalIgnoreCase. /// </summary> /// <param name="mapping"></param> public FileExtensionContentTypeProvider(IDictionary<string, string> mapping) { if (mapping == null) { throw new ArgumentNullException(nameof(mapping)); } Mappings = mapping; } /// <summary> /// The cross reference table of file extensions and content-types. /// </summary> public IDictionary<string, string> Mappings { get; private set; } /// <summary> /// Given a file path, determine the MIME type /// </summary> /// <param name="subpath">A file path</param> /// <param name="contentType">The resulting MIME type</param> /// <returns>True if MIME type could be determined</returns> public bool TryGetContentType(string subpath, [MaybeNullWhen(false)] out string contentType) { var extension = GetExtension(subpath); if (extension == null) { contentType = null; return false; } return Mappings.TryGetValue(extension, out contentType); } private static string GetExtension(string path) { // Don't use Path.GetExtension as that may throw an exception if there are // invalid characters in the path. Invalid characters should be handled // by the FileProviders if (string.IsNullOrWhiteSpace(path)) { return null; } int index = path.LastIndexOf('.'); if (index < 0) { return null; } return path.Substring(index); } } } }
创建自定义的BlazorWebViewHandler类
BlazorWebViewHandler是MAUI Blazor中处理BlazorWebView相关的一个类,我们自定义一个类替换它,添加自己需要的一些处理逻辑
Maui Blazor中iOS / Mac和其他平台的baseUrl是不统一的,iOS / Mac是 app://0.0.0.0 (原因在Maui源码的注释中有写到,iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme),其他平台是 https://0.0.0.0 ,所以我们的url设为相对路径才能统一,而且与页面同源,不会有跨域问题(笔者之前在iOS / Mac上的做法就是注册自定义协议,使用html2canvas截图时,结果发生了跨域问题)。
添加MauiBlazorWebViewHandler.cs
代码如下
using Microsoft.AspNetCore.Components.WebView.Maui; namespace MauiBlazorLocalMediaFile { public partial class MauiBlazorWebViewHandler : BlazorWebViewHandler { private const string AppHostAddress = "0.0.0.0"; #if IOS || MACCATALYST public const string BaseUri = $"app://{AppHostAddress}/"; #else public const string BaseUri = $"https://{AppHostAddress}/"; #endif public readonly static Dictionary<string, string> AppFilePathMap = new() { { FileSystem.AppDataDirectory, "appdata" }, { FileSystem.CacheDirectory, "cache" }, }; private static readonly string OtherFileMapPath = "file"; //把真实的文件路径转化为url相对路径 public static string FilePathToUrlRelativePath(string filePath) { foreach (var item in AppFilePathMap) { if (filePath.StartsWith(item.Key)) { return item.Value + filePath[item.Key.Length..].Replace(Path.DirectorySeparatorChar, '/'); } } return OtherFileMapPath + "/" + Uri.EscapeDataString(filePath); } //把url相对路径转化为真实的文件路径 public static string UrlRelativePathToFilePath(string urlRelativePath) { UrlRelativePathToFilePath(urlRelativePath, out string path); return path; } private static bool Intercept(string uri, out string path) { if (!uri.StartsWith(BaseUri)) { path = string.Empty; return false; } var urlRelativePath = uri[BaseUri.Length..]; return UrlRelativePathToFilePath(urlRelativePath, out path); } private static bool UrlRelativePathToFilePath(string urlRelativePath, out string path) { if (string.IsNullOrEmpty(urlRelativePath)) { path = string.Empty; return false; } urlRelativePath = Uri.UnescapeDataString(urlRelativePath); foreach (var item in AppFilePathMap) { if (urlRelativePath.StartsWith(item.Value + '/')) { string urlRelativePathSub = urlRelativePath[(item.Value.Length + 1)..]; path = Path.Combine(item.Key, urlRelativePathSub.Replace('/', Path.DirectorySeparatorChar)); if (File.Exists(path)) { return true; } } } if (urlRelativePath.StartsWith(OtherFileMapPath + '/')) { string urlRelativePathSub = urlRelativePath[(OtherFileMapPath.Length + 1)..]; path = urlRelativePathSub.Replace('/', Path.DirectorySeparatorChar); if (File.Exists(path)) { return true; } } path = string.Empty; return false; } } }
Android
添加MauiBlazorWebViewHandler.Android.cs
代码如下
using Android.Webkit; using MauiBlazorLocalMediaFile.Utilities; using WebView = Android.Webkit.WebView; namespace MauiBlazorLocalMediaFile { public partial class MauiBlazorWebViewHandler { #pragma warning disable CA1416 // 验证平台兼容性 protected override void ConnectHandler(WebView platformView) { base.ConnectHandler(platformView); platformView.SetWebViewClient(new MyWebViewClient(platformView.WebViewClient)); } #nullable disable private class MyWebViewClient : WebViewClient { private WebViewClient WebViewClient { get; } public MyWebViewClient(WebViewClient webViewClient) { WebViewClient = webViewClient; } public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request) { return WebViewClient.ShouldOverrideUrlLoading(view, request); } public override WebResourceResponse ShouldInterceptRequest(Android.Webkit.WebView view, IWebResourceRequest request) { var intercept = InterceptCustomPathRequest(request, out WebResourceResponse webResourceResponse); if (intercept) { return webResourceResponse; } return WebViewClient.ShouldInterceptRequest(view, request); } public override void OnPageFinished(Android.Webkit.WebView view, string url) => WebViewClient.OnPageFinished(view, url); protected override void Dispose(bool disposing) { if (!disposing) return; WebViewClient.Dispose(); } private static bool InterceptCustomPathRequest(IWebResourceRequest request, out WebResourceResponse webResourceResponse) { webResourceResponse = null; var uri = request.Url.ToString(); if (!Intercept(uri, out string path)) { return false; } if (!File.Exists(path)) { return false; } webResourceResponse = CreateWebResourceResponse(request, path); return true; } private static WebResourceResponse CreateWebResourceResponse(IWebResourceRequest request, string path) { string contentType = StaticContentProvider.GetResponseContentTypeOrDefault(path); var headers = StaticContentProvider.GetResponseHeaders(contentType); FileStream stream = File.OpenRead(path); var length = stream.Length; long rangeStart = 0; long rangeEnd = length - 1; string encoding = "UTF-8"; int stateCode = 200; string reasonPhrase = "OK"; //适用于音频视频文件资源的响应 bool partial = request.RequestHeaders.TryGetValue("Range", out string rangeString); if (partial) { //206,可断点续传 stateCode = 206; reasonPhrase = "Partial Content"; var ranges = rangeString.Split('='); if (ranges.Length > 1 && !string.IsNullOrEmpty(ranges[1])) { string[] rangeDatas = ranges[1].Split("-"); rangeStart = Convert.ToInt64(rangeDatas[0]); if (rangeDatas.Length > 1 && !string.IsNullOrEmpty(rangeDatas[1])) { rangeEnd = Convert.ToInt64(rangeDatas[1]); } } headers.Add("Accept-Ranges", "bytes"); headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{length}"); } //这一行删去似乎也不影响 headers.Add("Content-Length", (rangeEnd - rangeStart + 1).ToString()); var response = new WebResourceResponse(contentType, encoding, stateCode, reasonPhrase, headers, stream); return response; } } } }
iOS / Mac
iOS / Mac中我们要替换Maui对于app://自定义协议的注册,但是很多类、方法、属性、字段是不公开的,也就是internal和private,所以我们的代码用了很多反射。
添加 MauiBlazorWebViewHandler.MaciOS.cs
代码如下
using Foundation; using Microsoft.AspNetCore.Components.WebView; using Microsoft.AspNetCore.Components.WebView.Maui; using Microsoft.Extensions.Logging; using MauiBlazorLocalMediaFile.Utilities; using System.Globalization; using System.Reflection; using System.Runtime.Versioning; using UIKit; using WebKit; using RectangleF = CoreGraphics.CGRect; namespace MauiBlazorLocalMediaFile { #nullable disable public partial class MauiBlazorWebViewHandler { private BlazorWebViewHandlerReflection _base; private BlazorWebViewHandlerReflection Base => _base ??= new(this); [SupportedOSPlatform("ios11.0")] protected override WKWebView CreatePlatformView() { Base.LoggerCreatingWebKitWKWebView(); var config = new WKWebViewConfiguration(); // By default, setting inline media playback to allowed, including autoplay // and picture in picture, since these things MUST be set during the webview // creation, and have no effect if set afterwards. // A custom handler factory delegate could be set to disable these defaults // but if we do not set them here, they cannot be changed once the // handler's platform view is created, so erring on the side of wanting this // capability by default. if (OperatingSystem.IsMacCatalystVersionAtLeast(10) || OperatingSystem.IsIOSVersionAtLeast(10)) { config.AllowsPictureInPictureMediaPlayback = true; config.AllowsInlineMediaPlayback = true; config.MediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypes.None; } VirtualView.BlazorWebViewInitializing(new BlazorWebViewInitializingEventArgs() { Configuration = config }); // Legacy Developer Extras setting. config.Preferences.SetValueForKey(NSObject.FromObject(Base.DeveloperToolsEnabled), new NSString("developerExtrasEnabled")); config.UserContentController.AddScriptMessageHandler(Base.CreateWebViewScriptMessageHandler(), "webwindowinterop"); config.UserContentController.AddUserScript(new WKUserScript( new NSString(Base.BlazorInitScript), WKUserScriptInjectionTime.AtDocumentEnd, true)); // iOS WKWebView doesn't allow handling 'http'/'https' schemes, so we use the fake 'app' scheme config.SetUrlSchemeHandler(new SchemeHandler(this), urlScheme: "app"); var webview = new WKWebView(RectangleF.Empty, config) { BackgroundColor = UIColor.Clear, AutosizesSubviews = true }; if (OperatingSystem.IsIOSVersionAtLeast(16, 4) || OperatingSystem.IsMacCatalystVersionAtLeast(13, 3)) { // Enable Developer Extras for Catalyst/iOS builds for 16.4+ webview.SetValueForKey(NSObject.FromObject(Base.DeveloperToolsEnabled), new NSString("inspectable")); } VirtualView.BlazorWebViewInitialized(Base.CreateBlazorWebViewInitializedEventArgs(webview)); Base.LoggerCreatedWebKitWKWebView(); return webview; } private class SchemeHandler : NSObject, IWKUrlSchemeHandler { private readonly MauiBlazorWebViewHandler _webViewHandler; public SchemeHandler(MauiBlazorWebViewHandler webViewHandler) { _webViewHandler = webViewHandler; } [Export("webView:startURLSchemeTask:")] [SupportedOSPlatform("ios11.0")] public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask) { var intercept = InterceptCustomPathRequest(urlSchemeTask); if (intercept) { return; } var responseBytes = GetResponseBytes(urlSchemeTask.Request.Url?.AbsoluteString ?? "", out var contentType, statusCode: out var statusCode); if (statusCode == 200) { using (var dic = new NSMutableDictionary<NSString, NSString>()) { dic.Add((NSString)"Content-Length", (NSString)(responseBytes.Length.ToString(CultureInfo.InvariantCulture))); dic.Add((NSString)"Content-Type", (NSString)contentType); // Disable local caching. This will prevent user scripts from executing correctly. dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store"); if (urlSchemeTask.Request.Url != null) { using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, statusCode, "HTTP/1.1", dic); urlSchemeTask.DidReceiveResponse(response); } } urlSchemeTask.DidReceiveData(NSData.FromArray(responseBytes)); urlSchemeTask.DidFinish(); } } private byte[] GetResponseBytes(string? url, out string contentType, out int statusCode) { var allowFallbackOnHostPage = _webViewHandler.Base.IsBaseOfPage(_webViewHandler.Base.AppOriginUri, url); url = _webViewHandler.Base.QueryStringHelperRemovePossibleQueryString(url); _webViewHandler.Base.LoggerHandlingWebRequest(url); if (_webViewHandler.Base.TryGetResponseContentInternal(url, allowFallbackOnHostPage, out statusCode, out var statusMessage, out var content, out var headers)) { statusCode = 200; using var ms = new MemoryStream(); content.CopyTo(ms); content.Dispose(); contentType = headers["Content-Type"]; _webViewHandler?.Base.LoggerResponseContentBeingSent(url, statusCode); return ms.ToArray(); } else { _webViewHandler?.Base.LoggerReponseContentNotFound(url); statusCode = 404; contentType = string.Empty; return Array.Empty<byte>(); } } [Export("webView:stopURLSchemeTask:")] public void StopUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask) { } private static bool InterceptCustomPathRequest(IWKUrlSchemeTask urlSchemeTask) { var uri = urlSchemeTask.Request.Url.ToString(); if (uri == null) { return false; } if (!Intercept(uri, out string path)) { return false; } if (!File.Exists(path)) { return false; } long length = new FileInfo(path).Length; string contentType = StaticContentProvider.GetResponseContentTypeOrDefault(path); using (var dic = new NSMutableDictionary<NSString, NSString>()) { dic.Add((NSString)"Content-Length", (NSString)(length.ToString(CultureInfo.InvariantCulture))); dic.Add((NSString)"Content-Type", (NSString)contentType); // Disable local caching. This will prevent user scripts from executing correctly. dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store"); using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, 200, "HTTP/1.1", dic); urlSchemeTask.DidReceiveResponse(response); } urlSchemeTask.DidReceiveData(NSData.FromFile(path)); urlSchemeTask.DidFinish(); return true; } } } public class BlazorWebViewHandlerReflection { public BlazorWebViewHandlerReflection(BlazorWebViewHandler blazorWebViewHandler) { _blazorWebViewHandler = blazorWebViewHandler; _logger = new(() => { var property = Type.GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance); return (ILogger)property?.GetValue(_blazorWebViewHandler); }); _blazorInitScript = new(() => { var property = Type.GetField("BlazorInitScript", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); return (string)property?.GetValue(_blazorWebViewHandler); }); _appOriginUri = new(() => { var property = Type.GetField("AppOriginUri", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); return (Uri)property?.GetValue(_blazorWebViewHandler); }); } private readonly BlazorWebViewHandler _blazorWebViewHandler; private static readonly Type Type = typeof(BlazorWebViewHandler); private static readonly Assembly Assembly = Type.Assembly; private static readonly Type TypeLog = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.Log")!; private readonly Lazy<ILogger> _logger; private readonly Lazy<string> _blazorInitScript; private readonly Lazy<Uri> _appOriginUri; private object WebviewManager; private MethodInfo MethodTryGetResponseContentInternal; private MethodInfo MethodIsBaseOfPage; private MethodInfo MethodQueryStringHelperRemovePossibleQueryString; public ILogger Logger => _logger.Value; public string BlazorInitScript => _blazorInitScript.Value; public Uri AppOriginUri => _appOriginUri.Value; public bool DeveloperToolsEnabled => GetDeveloperToolsEnabled(); public void LoggerCreatingWebKitWKWebView() { var method = TypeLog.GetMethod("CreatingWebKitWKWebView"); method?.Invoke(null, new object[] { Logger }); } public void LoggerCreatedWebKitWKWebView() { var method = TypeLog.GetMethod("CreatedWebKitWKWebView"); method?.Invoke(null, new object[] { Logger }); } public void LoggerHandlingWebRequest(string url) { var method = TypeLog.GetMethod("HandlingWebRequest"); method?.Invoke(null, new object[] { Logger, url }); } public void LoggerResponseContentBeingSent(string url, int statusCode) { var method = TypeLog.GetMethod("ResponseContentBeingSent"); method?.Invoke(null, new object[] { Logger, url, statusCode }); } public void LoggerReponseContentNotFound(string url) { var method = TypeLog.GetMethod("ReponseContentNotFound"); method?.Invoke(null, new object[] { Logger, url }); } private bool GetDeveloperToolsEnabled() { var PropertyDeveloperTools = Type.GetProperty("DeveloperTools", BindingFlags.NonPublic | BindingFlags.Instance); var DeveloperTools = PropertyDeveloperTools.GetValue(_blazorWebViewHandler); var type = DeveloperTools.GetType(); var Enabled = type.GetProperty("Enabled", BindingFlags.Public | BindingFlags.Instance); return (bool)Enabled?.GetValue(DeveloperTools); } public IWKScriptMessageHandler CreateWebViewScriptMessageHandler() { Type webViewScriptMessageHandlerType = Type.GetNestedType("WebViewScriptMessageHandler", BindingFlags.NonPublic); if (webViewScriptMessageHandlerType != null) { // 获取 MessageReceived 方法信息 MethodInfo messageReceivedMethod = Type.GetMethod("MessageReceived", BindingFlags.Instance | BindingFlags.NonPublic); if (messageReceivedMethod != null) { // 创建 WebViewScriptMessageHandler 实例 object webViewScriptMessageHandlerInstance = Activator.CreateInstance(webViewScriptMessageHandlerType, new object[] { Delegate.CreateDelegate(typeof(Action<Uri, string>), _blazorWebViewHandler, messageReceivedMethod) }); return (IWKScriptMessageHandler)webViewScriptMessageHandlerInstance; } } return null; } public BlazorWebViewInitializedEventArgs CreateBlazorWebViewInitializedEventArgs(WKWebView wKWebView) { var blazorWebViewInitializedEventArgs = new BlazorWebViewInitializedEventArgs(); PropertyInfo property = typeof(BlazorWebViewInitializedEventArgs).GetProperty("WebView", BindingFlags.Public | BindingFlags.Instance); property.SetValue(blazorWebViewInitializedEventArgs, wKWebView); return blazorWebViewInitializedEventArgs; } public bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers) { if (MethodTryGetResponseContentInternal == null) { var Field_webviewManager = Type.GetField("_webviewManager", BindingFlags.NonPublic | BindingFlags.Instance); WebviewManager = Field_webviewManager.GetValue(_blazorWebViewHandler); MethodTryGetResponseContentInternal = WebviewManager.GetType().GetMethod("TryGetResponseContentInternal", BindingFlags.NonPublic | BindingFlags.Instance); } // 定义参数 object[] parameters = new object[] { uri, allowFallbackOnHostPage, 0, null, null, null }; bool result = (bool)MethodTryGetResponseContentInternal.Invoke(WebviewManager, parameters); // 获取返回值和输出参数 statusCode = (int)parameters[2]; statusMessage = (string)parameters[3]; content = (Stream)parameters[4]; headers = (IDictionary<string, string>)parameters[5]; return result; } public bool IsBaseOfPage(Uri baseUri, string? uriString) { if (MethodIsBaseOfPage == null) { var type = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.Maui.UriExtensions")!; MethodIsBaseOfPage = type.GetMethod("IsBaseOfPage", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); } return (bool)MethodIsBaseOfPage.Invoke(null, new object[] { baseUri, uriString }); } public string QueryStringHelperRemovePossibleQueryString(string? url) { if (MethodQueryStringHelperRemovePossibleQueryString == null) { var type = Assembly.GetType("Microsoft.AspNetCore.Components.WebView.QueryStringHelper")!; MethodQueryStringHelperRemovePossibleQueryString = type.GetMethod("RemovePossibleQueryString", BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); } return (string)MethodQueryStringHelperRemovePossibleQueryString.Invoke(null, new object[] { url }); } } }
Windows
添加MauiBlazorWebViewHandler.Windows.cs
代码如下
using MauiBlazorLocalMediaFile.Utilities; using Microsoft.Web.WebView2.Core; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Storage.Streams; using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2; namespace MauiBlazorLocalMediaFile { public partial class MauiBlazorWebViewHandler { protected override void ConnectHandler(WebView2Control platformView) { base.ConnectHandler(platformView); platformView.CoreWebView2Initialized += CoreWebView2Initialized; } protected override void DisconnectHandler(WebView2Control platformView) { platformView.CoreWebView2Initialized -= CoreWebView2Initialized; base.DisconnectHandler(platformView); } private void CoreWebView2Initialized(WebView2Control sender, Microsoft.UI.Xaml.Controls.CoreWebView2InitializedEventArgs args) { var webview2 = sender.CoreWebView2; webview2.WebResourceRequested += WebView2WebResourceRequested; } async void WebView2WebResourceRequested(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args) { await InterceptCustomPathRequest(webview2, args); } static async Task<bool> InterceptCustomPathRequest(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args) { string uri = args.Request.Uri; if (!Intercept(uri, out string filePath)) { return false; } if (File.Exists(filePath)) { args.Response = await CreateWebResourceResponse(webview2, args, filePath); } else { args.Response = webview2.Environment.CreateWebResourceResponse(null, 404, "Not Found", string.Empty); } return true; static string GetHeaderString(IDictionary<string, string> headers) => string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}")); static async Task<CoreWebView2WebResourceResponse> CreateWebResourceResponse(CoreWebView2 webview2, CoreWebView2WebResourceRequestedEventArgs args, string filePath) { var contentType = StaticContentProvider.GetResponseContentTypeOrDefault(filePath); var headers = StaticContentProvider.GetResponseHeaders(contentType); using var contentStream = File.OpenRead(filePath); var length = contentStream.Length; long rangeStart = 0; long rangeEnd = length - 1; int statusCode = 200; string reasonPhrase = "OK"; //适用于音频视频文件资源的响应 bool partial = args.Request.Headers.Contains("Range"); if (partial) { statusCode = 206; reasonPhrase = "Partial Content"; var rangeString = args.Request.Headers.GetHeader("Range"); var ranges = rangeString.Split('='); if (ranges.Length > 1 && !string.IsNullOrEmpty(ranges[1])) { string[] rangeDatas = ranges[1].Split("-"); rangeStart = Convert.ToInt64(rangeDatas[0]); if (rangeDatas.Length > 1 && !string.IsNullOrEmpty(rangeDatas[1])) { rangeEnd = Convert.ToInt64(rangeDatas[1]); } else { //每次加载4Mb,不能设置太多 rangeEnd = Math.Min(rangeEnd, rangeStart + 4 * 1024 * 1024); } } headers.Add("Accept-Ranges", "bytes"); headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/{length}"); } headers.Add("Content-Length", (rangeEnd - rangeStart + 1).ToString()); var headerString = GetHeaderString(headers); IRandomAccessStream stream = await ReadStreamRange(contentStream, rangeStart, rangeEnd); return webview2.Environment.CreateWebResourceResponse(stream, statusCode, reasonPhrase, headerString); } static async Task<IRandomAccessStream> ReadStreamRange(Stream contentStream, long start, long end) { long length = end - start + 1; contentStream.Position = start; using var memoryStream = new MemoryStream(); StreamCopy(contentStream, memoryStream, length); // 将内存流的位置重置为起始位置 memoryStream.Seek(0, SeekOrigin.Begin); var randomAccessStream = new InMemoryRandomAccessStream(); await randomAccessStream.WriteAsync(memoryStream.GetWindowsRuntimeBuffer()); return randomAccessStream; } // 辅助方法,用于限制StreamCopy复制的数据长度 static void StreamCopy(Stream source, Stream destination, long length) { //缓冲区设为1Mb,应该是够了 byte[] buffer = new byte[1024 * 1024]; int bytesRead; while (length > 0 && (bytesRead = source.Read(buffer, 0, (int)Math.Min(buffer.Length, length))) > 0) { destination.Write(buffer, 0, bytesRead); length -= bytesRead; } } } } }
在MauiProgram.cs中添加
添加在builder.Services.AddMauiBlazorWebView();的下面
builder.Services.ConfigureMauiHandlers(delegate (IMauiHandlersCollection handlers) { handlers.AddHandler<IBlazorWebView>((IServiceProvider _) => new MauiBlazorWebViewHandler()); });

试验一下
添加一个复制文件的静态类
简单写一个方法,用于把选中的文件复制到指定目录,并且返回所需要的url相对路径
为了防止文件被重复复制,我们以md5作为文件名
添加文件夹Utilities/File,在里面添加一个静态类MediaResourceFile.cs
代码如下
using System.Security.Cryptography; namespace MauiBlazorLocalMediaFile.Utilities { public static class MediaResourceFile { public static async Task<string?> CreateMediaResourceFileAsync(string targetDirectoryPath, string? sourceFilePath) { if (string.IsNullOrEmpty(sourceFilePath)) { return null; } using Stream stream = File.OpenRead(sourceFilePath); //新的文件以文件的md5为文件名,确保文件不会重复存在 //获取文件的md5有一点耗时,暂时没想到更好的方案 var fn = stream.CreateMD5() + Path.GetExtension(sourceFilePath); var targetFilePath = Path.Combine(targetDirectoryPath, fn); //如果文件存在就不用复制了 if (!File.Exists(targetFilePath)) { if (sourceFilePath.StartsWith(FileSystem.CacheDirectory)) { stream.Close(); await FileMoveAsync(sourceFilePath, targetFilePath); } else { //将流的位置重置为起始位置 stream.Seek(0, SeekOrigin.Begin); await FileCopyAsync(targetFilePath, stream); } } return MauiBlazorWebViewHandler.FilePathToUrlRelativePath(targetFilePath); } private static async Task FileCopyAsync(string targetFilePath, Stream sourceStream) { CreateFileDirectory(targetFilePath); using (FileStream localFileStream = File.OpenWrite(targetFilePath)) { await sourceStream.CopyToAsync(localFileStream, 1024 * 1024); }; } private static Task FileMoveAsync(string sourceFilePath, string targetFilePath) { CreateFileDirectory(targetFilePath); File.Move(sourceFilePath, targetFilePath); return Task.CompletedTask; } private static void CreateFileDirectory(string filePath) { string? directoryPath = Path.GetDirectoryName(filePath); if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath!); } } private static string CreateMD5(this Stream stream, int bufferSize = 1024 * 1024) { using MD5 md5 = MD5.Create(); byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); } md5.TransformFinalBlock(buffer, 0, 0); byte[] hash = md5.Hash ?? []; return BitConverter.ToString(hash).Replace("-", "").ToLower(); } } }
写一个选中视频文件并显示的示例页面
@page "/video" @using MauiBlazorLocalMediaFile.Utilities <h1>Video</h1> <video src="@Src" controls style="max-width:100%;"></video> <div style="word-break: break-all;">Src="@Src"</div> <button class="btn btn-primary" @onclick="()=>Pick(true)">选中并复制到AppDataDirectory</button> <button class="btn btn-primary" @onclick="()=>Pick(false)">选中不复制(仅限Windows)</button> @code { private string? Src; private async void Pick(bool copy) { #if !WINDOWS if (!copy) { return; } #endif var result = await MediaPicker.Default.PickVideoAsync(); var path = result?.FullPath; if (path is null) { return; } if (copy) { var targetDirectoryPath = Path.Combine(FileSystem.AppDataDirectory, "Video"); Src = await MediaResourceFile.CreateMediaResourceFileAsync(targetDirectoryPath, path); } else { Src = MauiBlazorWebViewHandler.FilePathToUrlRelativePath(path); } await InvokeAsync(StateHasChanged); } }
截图
用笔者比较喜欢的动画电影《魁拔》作为视频文件,大约500MB多一些
Windows

Android
iOS / Mac选中视频会被压缩,特别慢,所以就用一个短的视频了
iOS
Mac

后来补的一张音频的截图,用的原始路径

后记
这篇文章改了又改,总觉得有不妥之处。实在改不动了,就这么地吧,可能写的还是不够详细。
笔者水平有限,性能上可能还存在优化的空间,希望各位大佬不吝赐教,提出宝贵意见
源码
本文中的例子的源码放到 Github 和 Gitee 了
有需要的可以去看一下