1.背景
想通過(guò) web 錄制視頻,并將視頻上傳到后端要怎么實(shí)現(xiàn)呢勇劣?
2.整體思路
- 打開攝像頭 :MediaDevices.getUserMedia()
- 錄制:使用 MediaRecorder
- 內(nèi)存存儲(chǔ):創(chuàng)建一個(gè) [] 數(shù)組,存放字節(jié)潭枣,再轉(zhuǎn)成 blob 對(duì)象。
- 上傳:構(gòu)建file盆犁,再到 formData,使用ajax發(fā)起HTTP 請(qǐng)求
3.實(shí)現(xiàn)方式
獲得攝像頭設(shè)備
MediaDevices 接口提供訪問(wèn)連接媒體輸入的設(shè)備谐岁,如照相機(jī)和麥克風(fēng),以及屏幕共享等伊佃。它可以使你取得任何硬件資源的媒體數(shù)據(jù)窜司。
**MediaDevices.getUserMedia()**
會(huì)提示用戶給予使用媒體輸入的許可,媒體輸入會(huì)產(chǎn)生一個(gè)MediaStream
锭魔,里面包含了請(qǐng)求的媒體類型的軌道。此流可以包含一個(gè)視頻軌道(來(lái)自硬件或者虛擬視頻源迷捧,比如相機(jī)胀葱、視頻采集設(shè)備和屏幕共享服務(wù)等等)、一個(gè)音頻軌道(同樣來(lái)自硬件或虛擬音頻源笙蒙,比如麥克風(fēng)抵屿、A/D轉(zhuǎn)換器等等),也可能是其它軌道類型捅位。
在用戶通過(guò)提示允許的情況下轧葛,打開系統(tǒng)上的相機(jī)或屏幕共享和/或麥克風(fēng)艇搀,并提供 MediaStream
包含視頻軌道和/或音頻軌道的輸入。
MediaStream 接口是一個(gè)媒體內(nèi)容的流.焰雕。一個(gè)流包含幾個(gè)軌道,比如視頻和音頻軌道辟宗。
錄制
MediaRecorder() 構(gòu)造函數(shù)會(huì)創(chuàng)建一個(gè)對(duì)指定的 MediaStream 進(jìn)行錄制的 MediaRecorder 對(duì)象
var mediaRecorder = new MediaRecorder(stream[, options]);
...
if (navigator.mediaDevices.getUserMedia) {
var constraints = { audio: true, video: true };
var chunks = [];
var onSuccess = function(stream) {
var options = {
audioBitsPerSecond : 128000,
videoBitsPerSecond : 2500000,
mimeType : 'video/mp4'
}
var mediaRecorder = new MediaRecorder(stream,options);
m = mediaRecorder;
...
獲得錄制過(guò)程中的 數(shù)據(jù)
MediaRecorder.ondataavailable
調(diào)用它用來(lái)處理 dataavailable 事件, 該事件可用于獲取錄制的媒體資源 (在事件的 data 屬性中會(huì)提供一個(gè)可用的 Blob 對(duì)象.)
// 當(dāng)停止錄像以后的回調(diào)函數(shù)
_mediaRecorder.ondataavailable = function (data) {
console.log("# 產(chǎn)生錄制數(shù)據(jù)...");
console.log(data);
console.log("# ondataavailable, size = " + parseInt(data.size/1024) + "KB");
_chunks.push(data);
};
_mediaRecorder.onstop = function(e) {
console.log("# 錄制終止 ...");
const fullBlob = new Blob(_chunks);
const blobURL = window.URL.createObjectURL(fullBlob);
console.log("blob is ?, size="+parseInt(fullBlob.size/1024)+"KB. "); console.log(fullBlob);
console.log("blobURL =" + blobURL);
//saveFile(blobURL);
uploadFile(fullBlob);
}
從 blob 轉(zhuǎn)成 file
方法:
var myFile = new File(bits, name[, options]);
參數(shù)說(shuō)明:
bits: 一個(gè)包含ArrayBuffer吝秕,ArrayBufferView,Blob烁峭,或者 DOMString 對(duì)象的 Array — 或者任何這些對(duì)象的組合。這是 UTF-8 編碼的文件內(nèi)容则剃。
name: USVString,表示文件名稱,或者文件路徑镜遣。
示例:
var file = new File(["foo"], "foo.txt", {
type: "text/plain",
});
保存文件 到本地文件
生成一個(gè) A 標(biāo)簽,賦值 href 和 download 屬性即可悲关。
// 保存文件(產(chǎn)生下載的效果)
let saveFile = function(blob){
const link = document.createElement('a');
link.style.display = 'none';
link.href = blob;
link.download = 'media_.mp4';
document.body.appendChild(link);
link.click();
link.remove();
}
上傳到后端服務(wù)
- 創(chuàng)建file對(duì)象: file = new File([blob], "media_.mp4");
- 構(gòu)建成 fromData: var formData = new FormData();
- 再 Ajax 上傳即可。
let uploadFile = function(blob){
var file = new File([blob], "media_.mp4");
var formData = new FormData();
formData.append('file', file);
console.log(formData);
console.log("# 準(zhǔn)備上傳, fileName="+file.name +", size="+ parseInt(file.size/1024)+" KB");
let $output = $("#output");
$.ajax({
type:"POST",
url: "/uploadvideo",
data: formData,
processData: false,
contentType: false,
success:function (){
$output.prepend("上傳視頻成功!");
},
error : function() {
$output.prepend("上傳視頻失敗!");
}
});
}
判斷其 MIME 格式能否被客戶端錄制
MediaRecorder.isTypeSupported()方法會(huì)判斷其 MIME 格式能否被客戶端錄制艘绍。
var canRecord = MediaRecorder.isTypeSupported(mimeType)
"video/webm",
"audio/webm",
"video/webm\;codecs=vp8",
"video/webm\;codecs=daala",
"video/webm\;codecs=h264",
"audio/webm\;codecs=opus",
"video/mpeg"
關(guān)閉視頻流
遍歷 _mediaStream 秫筏,終止所有的 track 即可挎挖。
// 關(guān)閉流
var closeMediaStream = function(){
if(!_mediaStream) return;
console.log("# 關(guān)閉數(shù)據(jù)流");
_mediaStream.getTracks().forEach(function (track) {
track.stop();
});
_mediaStream = undefined;
_mediaRecorder = undefined;
}
后端代碼:接收視頻文件
創(chuàng)建一個(gè) controller 航夺,接收 MultipartFile 參數(shù),保存文件流即可阳掐。
/**
* @description:
* @author: zhangyunfei
* @date: 2021/4/25 19:22
*/
@RestController
@RequestMapping()
public class UploadFileController {
public static final String IMAGE_STORE_DIR = "/Users/zhangyunfei/git/demo/video2/public";
private static final String TAG = UploadFileController.class.getSimpleName();
@PostMapping(value = "/uploadvideo", name = "接收上傳文件")
public String uploadVideo(@RequestParam("file") MultipartFile uploadFile) {
String name = uploadFile.getOriginalFilename();
String size = String.format("%.2f MB", uploadFile.getSize() / 1024f / 1024f);
System.out.println(String.format("# name=%s, size=%s", name, size));
saveFile(uploadFile);
return "ok";
}
private void saveFile(MultipartFile uploadFile) {
try {
String fileName = UUID.randomUUID().toString() + ".mp4";
File path = new File(IMAGE_STORE_DIR, fileName);
uploadFile.transferTo(path);
LogUtil.d(TAG, "## 保存文件成功,路徑=%s", path.getPath());
} catch (IOException e) {
e.printStackTrace();
LogUtil.d(TAG, "## 保存文件失敗汛闸,%s", e);
}
}
}
后端設(shè)置:修改最大上傳文件大小和請(qǐng)求大小艺骂。
需要修改最大上傳文件大小和請(qǐng)求大小诸老,不然默認(rèn)的 1MB是不夠用的彻亲。
也很簡(jiǎn)單,修改配置文件即可苞尝。
spring:
servlet:
multipart:
max-file-size: 50MB #單個(gè)數(shù)據(jù)大小
max-request-size: 100MB #總數(shù)據(jù)大小
4.我的示例
示例指南見:http://www.reibang.com/p/052a7fecc358
代碼在 github: https://github.com/vir56k/demo/tree/master/video2
5.參考
Web API 接口參考
https://developer.mozilla.org/zh-CN/docs/Web/API
參考視頻示例:http://www.reibang.com/p/052a7fecc358
https://wendychengc.github.io/media-recorder-video-canvas/videocanvas.html
https://cloud.tencent.com/developer/article/1366886