首页 游戏 软件 资讯 排行榜 专题
首页
前端开发
Html5通过数据流方式播放视频的实现

Html5通过数据流方式播放视频的实现

热心网友
20
转载
2026-04-28

跨平台H5视频流播放:打通PC、Android与iOS的全兼容方案

在开发需要兼容PC、Android和iOS的H5应用时,通过数据流播放服务端视频文件是个常见需求。这事听起来简单,但实际落地,尤其是要让所有平台都“买账”,还真得花点心思。今天,咱们就来捋一捋其中的关键。

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈

基础方案:HTML5 Video标签的常规用法

说到H5播放视频,大家第一反应肯定是






这里的 src 属性直接指向一个静态的视频文件路径。不过,实际业务中,视频文件往往需要通过后端接口动态获取,比如请求格式变成 getVideo.do?fileId=xxx 。这时候,服务端的处理逻辑就需要相应调整了。

服务端基础实现:文件读取与流式输出

最常见的后端实现,是读取本地文件,然后将字节流写入HTTP响应。代码大概长这样:

public void downFile(File downloadFile,
       HttpServletResponse response,
       HttpServletRequest request) throws Exception {
 response.reset();
 response.setContentType("video/mp4;charset=UTF-8");
   InputStream in = null;
 ServletOutputStream out = null;
 try {
   out = response.getOutputStream();
    in = new FileInputStream(downloadFile);
  if(in !=null){
    byte[] b = new byte[1024];
       int i = 0;
       while((i = in.read(b)) > 0){
      out.write(b, 0, i);
       }
       out.flush();
        in.close();
      }
 } catch (Exception e) {
     e.printStackTrace();
  }finally{
  if(in != null) {
     try { in.close(); } catch (IOException e) { }
     in = null;
    }
   if(out != null) {
     try { out.close(); } catch (IOException e) { }
     out = null;
    }
  }
}

这个方法在PC浏览器和大部分Android手机上都工作良好,问题出在iOS的Safari浏览器上——视频很可能无法播放。问题根源在于,iOS Safari的视频请求机制有点特殊。

iOS的“特殊要求”:Range请求与断点续传

原来,iOS Safari在请求视频时,默认会启用一种类似“断点续传”的机制。它并非一次性请求整个文件,而是分块请求。具体表现是,在请求头(Request Header)里会携带一个Range字段,比如第一次请求可能是:Range: ‘bytes=0-1’

这就要求服务端必须能够识别并正确处理这个Range头部:解析其字段值,然后精确返回所请求的字节范围数据。

相应地,在响应头(Response Header)里,我们至少要设置好三个关键字段:

  • Content-Type:明确指定视频格式,例如 "video/mp4", "video/ogg", "video/mov" 等。
  • Content-Range:格式必须为 "bytes -/"。其中 startend 必须与请求头中的 Range 字段对应,total 则是文件的总大小。
  • Content-Length:本次响应返回的二进制数据长度。

服务端升级:支持Range请求的完整实现

基于以上分析,我们需要将后端的下载方法升级为支持断点续传的版本。下面是一个详细的Ja va实现示例:

