Blazor中HTML video标签锁定视频文件无法移动如何解决
问题场景
在Blazor Dialog组件中展示磁盘目录存储的视频文件,选中目标视频后点击「Upload」按钮上传至服务器,上传完成后需要将源文件移动至归档目录。执行移动操作时系统提示文件被其他进程占用,确认是<video>标签加载视频时持有文件锁导致。
初始实现的Razor代码如下:
<RadzenListBox TValue="FileDto" @ref=FileListBox Data=@TakesList> <Template Context="takeIn"> <RadzenCard> @{ var take = takeIn as FileDto; } <div class="row"> <RadzenCheckBox TValue="bool" @bind-Value=take.IsSelectedForUpload class="col-auto"/> <div class="col-auto"> <video class="w-25" controls @ref=@take.VideoReference> <source src=@($"{@take.FileName}#t=9.5") type="video/mp4"> Your browser does not support the video tag. </video> </div> </div> <RadzenProgressBar Max=100 Value=@take.PercentageFinished Visible=@(take.PercentageFinished > 0) /> </RadzenCard> </Template> </RadzenListBox>
补充说明
最初尝试改为列表中展示视频缩略图,避免直接加载完整视频锁定文件,该方案可解决列表页的文件锁定问题。后续新增独立Dialog组件承载视频播放器,用于单独播放选中的视频,原本预期关闭Dialog后文件锁会自动释放,但实际测试发现:播放完视频、关闭Dialog后,文件仍然处于锁定状态,无法执行移动操作。
可行解决方案
方案1:主动调用JS接口强制释放video持有的文件锁
浏览器不会在video元素隐藏、甚至Dialog关闭DOM移除后立刻释放文件句柄,必须主动触发媒体资源卸载流程,在关闭Dialog、执行文件移动操作前,通过IJSRuntime调用以下JS方法,传入video元素的引用:
function releaseVideoFileLock(videoEl) { if (!videoEl) return; // 先暂停播放 videoEl.pause(); // 移除所有source子节点 while (videoEl.firstChild) { videoEl.removeChild(videoEl.firstChild); } // 移除src属性 videoEl.removeAttribute('src'); // 调用load方法强制卸载原有资源,释放文件锁 videoEl.load(); }
注意:不要只设置videoEl.src = ''或者赋值空白地址,部分浏览器会将空地址解析为当前页面地址,不会触发资源释放,必须调用load()方法才能完成卸载。
方案2:改用后端流接口返回视频,从根源避免文件锁定
直接将本地物理路径作为video的src时,浏览器会直接打开本地文件句柄读取内容,锁的释放时机完全不可控。可以在后端新增一个视频访问接口,打开文件时指定共享读写/删除权限,就算浏览器持有连接,也不会阻塞本地文件移动操作:
// 后端接口示例(Program.cs中注册) app.MapGet("/api/video/play", async (string fileName, IWebHostEnvironment env) => { // 必须做路径合法性校验,防止路径遍历攻击 var videoRootPath = Path.Combine(env.ContentRootPath, "你的视频存储目录"); var fullPath = Path.GetFullPath(Path.Combine(videoRootPath, fileName)); if (!fullPath.StartsWith(videoRootPath) || !File.Exists(fullPath)) { return Results.NotFound(); } // 打开文件时设置共享模式,允许其他进程读写、删除、移动文件 var fileStream = new FileStream( fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: 4096, useAsync: true); // 开启范围处理,支持视频拖动进度条 return Results.File(fileStream, "video/mp4", enableRangeProcessing: true); });
前端将video的src改为指向该接口即可:
<source src=@($"/api/video/play?fileName={take.FileName}#t=9.5") type="video/mp4">
这个方案稳定性最高,不需要额外处理锁释放逻辑,就算视频正在播放,也可以正常执行文件移动、删除操作。
方案3:增加重试逻辑作为兜底
部分浏览器内核(尤其是旧版Chromium)就算主动调用了释放方法,也可能存在几十到上百毫秒的锁释放延迟,可以在执行文件移动操作时增加重试机制,最多重试3次,每次间隔200ms,覆盖偶发的锁未释放场景。
内容的提问来源于stack exchange,提问作者Paul Sinnema

