用JS輕松實現一個錄音、錄像、錄屏工具庫

大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列

前言

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

完整項目代碼放在 Github[2]

需求與思路

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

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

919e85ad22a616a66f26666d8c3523bb.png

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

3a3b71f1b9f1d83ffc7fff1b52a8c820.png

基礎功能

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

這里先把基礎的 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> 來查看錄音結果。

d73f0bf97dd887bf1f4ff6046488d9fe.png

之后來實現 開始停止

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?urlrecorder.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 在前端非常有用,比如上傳圖片時也可以調用它來實現圖片預覽,而不需要真的傳到后端才展示預覽圖片。

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

e4ebc0f9dd8733c42062ded8db684f21.png

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

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,可以開啟錄屏功能:

6e93cac7a3448add85d270206e14844b.png

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

6263499a71899c982fedb9de833f7ef3.png

上面把獲取媒體流的邏輯都扔在 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>
ba4200ffef0231b18558fcf6c6efaa69.png

禁音

最后,我們來實現禁音功能,原理也同樣簡單。拿到 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 關閉音軌來實現

這個小工具庫的實現就給大家帶到這里了,詳情可以查看 react-media-recorder[3] 這個庫的源碼,非常簡潔易懂,很適合入門看源碼的同學!

如果你也喜歡我的文章,可以點一波關注,或者一鍵三連再走,比心 ??

參考資料

[1]

react-media-recorder: https://github.com/0x006F/react-media-recorder

[2]

項目代碼: https://github.com/haixiangyan/react-media-recorder

[3]

react-media-recorder: https://github.com/0x006F/react-media-recorder

7e7b28308c7ff3e6b77ca84196854e17.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

7566719e670b5351c580129507dec08f.png

識別方二維碼加我微信、拉你進源碼共讀

今日話題

略。分享、收藏、點贊、在看我的文章就是對我最大的支持~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274847.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274847.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274847.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

文本字段和表單設計-UI組件系列

重點 (Top highlight)Forms have existed for a significant amount of time, greatly simplifying the task of drafting complaints and various other legal pleadings. With the advance of information and its processing, means to gather the data are also evolving. …

WCF 第四章 綁定 netMsmqBinding

MSMQ 為使用隊列創建分布式應用程序提供支持。WCF支持將MSMQ隊列作為netMsmqBinding綁定的底層傳輸協議的通信。 netMsmqBinding綁定允許客戶端直接把消息提交到一個隊列中同時服務端從隊列中讀取消息。客戶端和服務端之間沒有直接通信過程&#xff1b;因此&#xff0c;通信本 …

React 18 RC 版本發布啦,生產環境用起來!

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列今天給…

阿拉伯語排版設計_針對說阿拉伯語的用戶的測試和設計

阿拉伯語排版設計Let me start off with some data to put things into perspective “Why?”讓我從一些數據入手&#xff0c;以透視“為什么&#xff1f;”的觀點。 Arabic is the 5th most spoken language worldwide, with 420 million speakers, and is an official lang…

CMMI簡介

CMMI&#xff08;Capability Maturity Model Integration&#xff09;即能力成熟度模型集成 什么是CMMI CMMI是CMM模型的最新版本。早期的CMMI&#xff08;CMMI-SE/SW/IPPD&#xff09;1.02版本是應用于軟件業項目的管理方法&#xff0c;SEI在部分國家和地區開始推廣和試用。隨…

SVN:“SVN”不是內部命令,解決方法

1、安裝完TortoiseSVN-1.6.16.21511-x64-svn-1.6.17.msi 2、在運行窗口cmd---svn&#xff0c;提示&#xff1a; “SVN” 不是內部命令 郁悶&#xff0c;小有糾結 解決方法&#xff1a;安裝Slik-Subversion-1.6.17-x64.msi 命令行窗口關閉&#xff0c;再次打開命令行窗口&#x…

7個月,4000+人,500+源碼筆記,誠邀你參加源碼共讀~

