Wechat Mini | Note-6

微信小程序开发 Note-6

@ 2018年8月17日 22:51:33

上传短视频业务流程

用户选择视频(10s限制或即时拍摄);

BGM选择页面;

选择/不选择,并且输入描述(可为空);

上传(Controller);

保存视频截图(封面);

用户是否选择BGM;

​ -否——直接保存视频;

​ -是——合并视频和BGM成新视频,并保存;

用户选择视频

wx.chooseVideo(OBJECT)

拍摄视频或从手机相册中选视频,返回视频的临时文件路径。

OBJECT参数说明:

参数 类型 必填 说明
sourceType StringArray album 从相册选视频,camera 使用相机拍摄,默认为:[‘album’, ‘camera’]
compressed Boolead 是否压缩所选的视频源文件,默认值为true,需要压缩
maxDuration Number 拍摄视频最长拍摄时间,单位秒。最长支持 60 秒
success Function 接口调用成功,返回视频文件的临时文件路径,详见返回参数说明
fail Function 接口调用失败的回调函数
complete Function 接口调用结束的回调函数(调用成功、失败都会执行)
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
const app = getApp()
Page({
data: {
faceUrl: "../resource/images/noneface.png",
},
// 视频上传
uploadVideo: function() {
var me = this;
wx.chooseVideo({
sourceType: ['album'],
success: function(res) {
console.log(res);
var duration = res.duration;
var tempheight = res.height;
var tempwidth = res.width;
var tempVideoUrl = res.tempFilePath;
var tempCoverUrl = res.thumbTempFilePath;
if (duration > 11) {
wx.showToast({
title: '视频长度不能超过10秒',
icon: "none",
duration: 2500
});
} else if (duration < 3) {
wx.showToast({
title: '视频长度过短,请上传超过3秒以上的视频',
icon: "none",
duration: 2500
});
} else {
// TODO 打开BGM选择页面
}
}
});
}
})

后台BGM列表接口

1
2
3
4
5
6
7
8
9
10
11
12
// Service
@Service
public class BgmServiceImpl implements BgmService {
@Autowired
private BgmMapper bgmMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public List<Bgm> queryBgmList() {
return bgmMapper.selectAll();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Controller
@RestController
@Api(value = "BGM API", tags = {"BGM CONTROLLER"})
@RequestMapping("/bgm")
public class BgmController {
@Autowired
private BgmService bgmService;
@Autowired
private Sid sid;

@ApiOperation(value = "GET BGM LIST", notes = "GET BGM LIST API")
@PostMapping("/list")
public IMoocJSONResult list() {
return IMoocJSONResult.ok(bgmService.queryBgmList());
}
}

BGM页面联调 & 获取背景音乐列表

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
const app = getApp()
Page({
data: {
bgmList: [],
serverUrl: "",
},
onLoad: function() {
var me = this;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '加载背景音乐中...',
});
// BackEnd
wx.request({
url: serverUrl + '/bgm/list',
method: "POST",
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data);
wx.hideLoading();
if (res.data.status == 200) {
var bgmList = res.data.data;
me.setData({
bgmList: bgmList,
serverUrl: serverUrl
});
}
}
});
}
})
1
2
3
4
5
6
7
8
9
10
11
12
<view>
<form bindsubmit='upload'>
<radio-group name="bgmId">
<block wx:for="{{bgmList}}">
<view class='container'>
<audio name="{{item.name}}" author="{{item.author}}" src="{{serverUrl}}{{item.path}}" style="width:300px" id="{{item.id}}" controls loop></audio>
<radio style='margin-top:20px;' value=''></radio>
</view>
</block>
</radio-group>
</form>
</view>

