You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET MVC控制器视频流在Safari/苹果浏览器无法播放求助

解决ASP.NET MVC视频流在Safari中无法播放的问题

问题根源

Safari(桌面端及iOS端)的<video>元素要求视频流必须支持HTTP范围请求(Range Requests)——服务器需响应Range头,返回指定区间的字节数据,这是实现流式播放(拖拽进度、分段加载)的必要条件。当前代码仅一次性发送完整文件,未处理Range请求,导致Safari抛出NotSupportedError。

修复步骤

1. 修改控制器Action,支持Range请求

替换原FetchVideo方法为以下实现,它会处理Range头、返回正确的206 Partial Content状态码,同时修正字节读写逻辑错误:

[HttpGet]
[CustomAuthorize(Roles = Roles._SYS_ADMIN)]
public async Task FetchVideo(int videoID)
{
    try
    {
        Video video = new Video();
        video.Get(videoID);
        var videoBytes = video.Content;
        var totalBytes = videoBytes.Length;

        // 处理Range请求
        var rangeHeader = Request.Headers["Range"];
        long start = 0;
        long end = totalBytes - 1;

        if (!string.IsNullOrEmpty(rangeHeader))
        {
            // 解析Range头,格式如: bytes=0-1023
            var rangeParts = rangeHeader.Replace("bytes=", "").Split('-');
            start = long.Parse(rangeParts[0]);
            if (rangeParts.Length > 1 && !string.IsNullOrEmpty(rangeParts[1]))
            {
                end = long.Parse(rangeParts[1]);
                // 确保end不超过文件总长度
                end = Math.Min(end, totalBytes - 1);
            }
            // 返回206 Partial Content状态码
            Response.StatusCode = 206;
        }
        else
        {
            // 无Range请求时返回200 OK
            Response.StatusCode = 200;
        }

        var bytesToSend = end - start + 1;

        // 设置必要的响应头
        Response.ContentType = "video/mp4";
        Response.AddHeader("Accept-Ranges", "bytes");
        Response.AddHeader("Content-Disposition", "inline; filename=video.mp4");
        Response.AddHeader("Content-Length", bytesToSend.ToString());
        Response.AddHeader("Content-Range", $"bytes {start}-{end}/{totalBytes}");

        // 从指定位置读取并发送数据
        using (var memoryStream = new MemoryStream(videoBytes))
        {
            memoryStream.Seek(start, SeekOrigin.Begin);
            var buffer = new byte[4096];
            long remaining = bytesToSend;

            while (remaining > 0 && Response.IsClientConnected)
            {
                var readLength = await memoryStream.ReadAsync(buffer, 0, (int)Math.Min(buffer.Length, remaining));
                if (readLength == 0) break;

                await Response.OutputStream.WriteAsync(buffer, 0, readLength);
                await Response.OutputStream.FlushAsync();

                remaining -= readLength;
            }
        }
    }
    catch (Exception ex)
    {
        Response.StatusCode = 500;
        await Response.WriteAsync($"Error: {ex.Message}");
    }
    // 不要手动关闭Response,ASP.NET会自动处理
}

2. 修正前端代码冲突

原代码中<video>标签内嵌<source>可能导致加载冲突,调整如下:

调整后的HTML标记:

<!-- start: play video modal -->
<div class="modal fade" id="play-video-modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title text-uppercase" id="modal-title"></h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="row p-0 m-0">
                    <div class="col-md-12 p-0 m-0">
                        <div id="video-player-container" class="section d-flex justify-content-center embed-responsive embed-responsive-16by9">
                            <video id="video-player" class="embed-responsive-item" style="background-color: black;" autoplay playsinline muted controls loop>
                                Your browser does not support the video tag.
                            </video>
                        </div>
                    </div>
                </div>
            </div>
            <div id="modal-loader" class="text-center pt-4" style="display: none;">
                <div class="spinner-grow text-primary" role="status">
                    <span class="sr-only">Loading...</span>
                </div>
            </div>
            <div class="modal-footer">
                <button id="decline-modal" type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>
<!-- end: play video modal -->

调整后的jQuery代码:

$('#play-video-modal').on('show.bs.modal', function (e) {
  var promptUrl = '/Admin/Bundle/FetchVideo?videoID=' + value.Video.ID;
  var videoPlayer = $('#play-video-modal #video-player');
  
  // 重置视频源,避免缓存问题
  videoPlayer[0].pause();
  videoPlayer[0].src = promptUrl;
  videoPlayer[0].load();
  
  // 保留需要的播放属性(也可直接在HTML中定义)
  videoPlayer.attr({
     'autoplay': 'autoplay',
     'loop': 'loop',
     'muted': 'muted',
     'controls': 'controls',
     'playsinline': 'playsinline' 
  });

  // 视频描述建议放在模态框标题或单独区域,video标签的text不会显示
  // $('#modal-title').text(data.Description);
});

3. 额外验证点

  • 确保MP4文件为分段编码(fragmented MP4):非分段MP4在Safari中无法流式播放,可通过ffmpeg重新编码:
    ffmpeg -i input.mp4 -movflags frag_keyframe+empty_moov output_fragmented.mp4
    
  • 检查服务器是否允许自定义响应头,确保Accept-Ranges、Content-Range等头能正常返回。

原理说明

Safari视频播放依赖Range请求实现流式加载:它会先发送Range请求获取文件前几个字节解析视频元数据,再按需请求后续字节块。原代码未处理Range头,直接返回完整文件,导致Safari无法解析分段信息,从而抛出不支持的错误;修复后的代码正确处理Range请求,返回符合规范的响应,同时修正了字节读写的逻辑错误(原代码用buffer.Length而非实际读取的length,可能发送空字节)。

内容的提问来源于stack exchange,提问作者B.Spangenberg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 13:20:19