記錄--用JS輕鬆實現一個錄音、錄像、錄屏的工具庫

来源:https://www.cnblogs.com/smileZAZ/archive/2022/11/30/16939444.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 最近項目遇到一個要在網頁上錄音的需求,在一波搜索後,發現了 react-media-recorder 這個庫。今天就跟大家一起研究一下這個庫的源碼吧,從 0 到 1 來實現一個 React 的錄音、錄像和錄屏的功能。 完整項目代碼放 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

前言

最近項目遇到一個要在網頁上錄音的需求,在一波搜索後,發現了 react-media-recorder 這個庫。今天就跟大家一起研究一下這個庫的源碼吧,從 0 到 1 來實現一個 React 的錄音、錄像和錄屏的功能。

完整項目代碼放在 Github

需求與思路

首先要明確我們要完成的事:錄音錄像錄屏

這種錄製媒體流的原理其實很簡單。

只需要記住:把輸入 stream 存放在 blobList,最後轉預覽 blobUrl

基礎功能

有了上面的簡單思路後,我們可以先做一個簡單的錄音與錄像功能。

這裡先把基礎的 HTML 結構實現了:

const App = () => {
  const [audioUrl, setAudioUrl] = useState<string>('');
  
  const startRecord = async () => {}

  const stopRecord = async () => {}

  return (
    <div>
      <h1>react 錄音</h1>

      <audio src={audioUrl} controls />

      <button onClick={startRecord}>開始</button>
      <button>暫停</button>
      <button>恢復</button>
      <button onClick={stopRecord}>停止</button>
    </div>
  );
}

上面有 開始暫停恢復 以及 停止 四個功能,還加加了一個 <audio> 來查看錄音結果。

之後來實現 開始 與 停止

const medisStream = useRef<MediaStream>();
const recorder = useRef<MediaRecorder>();
const mediaBlobs = useRef<Blob[]>([]);

// 開始
const startRecord = async () => {
  // 讀取輸入流
  medisStream.current = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
  // 生成 MediaRecorder 對象
  recorder.current = new MediaRecorder(medisStream.current);

  // 將 stream 轉成 blob 來存放
  recorder.current.ondataavailable = (blobEvent) => {
    mediaBlobs.current.push(blobEvent.data);
  }
  // 停止時生成預覽的 blob url
  recorder.current.onstop = () => {
    const blob = new Blob(mediaBlobs.current, { type: 'audio/wav' })
    const mediaUrl = URL.createObjectURL(blob);
    setAudioUrl(mediaUrl);
  }

  recorder.current?.start();
}

// 結束,不僅讓 MediaRecorder 停止,還要讓所有音軌停止
const stopRecord = async () => {
  recorder.current?.stop()
  medisStream.current?.getTracks().forEach((track) => track.stop());
}

從上面可以看到,首先從 getUserMedia 獲取輸入流 mediaStream,以後還可以打開 video: true 來同步獲取視頻流。

然後將 mediaStream 傳給 mediaRecorder,通過 ondataavailable 來存放當前流中的 blob 數據。

最後一步,調用 URL.createObjectURL 來生成預覽鏈接,這個 API 在前端非常有用,比如上傳圖片時也可以調用它來實現圖片預覽,而不需要真的傳到後端才展示預覽圖片。

在點擊 開始 後,就可以看到當前網頁正在錄音啦:

現在把剩下的 暫停 以及 恢復 也實現了:

const pauseRecord = async () => {
  mediaRecorder.current?.pause();
}

const resumeRecord = async () => {
  mediaRecorder.current?.resume()
}

Hooks

在實現簡單功能之後,我們來嘗試一下把上面的功能都封裝成 React Hook,首先把這些邏輯都扔在一個函數中,然後返回 API:

const useMediaRecorder = () => {
  const [mediaUrl, setMediaUrl] = useState<string>('');

  const mediaStream = useRef<MediaStream>();
  const mediaRecorder = useRef<MediaRecorder>();
  const mediaBlobs = useRef<Blob[]>([]);

  const startRecord = async () => {
    mediaStream.current = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
    mediaRecorder.current = new MediaRecorder(mediaStream.current);

    mediaRecorder.current.ondataavailable = (blobEvent) => {
      mediaBlobs.current.push(blobEvent.data);
    }
    mediaRecorder.current.onstop = () => {
      const blob = new Blob(mediaBlobs.current, { type: 'audio/wav' })
      const url = URL.createObjectURL(blob);
      setMediaUrl(url);
    }

    mediaRecorder.current?.start();
  }

  const pauseRecord = async () => {
    mediaRecorder.current?.pause();
  }

  const resumeRecord = async () => {
    mediaRecorder.current?.resume()
  }

  const stopRecord = async () => {
    mediaRecorder.current?.stop()
    mediaStream.current?.getTracks().forEach((track) => track.stop());
    mediaBlobs.current = [];
  }

  return {
    mediaUrl,
    startRecord,
    pauseRecord,
    resumeRecord,
    stopRecord,
  }
}
在 App.tsx 里拿到返回值就可以了:
const App = () => {
  const { mediaUrl, startRecord, resumeRecord, pauseRecord, stopRecord } = useMediaRecorder();

  return (
    <div>
      <h1>react 錄音</h1>

      <audio src={mediaUrl} controls />

      <button onClick={startRecord}>開始</button>
      <button onClick={pauseRecord}>暫停</button>
      <button onClick={resumeRecord}>恢復</button>
      <button onClick={stopRecord}>停止</button>
    </div>
  );
}

封裝好之後,現在就可以在這個 Hook 里添加更多的功能了。

清除數據

在生成 blob url 的時候我們調用了 URL.createObjectURL API 來實現,生成後的 url 長這樣:

blob:http://localhost:3000/e571f5b7-13bd-4c93-bc53-0c84049deb0a
複製代碼

每次 URL.createObjectURL 後都會生成一個 url -> blob 的引用,這樣的引用也是會占用資源記憶體的,所以我們可以提供一個方法來銷毀這個引用。

const useMediaRecorder = () => {
  const [mediaUrl, setMediaUrl] = useState<string>('');
  
  ...

  return {
    ...
    clearBlobUrl: () => {
      if (mediaUrl) {
        URL.revokeObjectURL(mediaUrl);
      }
      setMediaUrl('');
    }
  }
}

錄屏

上面錄音和錄像使用 getUserMedia 來實現,而 錄屏則需要調用 getDisplayMedia 這個介面來實現。

為了能更好地區分這兩種情況,可以給開發者提供 audio, video 以及 screen 三個參數,告訴我們應該調哪個介面去獲取對應的輸入流數據:

const useMediaRecorder = (params: Params) => {
  const {
    audio = true,
    video = false,
    screen = false,
    askPermissionOnMount = false,
  } = params;

  const [mediaUrl, setMediaUrl] = useState<string>('');

  const mediaStream = useRef<MediaStream>();
  const audioStream = useRef<MediaStream>();
  const mediaRecorder = useRef<MediaRecorder>();
  const mediaBlobs = useRef<Blob[]>([]);

  const getMediaStream = useCallback(async () => {
    if (screen) {
      // 錄屏介面
      mediaStream.current = await navigator.mediaDevices.getDisplayMedia({ video: true });
      mediaStream.current?.getTracks()[0].addEventListener('ended', () => {
        stopRecord()
      })
      if (audio) {
        // 添加音頻輸入流
        audioStream.current = await navigator.mediaDevices.getUserMedia({ audio: true })
        audioStream.current?.getAudioTracks().forEach(audioTrack => mediaStream.current?.addTrack(audioTrack));
      }
    } else {
      // 普通的錄像、錄音流
      mediaStream.current = await navigator.mediaDevices.getUserMedia(({ video, audio }))
    }
  }, [screen, video, audio])
  
  // 開始錄
  const startRecord = async () => {
    // 獲取流
    await getMediaStream();

    mediaRecorder.current = new MediaRecorder(mediaStream.current!);
    mediaRecorder.current.ondataavailable = (blobEvent) => {
      mediaBlobs.current.push(blobEvent.data);
    }
    mediaRecorder.current.onstop = () => {
      const [chunk] = mediaBlobs.current;
      const blobProperty: BlobPropertyBag = Object.assign(
        { type: chunk.type },
        video ? { type: 'video/mp4' } : { type: 'audio/wav' }
      );
      const blob = new Blob(mediaBlobs.current, blobProperty)
      const url = URL.createObjectURL(blob);
      setMediaUrl(url);
      onStop(url, mediaBlobs.current);
    }

    mediaRecorder.current?.start();
  }
  
  ...
}

由於我們已經允許用戶來錄視頻以及聲音,所以在生成 URL 時,也要設置對應的 blobProperty 來生成對應媒體類型的 blobUrl