开发上传短视频接口 & Swagger测试上传

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
// Controller
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController {

@ApiOperation(value = "UPLOAD VIDEO", notes = "UPLOAD VIDEO API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "bgmId", value = "背景音乐ID", dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "videoSeconds", value = "视频长度", required = true, dataType = "double", paramType = "query"),
@ApiImplicitParam(name = "videoWidth", value = "视频宽度", required = true, dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "videoHeight", value = "视频高度", required = true, dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "desc", value = "视频描述", dataType = "String", paramType = "query")
})
@PostMapping(value = "/upload",headers = "content-type=multipart/form-data")
public IMoocJSONResult upload(String userId, String bgmId,
double videoSeconds, int videoWidth, int videoHeight, String desc,
@ApiParam(value = "短视频",required = true) MultipartFile file) throws Exception {
if (StringUtils.isBlank(userId)) {
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
// 文件保存的命名空间
String fileSpace = "D:/xxx";
// 保存到数据库中的相对路径
String uploadPathDB = "/" + userId + "/video";

FileOutputStream fileOutputStream = null;
InputStream inputStream = null;

try {
if (file != null) {
String fileName = file.getOriginalFilename();
if (StringUtils.isNotBlank(fileName)) {
// 文件上传的最终保存绝对路径
String finalVideoPath = fileSpace + uploadPathDB + "/" + fileName;
// 设置数据库保存的路径
uploadPathDB += ("/" + fileName);

File outFile = new File(finalVideoPath);
if (outFile.getParentFile() != null || !outFile.getParentFile().isDirectory()) {
// 创建父文件夹
outFile.getParentFile().mkdirs();
}
fileOutputStream = new FileOutputStream(outFile);
inputStream = file.getInputStream();
IOUtils.copy(inputStream, fileOutputStream);
}
} else {
return IMoocJSONResult.errorMsg("视频上传错误");
}
} catch (Exception e) {
e.printStackTrace();
return IMoocJSONResult.errorMsg("视频上传错误");
} finally {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}
return IMoocJSONResult.ok();
}
}

视频临时参数传入下一个页面

