IOS用H5播放语音(视频)流失败

前言

当前主要记录问题的发现过程以及解决过程,其中包含关键性的代码(也包含错误代码)

主线任务外的分析过程省略,但会展示部分的分析结果。

问题的发现

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);
/*获取 range*/
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. 使用第二个版本的代码

  1. 当前代码在安卓chrome等苹果外的设备测试正常,图片、音频(视频)展示正常,播放正常;
  2. 当打包IOSsafari浏览器中使用时,会出现两种现象:
    1. 视频在网络信号差时会缺少片段;
    2. 音频文件大于 50KB时,会出现异常情况,比如:全是杂音、播放出错;

5. 对苹果设备中的现象不断测试

经过长时间的测试,发现了不少现象:

  1. 本地访问不会报错(app与文件服务在同一台设备上);
  2. 音频大于 50KB时,会出现杂音、播放出错、时长不够、重复播放片段;
  3. 视频播放会缺少部分片段、时长不够;

6. 根据出现的现象,对现象进行分析

这些问题的出现,主要在app与文件服务是否在统一服务器上;

  1. 测试情况1app与文件服务部署在同一台服务器上;
  2. 测试情况2app与文件服务部署在不同服务器上;

针对这个相同服务器与不同服务器的请求进行分析,得出几种结论:

  1. 苹果外的设备会一次获取整个音频(视频)文件;
    1. header中的rangebytes=0-
  2. 苹果设备在测试情况1中,会发送两次请求获取文件:
    1. range 0-1请求当前文件是否存在。
    2. range 0-3000 请求当前文件(3000是文件大小,也就是header中的ContentLength)。
    3. 结果:音频(视频)可以正常播放;
  3. 苹果设备在测试情况2中,会发送多次请求获取文件:
    1. range 0-1请求当前文件是否存在。——请求正常返回
    2. range 0-3000 请求当前文件。——请求错误
    3. range 588-2563 分片请求当前文件。——请求正常返回
    4. range 2563-3000 分片请求当前文件。——请求正常返回
    5. 结果:会出现杂音、播放出错、时长不够、重复播放片段等现象

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中,会发送多次请求获取文件:

  1. range 0-1请求当前文件是否存在。——请求正常返回
  2. range 0-3000 请求当前文件。——请求错误
  3. range 588-2563 分片请求当前文件。——请求正常返回
  4. range 2563-3000 分片请求当前文件。——请求正常返回
  5. 结果:语音(视频)播放正常。

9. 针对正常结果进行分析

  1. 对于苹果设备的分片请求,没有正确的分片,会导致苹果设备获取到错误的片段,从而导致杂音。
  2. 苹果设备有一个分片是请求全部文件,但是报错了,不影响播放。

代码中的问题

这里的问题主要体现在文件分片的逻辑错误,在苹果设备外的其它设备上,是体现不出当前分片效果的。

本文地址: https://github.com/maxzhao-it/blog/post/40242/