大家好&#xff0c;我是若川。按照從易到難的順序&#xff0c;前面幾期&#xff08;比如&#xff1a;validate-npm-package-name、axios工具函數&#xff09;很多都只需要花2-3小時就能看完&#xff0c;并寫好筆記。但收獲確實很大。開闊視野、查漏補缺、升職加薪。已經有400筆…

火焰和煙霧的訓練圖像數據集_游戲開發者是煙霧和鏡子的大師

火焰和煙霧的訓練圖像數據集Video games are incredible. They transport us to new worlds, allow us to partake in otherwise impossible situations, and empower us in our every day lives. Games can make us feel like a part of something bigger than ourselves, per…

平衡樹SPLAY

一個比線段樹代碼還要又臭又長的數據結構&#xff0c;各式各樣的函數&#xff0c;咱也不知道別人怎么記住的&#xff0c;咱也不敢問 SPLAY的性質 1.某個節點的左子樹全部小于此節點&#xff0c;右子樹全部大于此節點 2.中序遍歷splay輸出的序列是按從小到大的順序 &#xff08;…

POJ 2696 計算表達式的值

時間限制: 1000ms內存限制:65536kB描述有些語言中表達式的運算符使用字符串表示&#xff0c;例如用mul代表*&#xff0c;用div代表/&#xff0c;用add代表&#xff0c;用sub代表-&#xff0c;用mod代表%。輸入第一行為表達式的個數n。其余n行每行一個表達式&#xff0c;表達式由…

為支持兩個語言版本,我基于谷歌翻譯API寫了一款自動翻譯的 webpack 插件

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列本文來…

全球 化 化_全球化設計

全球 化 化重點 (Top highlight)Designing for a global audience can feel daunting. Do you localize your product? Or, do you internationalize your product? And what does that even entail?為全球觀眾設計可能會令人生畏。 您是否將產品本地化&#xff1f; 還是您將…

springMVC_數據的處理過程

1、DispatcherServlet&#xff1a;作為前端控制器&#xff0c;負責分發客戶的請求到 Controller 其在web.xml中的配置如下&#xff1a; <servlet><servlet-name>dispatcherServlert</servlet-name><servlet-class>org.springframework.web.servlet.Dis…

面試體驗:Facebook 篇(轉)

http://www.cnblogs.com/cathsfz/archive/2012/11/05/facebook-interview-experience.html 2012-11-05 08:20 by Cat Chen, 23266閱讀, 121評論, 收藏, 編輯 Google、Microsoft 和 Yahoo 都是去年的事情了&#xff0c;接下來說說今年…

JavaScript 新增兩個原始數據類型

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列JavaS…

axure低保真原型_如何在Google表格中創建低保真原型

axure低保真原型Google Sheets is a spreadsheet, just like Microsoft Excel.Google表格是一個電子表格&#xff0c;就像Microsoft Excel一樣。 Most people associate it with calculating numbers. But Google Sheets is actually great for organizing your ideas, making…

Weblogic EJB 學習筆記(3)精

編輯實體bean的高級課程 1. 怎樣開發主健類 ejb的主健類主要用做持久存儲和ejb容器中的唯一標識符. 通常主健類的字段直接映射到數據庫中的主健字段. 如果主健只是由單個實體bean字段組成.且其數據類型是基本的java類.如string,則bean作者不必開發自定義的主健類. 只需要在配置…

Lerna 運行流程剖析

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列Lerna…

手動創建線程池 效果會更好_創建更好的,可訪問的焦點效果

手動創建線程池 效果會更好Most browsers has their own default, outline style for the :focus psuedo-class.大多數瀏覽器對于&#xff1a;focus psuedo-class具有其默認的輪廓樣式。 Chrome’s default outline styleChrome瀏覽器的默認輪廓樣式 This outline style is cr…

C++builder enum類型

C/C code #pragmaoption push -b-enumTThreadPriority { tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest, tpTimeCritical }; //這是字節型的.理論上說這是可能的最小整形.可以是1Byte, 2Bytes, 4Bytes...#pragmaoption pop#pragmaoption push -benumTThreadPriori…