最後在調用 hook 時傳入 screen: true,可以開啟錄屏功能:

註意:無論是錄像、錄音、錄屏都是要調用系統的能力,而網頁只是問瀏覽器要這個能力,但這樣的前提是瀏覽器已經擁有了系統許可權了,所以必須在系統設置里允許瀏覽器有這些許可權才能錄屏。

上面把獲取媒體流的邏輯都扔在 getMediaStream 函數里的做法,能很方便地用它來獲取用戶許可權,假如我們想在剛載入這個組件時就獲取用戶攝像頭、麥克風、錄屏許可權,就可以在 useEffect 里調用它

useEffect(() => {
  if (askPermissionOnMount) {
    getMediaStream().then();
  }
}, [audio, screen, video, getMediaStream, askPermissionOnMount])

預覽

錄像只需要在 getUserMedia 的時候設置 { video: true } 就可以實現錄像了。為了能更方便用戶在使用時能邊錄邊看效果,我們可以把視頻流也返回給用戶:

  return {
    ...
    getMediaStream: () => mediaStream.current,
    getAudioStream: () => audioStream.current
  }
用戶在拿到這些 mediaStream 之後就可以直接賦值到 srcObject 上來進行預覽了:
<button onClick={() => previewVideo.current!.srcObject = getMediaStream() || null}>
    預覽
</button>

禁音

最後,我們來實現禁音功能,原理也同樣簡單。拿到 audioStream 裡面的 audioTrack,再將它們設置 enabled = false 就可以了。

const toggleMute = (isMute: boolean) => {
  mediaStream.current?.getAudioTracks().forEach(track => track.enabled = !isMute);
  audioStream.current?.getAudioTracks().forEach(track => track.enabled = !isMute)
  setIsMuted(isMute);
}
使用時可以用它來禁用和開啟聲道:
<button onClick={() => toggleMute(!isMuted)}>{isMuted ? '打開聲音' : '禁音'}</button>

總結

上面用 WebRTC 的 API 簡單地實現了一個錄音、錄像、錄屏工具 Hook,這裡稍微做下總結吧:

  • getUserMedia 可用於獲取麥克風以及攝像頭的流
  • getDisplayMedia 則用於獲取屏幕的視頻、音頻流
  • 錄東西的本質是 stream -> blobList -> blob url,其中 MediaRecorder 可監聽 stream 從而獲取 blob 數據
  • MediaRecorder 還提供了開始、結束、暫停、恢復等多個與 Record 相關的介面
  • createObjectURLrevokeObjectURL 是反義詞,一個是創建引用,另一個是銷毀
  • 禁音可通過 track.enabled = false 關閉音軌來實現

本文轉載於:

https://juejin.cn/post/7071101341396893732

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux下用rm誤刪除文件的三種恢復方法 對於rm,很多人都有慘痛的教訓。我也遇到一次,一下午寫的程式就被rm掉了,幸好只是一個文件,第二天很快又重新寫了一遍。但是很多人可能就不像我這麼幸運了。本文收集了一些在Linux下恢復rm刪除的文件的方法,給大家作為參考。 1.幾點建議避免誤刪 首先,最好 ...
  • 大數據時代,資料庫 SaaS 是企業實現降本增效和業務創新的重要抓手。在騰訊全球數字生態大會資料庫 SaaS 專場上,騰訊雲發佈了多項資料庫 SaaS 產品能力升級,並重點分享了其在上雲、日常運維、資料庫遷移等多方面的實踐應用,為廣大企業構建和提升自身數據能力提供了有效參考。 騰訊雲資料庫副總經理羅 ...
  • 大量的數據科學職位需要精通 SQL,它也是數據分析師、數據科學家、數據建模崗最常考核的面試技能。在本篇內容中 ShowMeAI 將梳理彙總所有面試 SQL 問題,按照不同的主題構建練習專項塊。 ...
  • 閱識風雲是華為雲信息大咖,擅長將複雜信息多元化呈現,其出品的一張圖(雲圖說)、深入淺出的博文(雲小課)或短視頻(雲視廳)總有一款能讓您快速上手華為雲。更多精彩內容請單擊此處。 摘要:購買Redis實例時,實例類型有單機、主備、Proxy集群、Cluster集群和讀寫分離這麼多種,該怎麼選?別擔心,本 ...
  • TIS整合ChunJun實操 B站視頻: https://www.bilibili.com/video/BV1QM411z7w5/?spm_id_from=333.999.0.0 一、ChunJun 概述 ChunJun是一款易用、穩定、高效的批流統一的數據集成框架,可基於實時計算引擎Flink實現 ...
  • 當我們把介面都做好以後,我們需要去開發前端界面。 添加文章功能裡面,最重要的就是文章內容部分,需要配置上富文本編輯器,這樣才能給我們的內容增加樣式。 下載ueditor代碼 ueditor已經很久沒有更新了,我們現在去github下載壓縮好的代碼包 https://github.com/fex-te ...
  • 好家伙,本篇將繼續完善前端界面 效果展示: 1.註冊登陸 (後端已啟動) 2.註冊表單驗證 (前端實現的表單驗證) 在此之前: 我的第一個項目(二):使用Vue做一個登錄註冊界面 - 養肥胖虎 - 博客園 (cnblogs.com) 後端部分: 我的第一個項目(三):註冊登陸功能(後端) - 養肥胖 ...
  • Unhandled Runtime Error TypeError: Cannot read properties of null (reading '1') 錯誤再現 # 1. 安裝 next yarn add next # 2. 配置頁面 pages # 3. 啟動項目 ## 當啟動項目的時候, ...