1
2
3
4
5
6
7
8
// 打开BGM选择页面
wx.navigateTo({
url: '../chooseBgm/chooseBgm?duration=' + duration
+ "&tempHeight=" + tempHeight
+ "&tempWidth=" + tempWidth
+ "&tempVideoUrl=" + tempVideoUrl
+ "&tempCoverUrl=" + tempCoverUrl
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = getApp()
Page({
data: {
videoParams:{}
},
onLoad: function(params) {
console.log(params);
var me = this;
me.setData({
videoParams:params
});
},
// 在BGM选择页面,选择BGM后,上传视频
upload:function(e){
var me = this;
var bgmId = e.detail.value.bgmId;
var desc = e.detail.value.desc;
console.log(bgmId + "---" + desc);
}
})

小程序上传短视频联调

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
// Controler
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController {

@ApiOperation(value = "UPLOAD VIDEO", notes = "UPLOAD VIDEO API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "bgmId", value = "背景音乐ID", dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoSeconds", value = "视频长度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoWidth", value = "视频宽度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoHeight", value = "视频高度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "desc", value = "视频描述", dataType = "String", paramType = "form")
})
@PostMapping(value = "/upload",headers = "content-type=multipart/form-data")
public IMoocJSONResult upload(String userId, String bgmId,
double videoSeconds, int videoWidth, int videoHeight, String desc,
@ApiParam(value = "短视频",required = true) MultipartFile file) throws Exception {
if (StringUtils.isBlank(userId)) {
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
// 文件保存的命名空间
String fileSpace = "D:/videos_dev_face";
// 保存到数据库中的相对路径
String uploadPathDB = "/" + userId + "/video";

FileOutputStream fileOutputStream = null;
InputStream inputStream = null;

try {
if (file != null) {
String fileName = file.getOriginalFilename();
if (StringUtils.isNotBlank(fileName)) {
// 文件上传的最终保存绝对路径
String finalVideoPath = fileSpace + uploadPathDB + "/" + fileName;
// 设置数据库保存的路径
uploadPathDB += ("/" + fileName);

File outFile = new File(finalVideoPath);
if (outFile.getParentFile() != null || !outFile.getParentFile().isDirectory()) {
// 创建父文件夹
outFile.getParentFile().mkdirs();
}
fileOutputStream = new FileOutputStream(outFile);
inputStream = file.getInputStream();
IOUtils.copy(inputStream, fileOutputStream);
}
} else {
return IMoocJSONResult.errorMsg("视频上传错误");
}
} catch (Exception e) {
e.printStackTrace();
return IMoocJSONResult.errorMsg("视频上传错误");
} finally {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}

return IMoocJSONResult.ok();
}
}
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
const app = getApp()

Page({
data: {
bgmList: [],
serverUrl: "",
videoParams: {}
},
// 在BGM选择页面,选择BGM后,上传视频
upload: function(e) {
var me = this;
var bgmId = e.detail.value.bgmId;
var desc = e.detail.value.desc;
console.log(bgmId + "---" + desc);

var duration = me.data.videoParams.duration;
var tmpHeight = me.data.videoParams.tmpHeight;
var tmpWidth = me.data.videoParams.tmpWidth;
var tmpVideoUrl = me.data.videoParams.tmpVideoUrl;
var tmpCoverUrl = me.data.videoParams.tmpCoverUrl;

console.log(tmpHeight + "," + tmpWidth);
console.log(tmpVideoUrl);
console.log(tmpCoverUrl);

wx.showLoading({
title: '视频上传中',
});
// 上传短视频
var serverUrl = app.serverUrl;
wx.uploadFile({
url: serverUrl + "/video/upload",
formData: {
userId: app.userInfo.id,
bgmId: bgmId,
videoSeconds: duration,
videoWidth: tmpHeight,
videoHeight: tmpWidth,
desc: desc
},
filePath: tmpVideoUrl,
name: 'file',
header: {
'content-type': 'application/json'
},
success: function(res) {
// var data = JSON.parse(res.data);
console.log(res);
wx.hideLoading();
if (res.status == 200) {
wx.showToast({
title: '视频上传成功',
icon: 'success',
duration: 3000
});
}
}
});
}
})

FFMPEG

视音频处理工具;跨平台的视音频处理解决方案;

播放器(暴风);转码工具(格式工厂);直播、视频加码、滤镜、水印、特效;

1
ffmpeg -i input.mp4 output.avi

JAVA & FFMPEG

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
public class FFMPEGTest {
private String ffmpegEXE;

public FFMPEGTest(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}

public void convertor(String videoInputPath, String videoOutputPath) throws IOException {
// cmd执行命令
List<String> command = new ArrayList<>();
command.add(ffmpegEXE);
command.add("-i");
command.add(videoInputPath);
command.add(videoOutputPath);
// 建立进程cmd
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
// 关闭流,避免资源浪费
InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);
String line = "";
while((line = br.readLine()) != null){
}
if(br != null){
br.close();
}
if(inputStreamReader != null){
inputStreamReader.close();
}
if(errorStream != null){
errorStream.close();
}
}
public static void main(String[] args) {
// 执行.exe的路径
FFMPEGTest ffmpeg = new FFMPEGTest("D:\\FFMEPG\\bin\\ffmpeg.exe");
try {
ffmpeg.convertor("D:\\test.mp4", "D:\\done.avi");
} catch (IOException e) {
e.printStackTrace();
}
}
}

FFMPEG操作视频 & BGM

1
ffmpeg.exe -i test.mp4 -i bgm.mp3 -t 10 -y done.mp4

JAVA合并音视频

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
// 在JAVA & FFMPEG的基础上,进行修改即可
public class MergeVideoMp3 {
private String ffmpegEXE;

public MergeVideoMp3(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}

public void convertor(String videoInputPath, String mp3InputPath, double seconds, String videoOutputPath) throws IOException {
// cmd执行命令 ffmpeg.exe -i test.mp4 -i bgm.mp3 -t 10 -y done.mp4
List<String> command = new ArrayList<>();
command.add(ffmpegEXE);
command.add("-i");
command.add(videoInputPath);
command.add("-i");
command.add(mp3InputPath);
command.add("-t");
command.add(String.valueOf(seconds));
command.add("-y");
command.add(videoOutputPath);
...
}

public static void main(String[] args) {
// 执行.exe的路径
MergeVideoMp3 ffmpeg = new MergeVideoMp3("D:\\FFMEPG\\bin\\ffmpeg.exe");
try {
ffmpeg.convertor("D:\\test.mp4", "D:\\bgm.mp3", 10, "D:\\done.mp4");
} catch (IOException e) {
e.printStackTrace();
}
}
}

保存视频信息 & 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;
@Autowired
private Sid sid;
@Transactional(propagation = Propagation.REQUIRED)
@Override
public String saveVideo(Videos video) {
video.setId(sid.nextShort());
videosMapper.insertSelective(video);
return video.getId();
}
}
1
2
3
4
// Controller
// 具体代码与头像上传和保存到数据库基本类似;
// 代码过长,不进行发表;
// 详情看github;

