整合營銷服務(wù)商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          用WebRTC和Node.js開發(fā)實時視頻聊天應(yīng)用

          不多說,我們直奔主題。這篇文章教大家如何編寫一個視頻聊天應(yīng)用,使已連接的兩用戶端能共享視頻和音頻。操作很簡單,非常適合JavaScript語言訓(xùn)練——更準(zhǔn)確地說是WebRTC技術(shù)和Node.js。

          什么是WebRTC?

          Web Real-Time Communications 網(wǎng)頁實時通信,簡稱WebRTC。WebRTC是一個HTML5規(guī)范,它允許用戶在瀏覽器之間直接進行實時通信,不需要任何第三方插件。WebRTC可用于多種情境(比如文件共享),但端對端實時音頻和視頻通信是其主要功能。本文將著重為大家介紹這兩項。

          WebRTC所做的就是允許接入設(shè)備。你可以借WebRTC來實時使用麥克風(fēng)、攝像頭和分享你的屏幕。

          所以,WebRTC可以用最簡單的方式在網(wǎng)頁中實現(xiàn)音頻和視頻通信。

          WebRTC JavaScript API

          WebRTC說起來很復(fù)雜,它涉及到很多技術(shù)。但建立連接、通信和傳輸數(shù)據(jù)的操作是通過一套JS API來實現(xiàn)的,還比較簡單。其中主要的API包括:

          RTCPeerConnection:創(chuàng)建和導(dǎo)航端對端連接。

          RTCSessionDescription:描述連接(或潛在連接)的一端,以及它的配置方式。

          navigator.getUserMedia:捕捉音頻和視頻。

          為什么選擇Node.js?

          若要在兩個或多個設(shè)備之間進行遠程連接,你就需要一個服務(wù)器。在這種情況下,你也需要一個處理實時通信的服務(wù)器。Node.js是為實時可擴展的應(yīng)用而構(gòu)建的。要開發(fā)自由數(shù)據(jù)交換的雙向連接應(yīng)用程序,你可能會用到WebSockets,它允許在客戶端和服務(wù)器之間建立一個會話窗口。來自客戶端的請求會以循環(huán)的方式,更準(zhǔn)確地說是事件循環(huán)進行處理,這時Node.js是我們很好的一個選擇,因為它采取 “非阻塞(non-blocking) “的方式來解決請求。這樣我們在這該過程中就能實現(xiàn)低延遲和高吞吐量。

          如果你對開發(fā)微服務(wù)感興趣的話,一定要看看查看我們內(nèi)含650多位微服務(wù)專家意見的2020年微服務(wù)狀態(tài)報告!

          思路拓展:我們要創(chuàng)建的是什么?

          我們會創(chuàng)建一個非常簡單的應(yīng)用程序,它能讓我們將音頻和視頻流傳輸?shù)竭B接的設(shè)備——一個基礎(chǔ)款視頻聊天應(yīng)用程序。我們會用到的技術(shù)有:

          Express庫,提供靜態(tài)文件,比如代表用戶界面(UI)的HTML文件;

          socket.io庫,在兩個設(shè)備之間用WebSockets建立連接;

          WebRTC,允許媒體設(shè)備(攝像頭和麥克風(fēng))在連接的設(shè)備之間傳輸音頻和視頻流。

          實現(xiàn)視頻會話

          我們要做的第一件事是給我們的應(yīng)用程序提供一個作為UI的HTML文件。讓我們通過運行:npm init.js來初始化新的node.js項目。然后,我們需要通過運行:npm i -D typescript ts-node nodemon @types/express @types/socket.io安裝一些開發(fā)依賴項,運行:npm i express socket.io安裝生產(chǎn)依賴項。

          之后我們就可以在package.json文件中定義腳本,來運行我們的項目了。

          {
           "scripts": {
             "start": "ts-node src/index.ts",
             "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
           },
           "devDependencies": {
             "@types/express": "^4.17.2",
             "@types/socket.io": "^2.1.4",
             "nodemon": "^1.19.4",
             "ts-node": "^8.4.1",
             "typescript": "^3.7.2"
           },
           "dependencies": {
             "express": "^4.17.1",
             "socket.io": "^2.3.0"
           }
          }

          當(dāng)我們運行npm run dev命令時,nodemon會監(jiān)控src文件夾中每個以.ts結(jié)尾的文件有無任何變化。現(xiàn)在,我們要創(chuàng)建一個src文件夾。在這個文件夾中,我們會創(chuàng)建兩個typescript文件:index.ts和server.ts。

          在server.ts中,我們會創(chuàng)建server類,并使其與express和socket.io一起工作。

          import express, { Application } from "express";
          import socketIO, { Server as SocketIOServer } from "socket.io";
          import { createServer, Server as HTTPServer } from "http";
           
          export class Server {
           private httpServer: HTTPServer;
           private app: Application;
           private io: SocketIOServer;
           
           private readonly DEFAULT_PORT = 5000;
           
           constructor() {
             this.initialize();
           
             this.handleRoutes();
             this.handleSocketConnection();
           }
           
           private initialize(): void {
             this.app = express();
             this.httpServer = createServer(this.app);
             this.io = socketIO(this.httpServer);
           }
           
           private handleRoutes(): void {
             this.app.get("/", (req, res) => {
               res.send(`<h1>Hello World</h1>`); 
             });
           }
           
           private handleSocketConnection(): void {
             this.io.on("connection", socket => {
               console.log("Socket connected.");
             });
           }
           
           public listen(callback: (port: number) => void): void {
             this.httpServer.listen(this.DEFAULT_PORT, () =>
               callback(this.DEFAULT_PORT)
             );
           }
          }

          為正常運行服務(wù)器,我們需要在index.ts文件中創(chuàng)建一個新的Server類實例并調(diào)用listen方法。

          import { Server } from "./server";
           
          const server = new Server();
           
          server.listen(port => {
           console.log(`Server is listening on http://localhost:${port}`);
          });

          現(xiàn)在,如果我們運行:npm run dev會看到下面這樣的情景:

          當(dāng)打開瀏覽器,輸入http://localhost:5000,我們應(yīng)該注意到左上的 “Hello World “信息。

          然后我們就可以在public/index.html中創(chuàng)建一個新的HTML文件了。

          <!DOCTYPE html>
          <html lang="en">
           <head>
             <meta charset="UTF-8" />
             <meta name="viewport" content="width=device-width, initial-scale=1.0" />
             <meta http-equiv="X-UA-Compatible" content="ie=edge" />
             <title>Dogeller</title>
             <link
               href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,700&display=swap"
               rel="stylesheet"
             />
             <link rel="stylesheet" href="./styles.css" />
             <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
           </head>
           <body>
             <div class="container">
               <header class="header">
                 <div class="logo-container">
                   <img src="./img/doge.png" alt="doge logo" class="logo-img" />
                   <h1 class="logo-text">
                     Doge<span class="logo-highlight">ller</span>
                   </h1>
                 </div>
               </header>
               <div class="content-container">
                 <div class="active-users-panel" id="active-user-container">
                   <h3 class="panel-title">Active Users:</h3>
                </div>
                 <div class="video-chat-container">
                   <h2 class="talk-info" id="talking-with-info"> 
                     Select active user on the left menu.
                   </h2>
                   <div class="video-container">
                     <video autoplay class="remote-video" id="remote-video"></video>
                     <video autoplay muted class="local-video" id="local-video"></video>
                   </div>
                 </div>
               </div>
             </div>
             <script src="./scripts/index.js"></script>
           </body>
          </html>

          在這個新文件中,我們創(chuàng)建了兩個視頻元素:一個用于遠程視頻連接,另一個用于本地視頻。你可能已經(jīng)注意到我們也在導(dǎo)入本地腳本了。現(xiàn)在我們就來創(chuàng)建一個新的文件夾“腳本”,并在這個目錄下創(chuàng)建index.js文件。至于樣式,你可以從GitHub庫中下載它們。

          接下來你需要給瀏覽器提供index.html。首先,你需要告訴express你想提供哪些靜態(tài)文件。為了實現(xiàn)這一點,我們決定在Server類中實現(xiàn)一個新方法。

          private configureApp(): void {
             this.app.use(express.static(path.join(__dirname, "../public")));
           }

          不要忘記在initialize中調(diào)用configureApp。

          private initialize(): void {
             this.app = express();
             this.httpServer = createServer(this.app);
             this.io = socketIO(this.httpServer);
           
             this.configureApp();
             this.handleSocketConnection();
           }

          當(dāng)你輸入http://localhost:5000后,你應(yīng)該能看到你的index.html文件在運行。

          下一步要實現(xiàn)的是允許攝像頭和視頻訪問并將其流式傳輸?shù)絣ocal-video元素。要做到這一點,你需要打開public/scripts/index.js文件,并用以下方法實現(xiàn)它。

          navigator.getUserMedia(
           { video: true, audio: true },
           stream => {
             const localVideo = document.getElementById("local-video");
             if (localVideo) {
               localVideo.srcObject = stream;
             }
           },
           error => {
             console.warn(error.message);
           }
          );

          當(dāng)回到瀏覽器時,界面會出現(xiàn)一個提示請求訪問你的媒體設(shè)備,在接受請求后,你電腦的攝像頭就開始工作了。

          更多細節(jié)詳見A simple guide to concurrency in Node.js and a few traps that come with it。

          如何處理socket連接?

          接下來我們講講如何處理socket連接。我們需要將客戶端與服務(wù)器連接起來。為此,我們將使用socket.io。在public/scripts/index.js中,添加以下代碼:

          this.io.on("connection", socket => {
               const existingSocket = this.activeSockets.find(
                 existingSocket => existingSocket === socket.id
               );
           
               if (!existingSocket) {
                 this.activeSockets.push(socket.id);
           
                 socket.emit("update-user-list", {
                   users: this.activeSockets.filter(
                     existingSocket => existingSocket !== socket.id
                   )
                 });
           
                 socket.broadcast.emit("update-user-list", {
                   users: [socket.id]
                 });
               }
             }

          頁面刷新后,電腦會彈出一條消息,顯示 “Socket已連接”

          然后我們回到server.ts中,把已連接的socket存儲在內(nèi)存中,這只是為了保留唯一連接。所以,我們需要在Server類中添加一個新的私有字段,如下:

          private activeSockets: string[] = [];

          然后我們需要在socket連接中檢查socket是否已經(jīng)存在。如果不存在,把新的socket推送到內(nèi)存中,并向已連接的用戶發(fā)送數(shù)據(jù)。

          this.io.on("connection", socket => {
               const existingSocket = this.activeSockets.find(
                 existingSocket => existingSocket === socket.id
               );
           
               if (!existingSocket) {
                 this.activeSockets.push(socket.id);
           
                 socket.emit("update-user-list", {
                   users: this.activeSockets.filter(
                     existingSocket => existingSocket !== socket.id
                   )
                 });
           
                 socket.broadcast.emit("update-user-list", {
                   users: [socket.id]
                 });
               }
             }

          你還需要在socket斷開連接時及時響應(yīng),所以在socket連接中,你需要添加:

          socket.on("disconnect", () => {
             this.activeSockets = this.activeSockets.filter(
               existingSocket => existingSocket !== socket.id
             );
             socket.broadcast.emit("remove-user", {
               socketId: socket.id
             });
           });

          客戶端(即public/scripts/index.js)這邊,你需要妥善處理那些信息:

          socket.on("update-user-list", ({ users }) => {
           updateUserList(users);
          });
           
          socket.on("remove-user", ({ socketId }) => {
           const elToRemove = document.getElementById(socketId);
           
           if (elToRemove) {
             elToRemove.remove();
           }
          });

          以下是 updateUserList 函數(shù):

          function updateUserList(socketIds) {
           const activeUserContainer = document.getElementById("active-user-container");
           
           socketIds.forEach(socketId => {
             const alreadyExistingUser = document.getElementById(socketId);
             if (!alreadyExistingUser) {
               const userContainerEl = createUserItemContainer(socketId);
               activeUserContainer.appendChild(userContainerEl);
             }
           });
          }
          以及createUserItemContainer函數(shù):
          function createUserItemContainer(socketId) {
           const userContainerEl = document.createElement("div");
           
           const usernameEl = document.createElement("p");
           
           userContainerEl.setAttribute("class", "active-user");
           userContainerEl.setAttribute("id", socketId);
           usernameEl.setAttribute("class", "username");
           usernameEl.innerHTML = `Socket: ${socketId}`;
           
           userContainerEl.appendChild(usernameEl);
           
           userContainerEl.addEventListener("click", () => {
             unselectUsersFromList();
             userContainerEl.setAttribute("class", "active-user active-user--selected");
             const talkingWithInfo = document.getElementById("talking-with-info");
             talkingWithInfo.innerHTML = `Talking with: "Socket: ${socketId}"`;
             callUser(socketId);
           }); 
           return userContainerEl;
          }

          需要注意的是,我們給用戶容器元素添加了一個可以調(diào)用callUser函數(shù)的點擊監(jiān)聽器——但現(xiàn)在,它可以是一個空的函數(shù)。接下來,當(dāng)運行兩個瀏覽器窗口(其中一個作為私人窗口)時,你應(yīng)該注意到你的Web應(yīng)用程序中有兩個已經(jīng)連接的socket。

          點擊列表中的活躍用戶,這時我們需要調(diào)用callUser函數(shù)。但是在實現(xiàn)之前,你還需要在window對象中聲明兩個類。

          const { RTCPeerConnection, RTCSessionDescription } = window;

          我們會在callUser函數(shù)用到這兩個類:

          async function callUser(socketId) {
           const offer = await peerConnection.createOffer();
           await peerConnection.setLocalDescription(new RTCSessionDescription(offer));
           
           socket.emit("call-user", {
             offer,
             to: socketId
           });
          }

          現(xiàn)在我們要創(chuàng)建一個本地請求并發(fā)送給選定的用戶。服務(wù)器會監(jiān)聽一個叫做call-user的事件、攔截請求并將其轉(zhuǎn)發(fā)給選定的用戶。讓我們用server.ts來實現(xiàn)該操作:

          socket.on("call-user", data => {
             socket.to(data.to).emit("call-made", {
               offer: data.offer,
               socket: socket.id
             });
           });

          對于客戶端,你需要就call-made事件作出調(diào)整:

          socket.on("call-made", async data => {
           await peerConnection.setRemoteDescription(
             new RTCSessionDescription(data.offer)
           );
           const answer = await peerConnection.createAnswer();
           await peerConnection.setLocalDescription(new RTCSessionDescription(answer));
           
           socket.emit("make-answer", {
             answer,
             to: data.socket
           });
          });

          之后,在你從服務(wù)器得到的請求上設(shè)置一個遠程描述,并為這個請求創(chuàng)建一個答復(fù)。對于服務(wù)器端,你只需要將適當(dāng)?shù)臄?shù)據(jù)傳遞給選定的用戶即可。然后我們再在server.ts里面添加一個監(jiān)聽器。

          socket.on("make-answer", data => {
             socket.to(data.to).emit("answer-made", {
               socket: socket.id,
               answer: data.answer
             });
           });
          對于客戶端,我們需要處理 answer-made 事件。
          socket.on("answer-made", async data => {
           await peerConnection.setRemoteDescription(
             new RTCSessionDescription(data.answer)
           );
           
           if (!isAlreadyCalling) {
             callUser(data.socket);
             isAlreadyCalling = true;
           }
          });

          我們可以使用標(biāo)志isAlreadyCalling,它能幫助確保我們只需調(diào)用一次用戶。

          最后你需要做的是添加本地軌道,包括音頻和視頻到你的連接端。只有做到這一點,我們才能夠與連接的用戶共享視頻和音頻。要做到這一點,我們需要在navigator.getMediaDevice回調(diào)中調(diào)用peerConnection對象的addTrack函數(shù)。

          navigator.getUserMedia(
           { video: true, audio: true },
           stream => {
             const localVideo = document.getElementById("local-video");
             if (localVideo) {
               localVideo.srcObject = stream;
             }
           
             stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
           },
           error => {
             console.warn(error.message);
           }
          );

          另外,我們還需要為ontrack事件添加一個適當(dāng)?shù)奶幚沓绦颉?/p>

          peerConnection.ontrack = function({ streams: [stream] }) {
           const remoteVideo = document.getElementById("remote-video");
           if (remoteVideo) {
             remoteVideo.srcObject = stream;
           }
          };

          如圖示,我們已經(jīng)從傳遞的對象中獲取了流,并改變了遠程視頻中的srcObject來使用接收到的流。所以現(xiàn)在當(dāng)你點擊活躍用戶后,你應(yīng)該建立一個視頻和音頻連接,像下圖這樣:

          欲了解細節(jié),請參閱:Node.js and dependency injection – friends or foes?

          現(xiàn)在你知道如何編寫一個視頻聊天應(yīng)用了吧!

          WebRTC是一個很大的話題,內(nèi)容非常龐雜。如果你想了解它的運作原理,就需要花很大功夫。幸運的是,我們可以訪問易于使用的JavaScript API,它可以幫助我們創(chuàng)建很簡潔的應(yīng)用程序,例如視頻共享、聊天應(yīng)用程序等等。

          如果你想深入了解WebRTC,點擊此WebRTC官方文檔的鏈接。另外,我也推薦你閱讀MDN的文檔說明,它能幫助你更加了解此技術(shù)。

          聞一

          中國移動公布中期業(yè)績:凈利潤606億元 同比增長5.6%


          8月11日,中國移動今日發(fā)布2016年中期財報。今年上半年,中國移動營收3704億元,同比增長7.1%,凈利潤606億元,同比增長5.6%。

          中國移動上半年通信服務(wù)收入為3254億元,同比增長6.9%,這一收入主要包括兩方面:1、語音收入1200億元,同比下滑14.2%;2、流量收入1950億元,同比增長26.7%,其中手機上網(wǎng)流量同比增長133.9%。

          用戶數(shù)方面,移動用戶達到8.37億戶,同比增長2.4%。具體到4G,新增4G基站超過20萬個,總計達132萬個,4G用戶總數(shù)達到4.29億,滲透率達到51.2%,4G網(wǎng)絡(luò)流量占比也提升到88%。寬帶方面,上半年凈增1081萬戶,總數(shù)達到6584萬戶。


          新聞二

          迅雷第二季度凈虧損200萬美元


          北京時間8月10日晚間消息,迅雷(Nasdaq:XNET)公布了截至6月30日的2016財年第二季度未經(jīng)審計財報。第二季度,迅雷總營收為3810萬美元,同比增長22.3%,環(huán)比下滑0.9%。


          凈虧損400萬美元,而上一財季凈虧損為540萬美元。不按照美國通用會計準(zhǔn)則,凈虧損200萬美元,而上一財季凈虧損290萬美元。截至2016年6月30日,迅雷所持有的現(xiàn)金、現(xiàn)金等價物和短期投資總額為3.91億美元,相比之下截至2015年12月31日為4.321億美元。


          業(yè)績展望:迅雷預(yù)計,2016財年第三季度總營收將達到3800萬美元至4200萬美元,中間值同比增長19.4%。


          新聞三

          谷歌新專利:用無人機搭建視頻會議系統(tǒng)


          據(jù)外媒報道,谷歌新獲得一項專利顯示,未來將用無人機搭建視頻會議系統(tǒng)。而且該設(shè)備將能解決視頻不流暢、相機角度不合適的問題,使用戶感覺合作伙伴與自己身處同一間辦公室

          ebCodecs 是什么


          • WebCodecs 是一個 Web 規(guī)范,21 年 9 月份在 Chrome 94 中實現(xiàn)
          • WebCodecs 提供訪問編解碼能力的接口,可精細控制音視頻數(shù)據(jù)


          Web 音視頻 API 存在什么問題


          音視頻技術(shù)在 Web 平臺上的應(yīng)用非常廣泛,已有許多 Web API 間接調(diào)用了編解碼器來實現(xiàn)特定功能:

          • 視頻播放:MSE
          • 音頻解碼:WebAudio
          • 錄制視頻:MediaRecorder
          • 實時流媒體:WebRTC

          但沒有方法可以靈活配置或直接訪問編解碼器,所以許多應(yīng)用使用 JS 或 WASM (比如 ffmpeg.wasm)來實現(xiàn)編解碼功能,盡管存在諸多缺陷或限制

          • 低性能(WebCodecs 編碼速度可達到 ffmpeg.wasm 的 20 倍)
          • 高功耗
          • 額外網(wǎng)絡(luò)開銷(下載已內(nèi)置的編解碼器)

          這么做的原因是以前的 Web API 在特定場景都存在難以克服的障礙

          • WebAudio 只能解碼完整的音頻文件,但不支持數(shù)據(jù)流、不提供解碼進度信息、更不支持編碼
          • MediaRecorder 只能錄制特定格式(WebM、MP4)的視頻,無法控制編碼速度、輸出緩沖區(qū)等
          • WebRTC 與 MediaStream API 高度耦合,且不透明,僅能用于實時音視頻通信
          • Video 標(biāo)簽、MSE 最常用于視頻播放,但無法控制解碼速率、緩沖區(qū)長度,且只支持播放部分視頻容器格式

          總結(jié):目前 API 在特定場景做到簡單、夠用,但無法實現(xiàn)高效且精細地控制


          WebCodecs 設(shè)計目標(biāo)


          • 流式傳輸:對遠程、磁盤資源進行流式輸入輸出
          • 效率:充分利用設(shè)備硬件,在 Worker 中運行
          • 組合性:與其他 Web API(如 Streams、WebTransport 和 WebAssembly)配合良好
          • 可恢復(fù)性:在出現(xiàn)問題時能夠恢復(fù)的能力(網(wǎng)絡(luò)不足、資源缺乏導(dǎo)致的幀下降等)
          • 靈活性:能適應(yīng)各種場景(硬實時、軟實時、非實時),能在此之上實現(xiàn)類似 MSE 或 WebRTC 的功能
          • 對稱性:編碼和解碼具有相似的模式


          非 WebCodecs 目標(biāo)


          • 視頻容器 封裝/解封裝 相關(guān) API
          • 在 JS 或 WASM 中實現(xiàn)編解碼器

          以上總結(jié)于 譯 WebCodecs 說明https://hughfenghen.github.io/posts/2023/10/02/webcodecs-explainer/),讓大家快速了解 WebCodecs API 的背景和目標(biāo)


          WebCodecs 能做什么


          WebCodecs API 介紹


          先了解 WebCodecs API 在視頻生產(chǎn)消費鏈路所處的位置



          由圖可知 WebCodecs API 提供的能力

          • 控制編解碼過程
          • 訪問編解碼前后的底層數(shù)據(jù)



          • VideoFrame、EncodedVideoChunk 對應(yīng)編碼前的源圖像和編碼后的壓縮數(shù)據(jù),兩者均提供獲取底層二進制數(shù)據(jù)的接口;
          • VideoEncoder、VideoDecoder 用于前兩者的類型轉(zhuǎn)換
          • Web 平臺所有表達圖像相關(guān)的類型,都可以轉(zhuǎn)換為 VideoFrame
          • 這里可以看到編碼、解碼過程在 API 設(shè)計上的對稱性
          • 圖像編解碼習(xí)得的知識,同樣可以對稱遷移到音頻編解碼



          以上就是 WebCodecs 提供的核心 API,新增 API 的數(shù)量非常少,主要難點在音視頻相關(guān)的背景知識。

          利用 mp4box.js 解封裝 mp4 文件,得到 EncodedVideoChunk 后給 WebCodecs 解碼,即可實現(xiàn) mp4 -> 圖像幀。



          WebCodecs 不涉及環(huán)節(jié)

          音視頻生產(chǎn)消費鏈路中,由其他 Web API 提供,包括:

          • 音視頻數(shù)據(jù)的采集與渲染
          • 封裝/解封裝
          • 傳輸、存儲


          相關(guān) Web API


          基于底層 API 可以構(gòu)建的基礎(chǔ)能力



          • WebAudio:音頻處理
          • WebGPU/WebGL + OffscreenCanvas:離屏圖像處理
          • OPFS:本地文件讀寫
          • WebWorker + WASM:密集型計算
          • WebTransport:低延遲網(wǎng)絡(luò)傳輸

          基于 Web 平臺已有的能力,加上 WebCodecs 提供的編解碼能力,能幫助開發(fā)者實現(xiàn)那些功能呢?


          DEMO 演示及實現(xiàn)


          WebCodecs 是相對底層 API,簡單功能可能也需要寫非常多的輔助代碼,可以借助 WebAV 封裝的工具函數(shù)來快速實現(xiàn)功能

          WebAV 基于 WebCodecs,提供簡單易用的 API 在瀏覽器中處理音視頻數(shù)據(jù)

          接下來演示 DEMO 效果以及基于 WebAV 的代碼實現(xiàn)

          1.可控解碼

          以設(shè)備最快的速度解碼一個 20s 的視頻,并將視頻幀繪制到 Canvas 上

          可控解碼的意義不只是它能實現(xiàn)超快速或逐幀播放視頻,而在于它能快速遍歷所有幀,這是視頻處理的基礎(chǔ)


          ,時長00:02


          首先從 WebAV 導(dǎo)出一個 MP4Clip 對象,它只需要一個 MP4文件 URL 進行初始化

          然后使用 tick 方法獲取到視頻幀,再繪制到 canvas 上

          while true 表示不做任何等待,所以到底有多快取決于網(wǎng)絡(luò)下載和設(shè)備解碼的速度


          import { MP4Clip } from '@webav/av-cliper'
           
          // 傳入一個 mp4 文件流即可初始化
          const clip = new MP4Clip((await fetch('<mp4 url>')).body)
          await clip.ready
           
          let time = 0
          // 最快速度渲染視頻所有幀
          while (true) {
            const { state, video: videoFrame } = await clip.tick(time)
            if (state === 'done') break
            if (videoFrame != null && state === 'success') {
              ctx.clearRect(0, 0, cvs.width, cvs.height)
              // 繪制到 Canvas
              ctx.drawImage(videoFrame, 0, 0, videoFrame.codedWidth, videoFrame.codedHeight)
              // 注意,用完立即 close
              videoFrame.close()
            }
            // 時間單位是 微秒,所以差不多每秒取 30 幀,丟掉多余的幀
            time += 33000
          }
          clip.destroy()


          2. 添加水印

          給視頻添加隨時間移動的半透明文字水印


          ,時長00:23


          先把文字轉(zhuǎn)換成圖片,這樣很容易借助 css 實現(xiàn)各種文字效果;

          然后控制圖片按照一定規(guī)則移動,這里省略了動畫的配置;

          動畫配置方法跟 css 的動畫幾乎是一樣的,只需提供 0%,50% 特定時機的坐標(biāo)就行了,WebAV 會自動計算出中間狀態(tài)的坐標(biāo)值,來實現(xiàn)動畫效果;

          最后將 MP4Clip 跟 ImgClip 合成輸出一個新的視頻流


          const spr1 = new OffscreenSprite(
            new MP4Clip((await fetch('<mp4 url>')).body)
          )
           
          const spr2 = new OffscreenSprite(
            new ImgClip('水印')
          )
          spr2.setAnimation(/* animation config */)
           
          const com = new Combinator()
           
          await com.add(spr1, { main: true })
          await com.add(spr2, { offset: 0 })
          // com.ouput() => 輸出視頻流


          3. 綠幕摳圖

          帶綠幕的數(shù)字人形象與背景圖片合成視頻,使用 WebGL 對每幀圖像進行處理,將人物背景修改為透明效果

          摳圖實現(xiàn)參考文章:WebGL Chromakey 實時綠幕摳圖https://hughfenghen.github.io/posts/2023/07/07/webgl-chromakey/


          ,時長00:06


          // 創(chuàng)建摳圖工具函數(shù)
          const chromakey = createChromakey(/* 綠幕摳圖配置 */)
          // 背景綠幕的測試視頻
          const clip = new MP4Clip((await fetch('<mp4 url>')).body)
          // MP4 的每一幀 都會經(jīng)過 tickInterceptor
          clip.tickInterceptor = async (_, tickRet) => {
            if (tickRet.video == null) return tickRet
            return {
              ...tickRet,
              // 摳圖之后再返回
              video: await chromakey(tickRet.video)
            }
          }


          4. 花影

          在瀏覽器中運行的視頻錄制工具,可用于視頻課程制作、直播推流工作臺

          視頻演示視頻課程制作的基本操作,包含 “添加攝像頭、分享屏幕、修改素材層級、剪切視頻片段、預(yù)覽導(dǎo)出視頻” 五個步驟


          ,時長00:53


          WebCodecs 的應(yīng)用場景


          應(yīng)用場景預(yù)測

          • 視頻生產(chǎn)
          • 視頻剪輯、直播工作臺,搭配多人協(xié)同、AI 能力
          • 視頻消費
          • 播放器、視頻會議、云游戲
          • 算力轉(zhuǎn)移
          • 視頻壓縮、縮略圖生成、植入水印、調(diào)整速率


          視頻生產(chǎn):從零到一


          由于缺失編碼能力,導(dǎo)致 Web 端少有視頻生產(chǎn)工具;

          現(xiàn)有的 Web 視頻剪輯工具都強依賴服務(wù)端能力支持,交互體驗存在優(yōu)化空間;

          在 Web 頁面借助 Canvas 制作動畫是非常簡單的,借助 WebCodecs 的編碼能力,現(xiàn)在就能將動畫快速保存為視頻。

          視頻裁剪、添加水印、內(nèi)嵌字幕等基礎(chǔ)視頻剪輯能力,沒有 WebCodecs 都是難以實現(xiàn)的,WebCodecs 將填補該領(lǐng)域的空白


          視頻消費:能力增強


          借助 HTMLMediaElement、MSE,Web 平臺的視頻消費應(yīng)用已經(jīng)非常成熟;

          以上 API 雖然簡單易用,但無法控制細節(jié),常有美中不足之感

          比如,緩沖延遲控制、逐幀播放、超快速播放、解碼控制等

          WebCodecs 將支持構(gòu)建更強、體驗更好的視頻消費應(yīng)用


          算力轉(zhuǎn)移:成本體驗雙贏


          目前 Web 使用的音視頻服務(wù),其處理過程都是在服務(wù)器上完成的

          比如,眾多在線視頻處理工具提供的:壓縮(降低分辨率、碼率)、水印、變速、預(yù)覽圖 功能

          處理流程:用戶上傳視頻 -> 服務(wù)器處理 -> 用戶下載視頻;
          整個過程消耗了服務(wù)器的計算成本、帶寬成本,用戶上傳下載的等待時間

          WebCodecs 能讓更多的任務(wù)在本地運行,不僅降低了服務(wù)運營成本,還能提升用戶體驗


          案例分享


          沒有 WebCodecs 以上的工具已經(jīng)存在了,為什么相信它們會應(yīng)用 WebCodecs?

          首先,有了 WebCodecs 之后這些工具能做到體驗更好、更便宜、迭代更快;

          再結(jié)合以往經(jīng)驗和 Web 平臺所具備優(yōu)勢,相信 WebCodecs 未來會得到廣泛應(yīng)用

          分享兩個例子

          1. 用戶視頻消費行為變化

          1. 荒蕪 時代
            用戶行為:下載電影然后離線觀看,裝機必備本地視頻播放器
          2. Flash 時代
            用戶行為:在線觀看視頻逐漸流行
          3. HTML5 時代
            用戶行為:PC 平臺 Web 在線觀看成為首選
          4. WebCodecs 時代
            補齊音視頻編解碼能力
            用戶行為:期待 WebCodecs 配合 AI 加多人協(xié)同,音視頻剪輯、視頻會議、直播推流等工具將逐漸 Web 化

          2. 富文本編輯

          Web 開放了幾個核心 API,讓大部分文字編輯轉(zhuǎn)移到線上,產(chǎn)生大量優(yōu)秀的知識管理應(yīng)用

          借助 Web 的易訪問性、搭配協(xié)同編輯,將生產(chǎn)溝通效率提升了一個等級

          • contenteditable:可編輯節(jié)點
          • Selection:選區(qū)
          • Range:文檔片段

          還有大量產(chǎn)品案例:Notion、Figma、VSCode...

          總結(jié):一旦 Web 平臺具備某個領(lǐng)域的基礎(chǔ)能力,相關(guān)產(chǎn)品不可避免的 Web 化


          WebCodecs 的優(yōu)勢與限制


          優(yōu)勢


          性能



          ffmpeg.wasm 最大的障礙就是性能問題,導(dǎo)致難以大規(guī)模應(yīng)用,主要是因為它不能使用硬件加速所以編解碼非常慢

          測試簡單的視頻編碼場景,WebCodecs 的性能是 ffmpeg.wasm 的 20 倍

          Web 平臺

          Web 平臺天然具有的優(yōu)勢:跨平臺、便捷性、迭代效率

          再加上底層能力越來越完善,已具備構(gòu)建大型、專業(yè)軟件的條件;

          相信 WebCodecs 也能憑借 Web 平臺的加持,獲得更大的應(yīng)用空間


          限制


          • 生態(tài)不成熟
          • 比如 缺少優(yōu)秀的 封裝/解封裝 工具包,支持容器格式有限
          • 兼容性
          • 舊版本瀏覽器不支持 WebCodecs
          • 受限于瀏覽器提供的編解碼器
          • 編解碼的可控參數(shù)不夠豐富(為了通用性不可避免的交換)
          • 暫無法自定義編解碼器

          生態(tài)不成熟只需要時間和更多開發(fā)者的積極參與,一般 to B 產(chǎn)品對兼容性會更寬容一些,to C 的產(chǎn)品可以降級到服務(wù)端實現(xiàn)

          比較麻煩的是 Web 平臺提供的編解碼器相對 Native 直接調(diào)用來說,還是有一些差距

          如果需要自定義編解碼器,或?qū)幗獯a器的參數(shù)配置有非常高的要求,技術(shù)方案選擇的時候需要慎重考慮 WebCodecs


          愿景


          • WebCodecs 成為 Web 平臺音視頻處理的基礎(chǔ);
          • WebCodecs 像 HTML5 一樣,促進音視頻在 Web 平臺的應(yīng)用和發(fā)展。


          附錄


          • WebAV 基于 WebCodecs 構(gòu)建的音視頻處理 SDK(https://github.com/hughfenghen/WebAV
          • 譯 WebCodecs 說明(https://hughfenghen.github.io/posts/2023/10/02/webcodecs-explainer/
          • Web 音視頻(零)概覽(https://hughfenghen.github.io/posts/2023/07/16/webav-0-overview/
          • WebAV DEMO(https://hughfenghen.github.io/WebAV/demo/1_1-decode-video
          • 花影 在瀏覽器中運行的視頻錄制工具(https://github.com/hughfenghen/bloom-shadow

          作者:劉俊

          來源:微信公眾號:嗶哩嗶哩技術(shù)

          出處:https://mp.weixin.qq.com/s/d28Xq9dticMdbO0s0N5MzA


          主站蜘蛛池模板: 精品中文字幕一区在线| 奇米精品视频一区二区三区| 无码中文字幕乱码一区| 精品一区二区三区高清免费观看 | 亚洲一区二区三区四区在线观看| 成人精品一区二区电影| 日韩国产精品无码一区二区三区 | 春暖花开亚洲性无区一区二区| 国产一区二区精品久久| 日韩社区一区二区三区| 亚洲AV无码一区二区三区久久精品| 日本一区二区不卡在线| 国产精品区AV一区二区| 国产成人av一区二区三区不卡| 在线精品视频一区二区| 久久一区二区明星换脸| 色狠狠一区二区三区香蕉蜜桃| 一区二区三区精品视频| 国产精品一区二区无线| 日本无码一区二区三区白峰美 | 香蕉视频一区二区三区| 后入内射国产一区二区| 国产精品va一区二区三区| 精品一区二区ww| 深田咏美AV一区二区三区| 99久久精品日本一区二区免费| 男女久久久国产一区二区三区| 成人一区二区免费视频| 中日韩一区二区三区| 精品国产亚洲一区二区在线观看 | 日韩电影一区二区三区| 国偷自产av一区二区三区| 成人精品一区二区三区不卡免费看 | 中文字幕一区视频| 人妻AV中文字幕一区二区三区| 性色AV一区二区三区| 国产suv精品一区二区33| 国产一区二区免费| 在线视频一区二区三区| 麻豆一区二区免费播放网站| 亚洲香蕉久久一区二区三区四区|