前言
当前主要记录问题的发现过程以及解决过程,其中包含关键性的代码(也包含错误代码)。
主线任务外的分析过程省略,但会展示部分的分析结果。
问题的发现
1. 使用文件服务
有一个文件服务,这个文件服务可以对接多种存储,所以我们封装了上传、下载文件的方法。
2. 调用文件服务接口
上传、下载文件是没什么问题的,问题出现在了使用 与 标签上。
标签的 src
指向音频(视频)文件的下载地址。
备注:如果这里不是通过下载地址返回文件流,下面的叙述就可以不用看了。
下载代码展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class FileSystemController { @GetMapping({"/{id}"}) public ResponseEntity<org.springframework.core.io.Resource> download(@PathVariable String id) throws IOException { org.springframework.core.io.Resource resource = this.fileServerService.downloadAttach(id); HttpHeaders headers = new HttpHeaders(); long contentLength = 0L; try { headers.add("Content-Disposition", "attachment;filename=" + resource.getFilename()); contentLength = resource.contentLength(); } catch (IOException var7) { log.error(var7.getMessage()); } return ((BodyBuilder) ResponseEntity.ok().headers(headers)).contentLength(contentLength).contentType(MediaType.parseMediaType("application/octet-stream")).body(resource); } }
|
此时会发现,所有的音频(视频)文件流 src
都是播放不了的。
如果仔细探究当前代码,会发现当前会返回”多余的结果”,由此猜测:这些”多余的结果”导致无法直接播放音频(视频),图片也是一样的道理。
3. 单独开发音频(视频)文件预览接口
第一个版本比较简陋,这里不做分析。
代码展示:第二个版本
第二个版本对音频(视频)文件做了分片处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| public class FileSystemController { @GetMapping({"/{id}"}) public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) { org.springframework.core.io.Resource resource; try { resource = this.fileServerService.downloadAttach(id); } catch (RuntimeException runtimeException) { log.error("当前获取的文件不存在 {} ", id); return; } try { response.setHeader("Content-Disposition", "attachment;filename=" + resource.getFilename()); String fileName = id.toLowerCase(); String extName = fileName.substring(fileName.lastIndexOf(".") + 1); String requestRange = request.getHeader("range"); long contentLength = resource.contentLength(); if (StringUtils.hasText(requestRange)) { String[] rangeLimit = requestRange.substring(6).split("-"); String rangeStart = rangeLimit[0]; if (!StringUtils.hasText(rangeStart)) { rangeStart = "0"; } String rangeEnd = rangeLimit.length == 2 ? rangeLimit[1] : ""; if (StringUtils.hasText(rangeEnd)) { response.setHeader("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + resource.contentLength()); contentLength = Integer.parseInt(rangeEnd) - Integer.parseInt(rangeStart) + 1; response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Last-Modified", new Date().toString()); } else { response.setHeader("Content-Range", "bytes " + rangeStart + "-" + resource.contentLength() + "/" + resource.contentLength()); } } else { response.setHeader("Content-Range", "bytes 0-" + resource.contentLength() + "/" + resource.contentLength()); } response.setContentLengthLong(contentLength); if (videoExtNames.contains(extName)) { response.setHeader("Content-Type", "video/" + extName); response.setContentType("video/" + extName); } else if (audioExtNames.contains(extName)) { response.setHeader("Content-Type", "audio/" + extName); response.setContentType("audio/" + extName); } else { response.setContentType("application/octet-stream"); } BufferedInputStream bufferedInputStream = new BufferedInputStream(resource.getInputStream()); byte[] b = new byte[bufferedInputStream.available()]; bufferedInputStream.read(b); ServletOutputStream outputStream = response.getOutputStream(); outputStream.write(b); outputStream.flush(); outputStream.close(); } catch (Exception e) { log.error("获取文件报错:{}", e.getMessage()); } } }
|
4. 使用第二个版本的代码
- 当前代码在安卓、chrome等苹果外的设备测试正常,图片、音频(视频)展示正常,播放正常;
- 当打包IOS和safari浏览器中使用时,会出现两种现象:
- 视频在网络信号差时会缺少片段;
- 音频文件大于
50KB
时,会出现异常情况,比如:全是杂音、播放出错;
5. 对苹果设备中的现象不断测试
经过长时间的测试,发现了不少现象:
- 本地访问不会报错(
app
与文件服务在同一台设备上);
- 音频大于
50KB
时,会出现杂音、播放出错、时长不够、重复播放片段;
- 视频播放会缺少部分片段、时长不够;
6. 根据出现的现象,对现象进行分析
这些问题的出现,主要在app
与文件服务是否在统一服务器上;
- 测试情况1:
app
与文件服务部署在同一台服务器上;
- 测试情况2:
app
与文件服务部署在不同服务器上;
针对这个相同服务器与不同服务器的请求进行分析,得出几种结论:
- 苹果外的设备会一次获取整个音频(视频)文件;
header
中的range
为 bytes=0-
;
- 苹果设备在测试情况1中,会发送两次请求获取文件:
range 0-1
请求当前文件是否存在。
range 0-3000
请求当前文件(3000
是文件大小,也就是header
中的ContentLength
)。
- 结果:音频(视频)可以正常播放;
- 苹果设备在测试情况2中,会发送多次请求获取文件:
range 0-1
请求当前文件是否存在。——请求正常返回
range 0-3000
请求当前文件。——请求错误
range 588-2563
分片请求当前文件。——请求正常返回
range 2563-3000
分片请求当前文件。——请求正常返回
- 结果:会出现杂音、播放出错、时长不够、重复播放片段等现象
7. 通过现象调整代码
经过一番风雨发现:
分片操作不对,在分片时,需要根据分片的大小,读取文件不同的片段。
改动后的代码:
1 2 3 4 5 6 7 8 9 10 11 12
| public class FileSystemController { public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) { if (StringUtils.hasText(requestRange)) { outputStream.write(b, (int) rangeStart, (int) contentLength); } else { outputStream.write(b); } } }
|
这里会把当前苹果设备请求的分片大小,写到输出流中。
8. 针对有问题的测试情况2进行测试
苹果设备在测试情况2中,会发送多次请求获取文件:
range 0-1
请求当前文件是否存在。——请求正常返回
range 0-3000
请求当前文件。——请求错误
range 588-2563
分片请求当前文件。——请求正常返回
range 2563-3000
分片请求当前文件。——请求正常返回
- 结果:语音(视频)播放正常。
9. 针对正常结果进行分析
- 对于苹果设备的分片请求,没有正确的分片,会导致苹果设备获取到错误的片段,从而导致杂音。
- 苹果设备有一个分片是请求全部文件,但是报错了,不影响播放。
代码中的问题
这里的问题主要体现在文件分片的逻辑错误,在苹果设备外的其它设备上,是体现不出当前分片效果的。
本文地址: https://github.com/maxzhao-it/blog/post/40242/