上传封面图 & 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;

@Transactional(propagation = Propagation.REQUIRED)
@Override
public void updateVideo(String videoId, String coverPath) {
Videos video = new Videos();
video.setId(videoId);
video.setCoverPath(coverPath);
videosMapper.updateByPrimaryKeySelective(video);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Controller
// 具体代码与头像上传和保存到数据库基本类似;
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController extends BasicController {

@Autowired
private VideoService videoService;
@ApiOperation(value = "UPLOAD COVER PIC", notes = "UPLOAD COVER PIC API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoId", value = "视频ID", required = true, dataType = "String", paramType = "form")
})
@PostMapping(value = "/uploadCover", headers = "content-type=multipart/form-data")
public IMoocJSONResult uploadCover(String userId, String videoId, @ApiParam(value = "封面", required = true) MultipartFile file) throws Exception {
...
videoService.updateVideo(videoId,uploadPathDB);
return IMoocJSONResult.ok();
}
}

小程序上传视频业务流程联调

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
// 封面图上传 补充
const app = getApp()
Page({
data: {
bgmList: [],
serverUrl: "",
videoParams: {}
},
// 在BGM选择页面,选择BGM后,上传视频
upload: function(e) {

wx.showLoading({
title: '视频上传中',
});
// 上传短视频
...
success: function(res) {
var data = JSON.parse(res.data);
wx.hideLoading();
if (data.status == 200) {
var videoId = data.data;
// 封面图上传
wx.showLoading({
title: '封面图上传中',
});
wx.uploadFile({
url: serverUrl + "/video/uploadCover",
formData: {
userId: app.userInfo.id,
videoId: videoId
},
filePath: tmpCoverUrl,
name: 'file',
header: {
'content-type': 'application/json'
},
success: function(res) {
var data = JSON.parse(res.data);
wx.hideLoading();
if (data.status == 200) {
wx.showToast({
title: '上传成功',
icon: "success"
});
wx.navigateBack({
delta: 1,
});
} else {
wx.showToast({
title: '上传失败',
icon: "none"
});
}
}
});
} else {
wx.showToast({
title: '上传失败',
icon: "none"
});
}
}
});
}
})

联调手机端 & 坑

在使用手机端测试时,res所返回的数据与PC端返回的数据不同;

手机端所返回的数据中,不包含封面图的数据信息;

所以,才用FFMPEG生成截图的方式,生成封面图;

使用FFMPEG生成截图

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
public class FetchVideoCover {
// 视频路径
private String ffmpegEXE;

public void getCover(String videoInputPath, String coverOutputPath) throws IOException {
// ffmpeg.exe -ss 00:00:01 -i spring.mp4 -vframes 1 bb.jpg
List<String> command = new java.util.ArrayList<String>();
command.add(ffmpegEXE);

// 指定截取第1秒
command.add("-ss");
command.add("00:00:01");

command.add("-y");
command.add("-i");
command.add(videoInputPath);

command.add("-vframes");
command.add("1");

command.add(coverOutputPath);

ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();

InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);

String line = "";
while ((line = br.readLine()) != null) {
}

if (br != null) {
br.close();
}
if (inputStreamReader != null) {
inputStreamReader.close();
}
if (errorStream != null) {
errorStream.close();
}
}
}

上传视频流程 & 整合视频截图功能

上传流程联调

在上传视频的同时,制作封面图,并且将封面图的CoverPath存储到数据库即可;

就解决了PC和手机传回数据不同的问题,不再需要二次request的过程;

1
2
3
// 产生视频封面
FetchVideoCover videoInfo = new FetchVideoCover(FFMPEG_EXE);
videoInfo.getCover(finalVideoPath, FILE_SPACE + coverPathDB);

@ 2018年8月19日 19:47:47