一周排行
    -Advertisement-
    Play Games
  • 就像 Web Api 介面可以對入參進行驗證,避免用戶傳入非法的或者不符合我們預期的參數一樣,選項也可以對配置源的內容進行驗證,避免配置中的值與選項類中的屬性不對應或者不滿足預期,畢竟大部分配置都是通過字元串的方式,驗證是很有必要的。 1. 註解驗證 像入參驗證一樣,選項驗證也可以通過特性註解方便地 ...
  • 原文作者:aircraft 原文鏈接:https://www.cnblogs.com/DOMLX/p/17270107.html 加工的泛型類如下: using System; using System.Collections.Generic; using System.IO; using Syst ...
  • 在前一篇文章,我們瞭解瞭如何通過.NET6+Quartz開發基於控制台應用程式的定時任務,今天繼續在之前的基礎上,進一步講解基於ASP.NET Core MVC+Quartz實現定時任務的可視化管理頁面,僅供學習分享使用,如有不足之處,還請指正。 涉及知識點 Quartz組件,關於Quartz組件的 ...
  • 面向對象1 面向對象,更在乎的結果,而過程的實現並不重要 IDea快捷鍵(基礎版) | 快捷鍵 | 作用 | | | | | ctrl + / | 快捷註釋 | | ctrl + shift + / | 多行註釋 | | ctrl + d | 快速複製 | | ctrl + shift + up/d ...
  • NX中的checkmate功能是用於檢查模型、圖紙數據的工具,在UGOPEN中有例子。手動操作可以檢查已載入的裝配下所有零部件,可以設置通過後保存模型,檢查結果保存到Teamcenter中,預設保存在零組件版本下。 代碼中可以設置多個檢查規則。相關設置可以在用戶預設設置中進行設置。 1 // 2 / ...
  • JavaSE 運算符 算術運算符:+,-,*,/,%,++(自增),--(自減) i++:先用後+1;++i:先+1後用 賦值運算符:= 擴展賦值運算符:+=,-=,*=,/= a+=b >a=a+b: ​ 可讀性差,但是編譯效率高,且會自動進行類型轉換; ​ 當ab為基本數據類型時,a+b和b+a ...
  • 面向對象2 訪問修飾符 | | private | default | protected | public | | | | | | | | 當前類 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_che ...
  • 推薦一些學習qml教程 Qt官方的QML教程: https://doc.qt.io/qt-5/qtqml-index.html 這是一個由Qt官方提供的完整的QML教程,包含了所有基本知識和高級語法。 QML中文網:http://www.qmlcn.com/ 這是一個非常不錯的中文QML學習網站,提 ...
  • QAbstractBUtton: 所有按鈕控制項的基類 提供按鈕的通用功能 繼承自QWidget 屬於抽象類別,不能直接去使用,必須藉助於子類(除非你覺得子類不夠用,想自定義一個按鈕) 大部分功能之前已經使用過,在這裡只作簡單介紹 文本設置: setText(str) :設置按鈕提示文本 text() ...
  • 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇介紹 VLD 配置文件中配置項 StartDisabled 的使用方法。 ...