public void downRangeFile(File downloadFile,
        HttpServletResponse response,
        HttpServletRequest request) throws Exception {
 if (!downloadFile.exists()) {
  response.sendError(HttpServletResponse.SC_NOT_FOUND);
  return;
 }
 long fileLength = downloadFile.length();// 记录文件大小
   long pastLength = 0;// 记录已下载文件大小
   int rangeSwitch = 0;// 0:从头开始的全文下载;1:从某字节开始的下载(bytes=27000-);2:从某字节开始到某字节结束的下载(bytes=27000-39000)
   long contentLength = 0;// 客户端请求的字节总量
   String rangeBytes = "";// 记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容
   RandomAccessFile raf = null;// 负责读取数据
   OutputStream os = null;// 写出数据
   OutputStream out = null;// 缓冲
   int bsize = 1024;// 缓冲区大小
   byte b[] = new byte[bsize];// 暂存容器

   String range = request.getHeader("Range");
 int responseStatus = 206;
 if (range != null && range.trim().length() > 0 && !"null".equals(range)) {// 客户端请求的下载的文件块的开始字节
    responseStatus = ja vax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT;
  System.out.println("request.getHeader(\"Range\")=" + range);
  rangeBytes = range.replaceAll("bytes=", "");
  if (rangeBytes.endsWith("-")) {
   rangeSwitch = 1;
   rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
   pastLength = Long.parseLong(rangeBytes.trim());
   contentLength = fileLength - pastLength;
  } else {
   rangeSwitch = 2;
   String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-'));
   String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length());
   pastLength = Long.parseLong(temp0.trim());
  }
 } else {
  contentLength = fileLength;// 客户端要求全文下载
   }

 // 清除首部的空白行
   response.reset();
 // 告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-Ranges: bytes
   response.setHeader("Accept-Ranges", "bytes");
 // 如果是第一次下,还没有断点续传,状态是默认的 200,无需显式设置;响应的格式是:HTTP/1.1
   if (rangeSwitch != 0) {
  response.setStatus(responseStatus);
  // 不是从最开始下载,断点下载响应号为206
    // 响应的格式是:
    // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
    switch (rangeSwitch) {
   case 1: {
    String contentRange = new StringBuffer("bytes ")
      .append(new Long(pastLength).toString()).append("-")
      .append(new Long(fileLength - 1).toString())
      .append("/").append(new Long(fileLength).toString())
      .toString();
    response.setHeader("Content-Range", contentRange);
    break;
   }
   case 2: {
    String contentRange = range.replace("=", " ")
      + "/"
      + new Long(fileLength).toString();
    response.setHeader("Content-Range", contentRange);
    break;
   }
   default: {
    break;
   }
  }
 } else {
  String contentRange = new StringBuffer("bytes ").append("0-")
    .append(fileLength - 1).append("/").append(fileLength)
    .toString();
  response.setHeader("Content-Range", contentRange);
 }

 try {
  response.setContentType("video/mp4;charset=UTF-8");
   response.setHeader("Content-Length", String.valueOf(contentLength));
  os = response.getOutputStream();
  out = new BufferedOutputStream(os);
  raf = new RandomAccessFile(downloadFile, "r");
  try {
   long outLength = 0;// 实际输出字节数
     switch (rangeSwitch) {
    case 0: {
    }
    case 1: {
     raf.seek(pastLength);
     int n = 0;
     while ((n = raf.read(b)) != -1) {
      out.write(b, 0, n);
      outLength += n;
     }
     break;
    }
    case 2: {
     raf.seek(pastLength);
     int n = 0;
     long readLength = 0;// 记录已读字节数
       while (readLength <= contentLength - bsize) {// 大部分字节在这里读取
        n = raf.read(b);
      readLength += n;
      out.write(b, 0, n);
      outLength += n;
     }
     if (readLength <= contentLength) {// 余下的不足 1024 个字节在这里读取
        n = raf.read(b, 0, (int) (contentLength - readLength));
      out.write(b, 0, n);
      outLength += n;
     }
     break;
    }
    default: {
     break;
    }
   }
   System.out.println("Content-Length为:" + contentLength + ";实际输出字节数:" + outLength);
   out.flush();
  } catch (IOException ie) {
   // ignore
    }
 } catch (Exception e) {
  e.printStackTrace();
 } finally {
  if (out != null) {
   try {
    out.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
  if (raf != null) {
   try {
    raf.close();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
}

H5前端页面适配

前端页面基本无需大改,主要是确保标签的src指向我们新的支持断点续传的接口。为了更好的移动端体验,可以加上一些优化属性:







至此,一套完整的、兼容PC、Android和iOS的H5视频流播放方案就搭建完成了。核心就在于后端对HTTP Range请求头的兼容性处理。搞定这一点,跨平台视频播放的兼容性问题也就迎刃而解了。

来源:https://www.jb51.net/html5/773384.html
免责声明: 游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

相关攻略

Html5通过数据流方式播放视频的实现
前端开发
Html5通过数据流方式播放视频的实现

跨平台H5视频流播放:打通PC、Android与iOS的全兼容方案 在开发需要兼容PC、Android和iOS的H5应用时,通过数据流播放服务端视频文件是个常见需求。这事听起来简单,但实际落地,尤其是要让所有平台都“买账”,还真得花点心思。今天,咱们就来捋一捋其中的关键。 基础方案:HTML5 Vi

热心网友
04.28
在HTML5 localStorage中存储对象的示例代码
前端开发
在HTML5 localStorage中存储对象的示例代码

解决方案: 很多朋友在初次使用HTML5的localStorage时,都会遇到一个经典的“坑”:直接存进去一个Ja vaScript对象,取出来却变成了一串看不懂的字符。这不,上面这段代码就遇到了这个问题,明明存了个对象,取出来却成了“[object Object]”这个字符串。 如果你去翻看App

热心网友
04.28
如何在HTML5中利用NavigationPreload技术提升ServiceWorker启动性能
前端开发
如何在HTML5中利用NavigationPreload技术提升ServiceWorker启动性能

如何在HTML5中利用Na vigationPreload技术提升ServiceWorker启动性能 先澄清一个常见的误解:Na vigation Preload 的核心目标,并非直接“加快 Service Worker 的启动速度”。它的精妙之处在于,当浏览器在启动 Service Worker

热心网友
04.28
前端开发就业方向
前端开发
前端开发就业方向

前端开发就业方向详解:你适合哪条路? 互联网技术这张棋盘上,棋局早已今非昔比。如果你还停留在“前端就是切图”的老印象里,那可就跟不上版本了。今天的现代前端工程师,手上握着的是 HTML、CSS、Ja vaScript 这三板斧,脑子里转的却是工程化、组件化这套组合拳。技术栈越来越宽,职业路径也越来越

热心网友
04.26
《前端开发轻松上手》B站系列视频
前端开发
《前端开发轻松上手》B站系列视频

在B站开始更新《前端开发轻松上手》系列视频 是时候来点新玩意儿了。接下来,我们会依据一个清晰且久经考验的路径——没错,就是 HTML、CSS 再到 Ja vaScript 这个经典三步曲——来开启一段前端开发之旅。目标很简单:让你能真正轻松上手,把那些看似复杂的代码,变成手中游刃有余的工具。 光讲理

热心网友
04.26

最新APP

宝宝过生日
宝宝过生日
应用辅助 04-07
台球世界
台球世界
体育竞技 04-07
解绳子
解绳子
休闲益智 04-07
骑兵冲突
骑兵冲突
棋牌策略 04-07
三国真龙传
三国真龙传
角色扮演 04-07

热门推荐

MySQL视图如何处理自增主键映射_逻辑主键生成策略
数据库
MySQL视图如何处理自增主键映射_逻辑主键生成策略

MySQL视图自增主键映射与逻辑主键生成方案详解 在数据库设计与优化实践中,视图(View)是简化复杂查询、封装业务逻辑的强大工具。然而,许多开发者在操作视图时,常希望实现类似数据表的自动主键生成功能,这在实际应用中却面临诸多限制。本文将深入解析MySQL视图与自增主键的关系,并提供切实可行的逻辑主

热心网友
04.28
mysql数据库字符集如何统一调整_修改配置文件解决乱码问题
数据库
mysql数据库字符集如何统一调整_修改配置文件解决乱码问题

MySQL启动时默认字符集没生效?检查my cnf的加载顺序和位置 先明确一个关键点:MySQL启动时,并不会漫无目的地去读取所有可能的配置文件。它有一套固定的、按优先级排列的查找路径(通常是 etc my cnf、 etc mysql my cnf,最后才是 ~ my cnf),并且找到第一个

热心网友
04.28
如何建立基本医疗保险统筹基金和个人帐户
办公文书
如何建立基本医疗保险统筹基金和个人帐户

基本医疗保险的“双账户”模式:统筹与个人如何分工? 说起咱们的基本医疗保险,它的运作核心可以概括为“社会统筹与个人账户相结合”。简单来说,整个医保基金就像一个大池子,但这个池子被清晰地划分为两个部分:一个是大家共用的“统筹基金”,另一个则是属于参保人自己的“个人账户”。 那么,钱是怎么分别流入这两个

热心网友
04.28
如何定义记录类型_TYPE IS RECORD自定义多字段结构
数据库
如何定义记录类型_TYPE IS RECORD自定义多字段结构

TYPE IS RECORD 语法详解与核心应用指南 在PL SQL数据库编程中,TYPE IS RECORD是定义自定义复合数据类型的关键工具。其标准语法结构为:TYPE 类型名 IS RECORD (字段名 数据类型 [DEFAULT 默认值] [NOT NULL]);。通过该语法,开发者可以灵

热心网友
04.28
参保人可选择几家定点医疗机构
办公文书
参保人可选择几家定点医疗机构

在定点医疗机构的选择上,政策其实给参保人留出了不小的灵活空间。获得定点资格的专科和中医医疗机构,会自动成为统筹区内所有参保人的可选范围,这为大家获取特色医疗服务提供了基础保障。 在此之外,每位参保人还能根据自身需要,再额外挑选3到5家不同层次的医疗机构。比如,你可以选择一家综合三甲医院应对复杂病情,

热心网友
04.28