記錄--用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
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...