記錄--用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
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...