在《使用可插入流去除视频会议背景》文章中,我们向您展示了如何使用 MediaTrack API 的可插入流和WebCodecs API 的 VideoFrame 接口执行背景删除,作为 HTML Canvas 元素的替代方法。我们刚刚触及了完整的实时视频处理管道的表面,今天我们将扩展此功能。我们将增加处理视频流的能力,以便在广播应用中添加文本和叠加图像(甚至是二维码!)。
前提条件
此应用程序的代码可在 Github 上找到。如果您愿意,可以一边阅读一边编码,或者直接进入完成版本。您至少需要 NodeJS 16。
您可以使用git cli 工具下载代码,如下所示。
# Download code from the repo
git clone https://github.com/WebRTCventures/live-video-processing-demo
cd live-video-processing-demo
# If you plan to code along, switch to code-along branch
git checkout code-along
如果不想安装 Git,也可以使用存储库中可用的 zip 版本下载代码。如果您打算一起编码,请务必从code-along分支下载zip 文件。
一旦你下载了代码,请确保安装所有需要的依赖项。要做到这一点,打开终端窗口,导航到项目文件夹,运行 npm 安装命令,如下所示:
# navigate to project folder
cd route/to/live-video-processing-demo
# install dependencies
npm install
运行基础应用程序
首先,让我们熟悉一下我们将使用的基础点对点广播程序。这与代码共享分支中的程序相同。要运行它,打开一个终端程序,从项目文件夹中,使用 Node 运行 server.js 文件,如下所示:
# run the application
node server
接下来,在您喜欢的 Web 浏览器中打开http://localhost:3000/presenter.html。然后,再打开几个浏览器选项卡并导航到http://localhost:3000/viewer.html。您会看到一个用户在广播他/她的视频流,还有几个其他用户正在观看它。
此外,所有用户都可以使用右侧的控件聊天。
创建实时视频处理管道
现在您已经运行了应用程序,我们已准备好创建我们的实时视频处理管道。为此,让我们在公用文件夹下创建一个新文件track-utils.js。在那里,添加一个函数来创建一个已处理的视频轨道。
这个函数将类似于我们的背景清除帖子中描述的函数,有一对 MediaStreamTrackProcessor 和MediaStreamTrackGenerator 对象,它们将被用作管道的输入和输出,还有一个TransformStream对象,将作为转换器。
这个 TransformStream 对象接收一个transform
函数——所有魔法都在这里发生——作为参数以及将被处理的视频轨道。最后,我们将所有内容捆绑在一起并返回MediaStreamTrackGenerator
包含已处理视频轨道的对象。该函数的代码如下所示:
// public/track-utils.js
// function that creates a processed track
// it receives a track and a transform function
function createProcessedTrack({ track, transform }) {
// create MediaStreamTrackProcessor and MediaStreamTrackGenerator objects
const trackProcessor = new MediaStreamTrackProcessor({ track });
const trackGenerator = new MediaStreamTrackGenerator({ kind: track.kind });
// create the transformer object passing the transform function
const transformer = new TransformStream({ transform });
// connecting all together
trackProcessor.readable
.pipeThrough(transformer)
.pipeTo(trackGenerator.writable);
// returning the resulting track
return trackGenerator;
}
现在,我们需要创建一个transform
函数。该函数接收两个参数:视频帧和控制器。它处理每个视频帧,应用所需的操作,然后将其添加到控制器的队列中以创建新的处理轨道。
该功能的内容会根据所需的效果而有所不同,我们的目标是能够灵活地动态更改它。为了简化这个过程,我们将使用一个“转换函数工厂”,每次都会创建一个自定义转换函数。
让我们从一个非常小的转换函数开始,我们只需将当前视频帧传递给控制器而不做任何修改。我们将使用这个来“清除”我们所做的任何其他处理。
// public/track-utils.js
...
// our "clean" transform factory
function cleanStream() {
// it returns the actual transform function
return function transform(frame, controller) {
// for now, let's queue the current video frame
controller.enqueue(frame);
}
}
下一步是将此管道包含在演示者的视图中。为此,首先在presenter.html上添加新创建的脚本。然后,让我们创建一个存储所选transform
函数的变量,并更改其处理程序,getUserMedia
以便它在将视频轨道添加到视频元素之前对其进行处理。
诀窍是将获取的视频轨道与getUserMedia
我们从变换函数工厂获得的变换函数一起传递给我们的createProcessedTrack
. 我们通过使用调用存储在transformFn中的任何转换函数的匿名函数来实现这一点。
代码如下所示:
<!-- public/presenter.html -->
...
<script src="/socket.io/socket.io.js"></script>
<!-- adding the newly created script -->
<script src="track-utils.js"></script>
<script src="main.js"></script>
<script>
const localVideo = document.getElementById("localVideo");
// a variable to store the selected transformation
let transformFn;
navigator.mediaDevices
.getUserMedia({ audio: false, video: true })
.then((stream) => {
/* use the stream */
// create a transform function and assign it to transformFn variable
transformFn = cleanStream();
// start the video processing pipeline
const pTrack = createProcessedTrack({
track: stream.getVideoTracks()[0],
transform: (frame, controller) => transformFn(frame, controller)
});
// add the processed track to video element in a new stream
localVideo.srcObject = new MediaStream([pTrack]);
styleVideos();
startSignaling('p');
})
...
试着刷新presenter视图(如果你已经关闭了它,则再次打开它)。虽然看起来什么也没有发生,但我们的视频处理管道正在幕后工作
添加文字
为了让它更有趣,让我们为演示者添加一种在视频上显示文本公告的方法。为此,我们将创建一个新的转换函数工厂,用于设置必要的组件并提供所需的transform
功能。
此函数将视频流与移动文本叠加层相结合,并在 中呈现最终结果OffscreenCanvas
,然后使用 WebCodecs API 的接口将其用于创建新的视频帧VideoFrame
。
// public/text-utils.js
...
// a customizable transform function factory for adding text
// let's add some default values
function showText({
text,
txtInitialX,
txtColor = 'white',
txtFontSize = '48px',
txtFont = 'serif',
textSpeed = 2,
bgColor = '#08b9a6',
bgPadding = 10,
position = 'top'
}) {
// an ofscreencanvas for drawing video frame and text
const canvas = new OffscreenCanvas(1, 1);
const ctx = canvas.getContext('2d');
// some values for text size and x position in the canvas
const intTxtFontSize = parseInt(txtFontSize);
let x = txtInitialX;
// the transform function
return function transform(frame, controller) {
// set canvas size same as the video frame
const width = frame.displayWidth;
const height = frame.displayHeight;
canvas.width = width;
canvas.height = height;
// determine position of the text based on the params
const bgHeight = intTxtFontSize + bgPadding;
const bgPositionY = position === 'bottom'
? height - (intTxtFontSize + bgPadding + 5)
: 5;
const txtPositionY = position === 'bottom'
? height - (Math.floor(bgPadding / 2) + 10)
: 5 + intTxtFontSize;
// let's draw!
ctx.clearRect(0, 0, width, height);
ctx.drawImage(frame, 0, 0, width, height);
ctx.font = txtFontSize + ' ' + txtFont;
ctx.fillStyle = bgColor;
ctx.fillRect(0, bgPositionY, width, bgHeight)
ctx.fillStyle = txtColor;
ctx.fillText(text, x, txtPositionY);
// move the x position of the text
x -= textSpeed;
// restart the position after it leaves the screen
if (x <= (0 - 100 - text.length * 20)) {
x = width
}
// create a new frame based on the content of the canvas
const newFrame = new VideoFrame(canvas, { timestamp: frame.timestamp });
// close the current frame
frame.close();
// enqueue the new one
controller.enqueue(newFrame);
}
}
下一步是从 UI 调用这个新的变换函数工厂并更新变量的值transformFn
,以便管道相应地处理视频轨道。此外,我们将包括一个额外的文本框供用户输入他们的公告和一个按钮来显示它们。
当我们这样做的时候,让我们也添加一种方法让用户清除消息并返回到原始流。或者我们应该说,应用我们之前添加的“干净”转换。
<!-- public/presenter.html -->
...
<div id="chat" class="chat">
<!-- adding the new UI elements -->
<input id="announcement" type="text" placeholder="Make an announcement..." />
<button id="announcementBtn">Announce</button>
<button id="clsAnnouncementBtn">Clear</button>
...
</div>
...
<script>
...
let transformFn;
// get references from html elements
const announcement = document.getElementById('announcement');
const announcementBtn = document.getElementById('announcementBtn');
const clsAnnouncementBtn = document.getElementById('clsAnnouncementBtn');
...
// an event for adding the announcement
announcementBtn.addEventListener('click', () => {
// update transformFn with transform function from showText factory
// use current video element stream width to calculate initial x position
transformFn = showText({
text: announcement.value,
txtInitialX: localVideo.
srcObject.
getVideoTracks()[0].
getSettings()['width']
});
});
// an event for "cleaning" the stream
clsAnnouncementBtn.addEventListener('click', () => {
transformFn = cleanStream();
announcement.value = '';
});
</script>
看看我们的前两个transform
函数了!再次打开应用程序,现在尝试发送公告。您应该会看到类似下图的内容。
现在的广播应用具有在视频流之上添加文本的能力。
你也应该能够清除视频流,这与改变为不进行任何处理的转换函数是一样的。
添加叠加图像
让我们为我们的管道引入一个新功能:将图像叠加到视频流上的能力。为此,我们将创建另一个转换函数工厂。在这里,我们将利用File API读取用户提供的图像,然后利用 OffscreenCanvas
组合所有元素。
和以前一样,我们将使用 WebCodecs API 的 VideoFrame 接口为处理过的轨道生成视频帧。
<!-- public/presenter.html -->
...
<div id="chat" class="chat">
<!-- adding the new UI elements -->
<input id="announcement" type="text" placeholder="Make an announcement..." />
<button id="announcementBtn">Announce</button>
<button id="clsAnnouncementBtn">Clear</button>
...
</div>
...
<script>
...
let transformFn;
// get references from html elements
const announcement = document.getElementById('announcement');
const announcementBtn = document.getElementById('announcementBtn');
const clsAnnouncementBtn = document.getElementById('clsAnnouncementBtn');
...
// an event for adding the announcement
announcementBtn.addEventListener('click', () => {
// update transformFn with transform function from showText factory
// use current video element stream width to calculate initial x position
transformFn = showText({
text: announcement.value,
txtInitialX: localVideo.
srcObject.
getVideoTracks()[0].
getSettings()['width']
});
});
// an event for "cleaning" the stream
clsAnnouncementBtn.addEventListener('click', () => {
transformFn = cleanStream();
announcement.value = '';
});
</script>
接下来,添加一个输入元素,用户可以在其中添加图像。我们还需要一个事件侦听器,在添加图像后创建一个新函数transform
。
<!-- public/presenter.html -->
...
<div id="chat" class="chat">
<input type="file" id="image" placeholder="Add an image..." accept="image/*" />
<input id="announcement" type="text" placeholder="Make an announcement..." />
...
<script>
...
const image = document.getElementById('image');
let transformFn;
...
image.addEventListener('change', () => {
transformFn = showImage({image: image.files[0]});
announcement.value = '';
});
...
再次打开应用程序并尝试添加图像。您应该会看到类似于下图的内容:
添加二维码
这种新的超能力感觉很棒,不是吗?你还能想到什么样的现场视频处理?二维码呢?这些是与他人分享信息的便捷方式,我们可以使用我们的管道来轻松地将它们添加到我们的应用程序中。
首先,我们需要生成一个二维码。让我们将QRCode.js二维码生成器库添加到代码中。这就像将以下行添加到presenter.html文件一样简单。
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js">
</script>
然后,让我们创建一个转换函数工厂。它将类似于我们为添加图像而创建的那个。但是我们将使用刚刚添加的第三方库生成一个新的 QR 码,而不是读取图像。转换函数工厂的代码如下所示:
// public/text-utils.js
...
// a customizable transform function factory for adding QR codes
// let's add some default values
function showQr({
text,
qrWidth = 256,
qrHeight = 256,
colorDark = '#000000',
colorLight = '#FFFFFF',
positionX = 10,
positionY = 10
}) {
// a canvas for bundling everything together
const canvas = new OffscreenCanvas(1, 2);
const ctx = canvas.getContext('2d');
// a div element for hosting the QR code
const qrDiv = document.createElement('div');
// generating a new QR code on the qrDiv element
new QRCode(qrDiv, {
text,
width: qrWidth,
height: qrHeight,
colorDark,
colorLight
});
// the transform function
return function transform(frame, controller) {
// setting canvas size same as the video frame
const width = frame.displayWidth;
const height = frame.displayHeight;
canvas.width = width;
canvas.height = height;
// drawing the current video frame and QR code
ctx.clearRect(0, 0, width, height);
ctx.drawImage(frame, 0, 0, width, height);
ctx.drawImage(
qrDiv.querySelector('canvas'),
positionX,
positionY,
qrWidth,
qrHeight
);
// get the current video frame timestamp before closing it
const timestamp = frame.timestamp;
// close the current video frame
frame.close();
// create a new video frame based on the content of the canvas
const newFrame = new VideoFrame(canvas, { timestamp });
// enqueue the new video frame
controller.enqueue(newFrame);
}
}
对于 UI,让我们重用公告文本输入并添加一个额外的按钮,用于根据此类输入的内容生成 QR 码。最后,让我们为这个新按钮添加一个事件侦听器以更新选定的转换:
<!-- public/presenter.html -->
...
<input id="announcement" type="text" placeholder="Make an anoouncement..." />
<button id="announcementBtn">Announce</button>
<button id="qrBtn">Make QR</button>
<button id="clsAnnouncementBtn">Clear</button>
...
<script>
...
const qrBtn = document.getElementById('qrBtn');
let transformFn;
...
qrBtn.addEventListener('click', () => {
transformFn = showQr({ text: announcement.value });
image.value = '';
});
...
让我们再次打开该应用程序并观看显示二维码的视频。
结论
用于 MediaStreamTrack 和 WebCodecs API 的可插入流提供了一种简单直接的方法来为实时通信应用程序实施实时视频处理管道。这使您能够整合强大的功能,例如添加文本、叠加图像,甚至二维码,从而大大提高应用程序的价值。请留意未来的帖子,我们将在其中探索这些功能的真实实现。敬请期待更多精彩更新!
作者:Hector Zelaya
原文:https://webrtc.ventures/2023/05/enhancing-webrtc-video-streams-adding-text-images-and-qr-codes/
—-机器翻译,仅作参考—-
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/25337.html