使用HTML5 video标签+JavaBean+Servlet播放视频遇异常求助
问题排查与解决方案
先梳理下你的场景:你在JSF的.xhtml页面用HTML5 <video>标签,结合JavaBean和Servlet播放服务器端视频,但遇到了三个错误。我来逐个分析原因并给出修复方案:
你的代码片段
页面视频标签
<video controls="controls" style="margin-top: 60px;width: 70%;height: 80vh"> <source src="/SPortal/VideoReader" type="video/mp4" /> </video>
JavaBean代码
@PostConstruct public void prepeareMethod() { //check the type of learning object try { LearningObject lo = dao.getLearningObjByID(reqId); String learnObjType = lo.getLearningObjTyId().getLearningObjectType(); initialFile = new File(lo.getDirPath() + "\\" + lo.getFileName()); InputStream targetStream = new FileInputStream(initialFile); FacesContext facesContext = FacesContext.getCurrentInstance(); HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false); session.setAttribute("loBytesArray", IOUtils.toByteArray(targetStream)); } catch (Exception e) { e.printStackTrace(); } }
Servlet代码
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { byte[] pdfBytesArray = (byte[]) request.getSession().getAttribute("loBytesArray"); request.getSession().removeAttribute("loBytesArray"); response.setContentType("video/mp4"); response.setContentLength(pdfBytesArray.length); response.getOutputStream().write(pdfBytesArray); }
错误逐个分析与修复
1. Warning: Setting non-serializable attribute value into ViewMap: (key: lessonLearnBN, value class: com.beans.LessonLearnBN)
原因
JSF的ViewMap(视图范围的存储容器)要求存入的对象必须实现java.io.Serializable接口,你的LessonLearnBN类没有实现这个接口,导致JSF在尝试将其存入ViewMap时抛出警告。
修复方案
修改LessonLearnBN类,让它实现Serializable接口,建议同时添加序列化UID(可选但推荐,避免序列化版本冲突):
public class LessonLearnBN implements Serializable { // 你的类原有代码 // 添加序列化UID private static final long serialVersionUID = 1L; }
2. java.io.IOException: Connection closed / An established connection was aborted by the software in your host machine
原因
这个错误本质是浏览器(客户端)提前断开了与Servlet的连接,主要有两个核心诱因:
- 没有支持Range请求:HTML5的
<video>标签会发送Range请求头来分段加载视频(比如只请求视频的前10秒内容),但你的Servlet是一次性返回整个视频文件,不处理Range请求,浏览器会因为不符合预期而中断连接。 - Session存储大文件不合理:你把整个视频文件读入Session的字节数组中,既浪费服务器内存,又可能因为Session序列化(如果服务器有集群)或者传输超时导致连接中断。
修复方案
步骤1:重构JavaBean,不再把视频文件存入Session
只把视频的ID存入Session,或者直接在视频标签的请求中传递ID,让Servlet自己去查询文件路径:
@PostConstruct public void prepeareMethod() { try { LearningObject lo = dao.getLearningObjByID(reqId); String learnObjType = lo.getLearningObjTyId().getLearningObjectType(); // 只存视频ID到Session,不存整个文件 FacesContext facesContext = FacesContext.getCurrentInstance(); HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(false); session.setAttribute("currentLoId", reqId); } catch (Exception e) { e.printStackTrace(); } }
步骤2:修改视频标签,传递视频ID参数
<video controls="controls" style="margin-top: 60px;width: 70%;height: 80vh"> <source src="/SPortal/VideoReader?loId=#{lessonLearnBN.reqId}" type="video/mp4" /> </video>
步骤3:重构Servlet,支持Range请求并直接读取磁盘文件
这样既节省内存,又符合<video>标签的加载逻辑:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 从请求参数获取视频ID,查询文件路径 String loId = request.getParameter("loId"); if (loId == null || loId.isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } // 从数据库查询视频文件路径(这里复用你的LearningObject逻辑) LearningObject lo = dao.getLearningObjByID(Integer.parseInt(loId)); File videoFile = new File(lo.getDirPath() + "\\" + lo.getFileName()); if (!videoFile.exists()) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } long fileLength = videoFile.length(); long start = 0; long end = fileLength - 1; // 处理Range请求 String rangeHeader = request.getHeader("Range"); if (rangeHeader != null) { response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); String[] ranges = rangeHeader.split("=")[1].split("-"); start = Long.parseLong(ranges[0]); if (ranges.length > 1) { end = Long.parseLong(ranges[1]); } // 确保end不超过文件长度 end = Math.min(end, fileLength - 1); } long contentLength = end - start + 1; response.setContentType("video/mp4"); response.setHeader("Content-Length", String.valueOf(contentLength)); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength); response.setHeader("Accept-Ranges", "bytes"); // 分段读取文件并写入响应流,用try-with-resources自动关闭资源 try (RandomAccessFile raf = new RandomAccessFile(videoFile, "r"); OutputStream out = response.getOutputStream()) { raf.seek(start); byte[] buffer = new byte[4096]; // 4KB缓冲区,平衡性能与内存 long bytesWritten = 0; while (bytesWritten < contentLength) { int readBytes = raf.read(buffer); if (readBytes == -1) break; // 文件读取完毕 // 处理最后一段可能不足缓冲区大小的情况 if (bytesWritten + readBytes > contentLength) { readBytes = (int) (contentLength - bytesWritten); } out.write(buffer, 0, readBytes); bytesWritten += readBytes; } out.flush(); } catch (IOException e) { // 这里的异常大概率是客户端断开连接,属于正常情况,记录日志即可 System.out.println("视频加载时客户端断开连接:" + e.getMessage()); } }
3. Severe: Error Rendering View[/Error.xhtml] java.lang.IllegalStateException: getOutputStream() has already been called for this response
原因
这个错误是因为同一个HTTP响应中,程序先调用了response.getOutputStream()(你的Servlet写入视频流时),之后JSF尝试渲染Error.xhtml页面时又调用了response.getWriter(),两者不能同时使用,导致冲突。通常是Servlet抛出异常后,容器转发到Error页面,但此时响应流已经被打开。
修复方案
- 确保Servlet异常处理正确:在Servlet中如果发生异常,先设置响应状态码(比如
response.sendError(SC_INTERNAL_SERVER_ERROR)),不要继续写入响应流。 - 使用try-with-resources管理响应流:如上面的Servlet示例,用try-with-resources自动关闭输出流,避免资源泄漏。
- 检查Error.xhtml的配置:确保Error页面是标准的JSF页面,其后台Bean没有错误地操作响应流。
内容的提问来源于stack exchange,提问作者Yara Mousa
相关产品推荐
相关产品推荐

