示例:H5中通過(guò) web 錄制視頻(攝像頭)并上傳

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

https://segmentfault.com/q/1010000011489899

http://www.zuidaima.com/blog/3819727543307264.htm

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宙址,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子抡砂,更是在濱河造成了極大的恐慌,老刑警劉巖注益,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異丑搔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)煮仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門谎仲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)浙垫,“玉大人,你說(shuō)我怎么就攤上這事夹姥。” “怎么了佃声?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)圾亏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)志鹃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任缰趋,我火速辦了婚禮,結(jié)果婚禮上秘血,老公的妹妹穿的比我還像新娘。我一直安慰自己灰粮,他們只是感情好忍坷,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佩研,像睡著了一般。 火紅的嫁衣襯著肌膚如雪旬薯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天绊序,我揣著相機(jī)與錄音,去河邊找鬼。 笑死蚂会,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胁住。 我是一名探鬼主播刊咳,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼儡司,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了捕犬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤柴钻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后贴届,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜡吧,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年昔善,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柬批。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袖订,死狀恐怖氮帐,靈堂內(nèi)的尸體忽然破棺而出洛姑,到底是詐尸還是另有隱情,我是刑警寧澤楞艾,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站蕴侧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏净宵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一择葡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧敏储,春花似錦、人聲如沸已添。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呛讲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贝搁,已是汗流浹背芽偏。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留污尉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓被碗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親锐朴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348