从网络摄像头到GIF动画:chat.meatspac.es背后的秘密!

My team mate Edna Piranha is not only an awesome hacker; she's also a fantastic philosopher! Communication and online interactions is a subject that has kept her mind busy for a long time, and it has also resulted in a bunch of interesting experimental projects that have fostered an unexpected community and tons of spontaneous collaboration, plus have helped unearth a browser bug or two!

我的队友埃德娜·皮拉尼亚 ( Edna Piranha )不仅是一位了不起的黑客,而且还是一位出色的黑客。 她也是一位出色的哲学家! 交流和在线互动是让她长时间忙碌的话题,它还导致了许多有趣的实验项目,这些项目培育了一个意想不到的社区和大量自发的协作,此外还帮助发掘了浏览器错误或二!

We could spend hours just going through her list of projects and getting amazed by all the ways in which she's approaching the same aspect (human interaction) from different angles, both philosophical and technical, but this isn't Edna Piranha's Fan Club Blog, and David only asked me to write about Animated GIFs in the widely successful Meatspace Chat, so let's focus on that.

我们可能要花几个小时来浏览她的项目清单,并对她从哲学和技术两个角度从不同角度接近同一方面(人类互动)的所有方式感到惊讶,但这不是Edna Piranha的粉丝俱乐部博客 ,以及David只要求我在广受欢迎的Meatspace聊天中写有关动画GIF的内容,所以让我们集中讨论一下。

It all started about a year ago. Edna had just built a decentralised microblogging engine with Redis, and was trying to find a use case for a database she had just heard about, called LevelDB.

一切始于大约一年前。 埃德娜(Edna)刚刚用Redis构建了一个去中心化的微博引擎 ,并试图为她刚刚听说的一个叫做LevelDB的数据库找到一个用例。

She showed me a real time chat app she had hacked in a couple of hours, using LevelDB as the temporary, ephemeral storage. Anyone could sign in using Persona, and start sending messages to the one chat room. The avatar associated to your Persona account would be shown along with the message you sent, and messages would be deleted after a few minutes.

她向我展示了一个实时聊天应用程序,它使用LevelDB作为临时的临时存储,在几个小时内就被黑了。 任何人都可以使用Persona登录,然后开始向一个聊天室发送消息。 与您的Persona帐户关联的化身将与您发送的消息一起显示,几分钟后,消息将被删除。

By that time I had been working on rtcamera, a camera app that could generate animated GIFs using your webcam as input, and somehow our thought trails converged: wouldn't it be super cool to use the webcam's input instead of an static avatar?

那时我一直在研究rtcamera ,这是一个摄像头应用程序,可以使用您的网络摄像头作为输入来生成动画GIF,并且以某种方式融合了我们的想法:使用网络摄像头的输入而不是静态头像不是很酷吗?

It was easy to implement this using the two libraries that I had extracted out from rtcamera: gumHelper and Animated_GIF, and the rest is history!

使用我从rtcamera中提取的两个库很容易实现这一点gumHelperAnimated_GIF ,剩下的就是历史了!

But for those of you who don't know about history: we kept the chat in private for a while because Edna was going to present it at RealtimeConf. And then... it just exploded! People started coming to the site in flocks and being both puzzled by the unexpected cheerness and the general Back to the True Web raw and honest spirit: no sign up forms, no name to fill in, no identity to build and maintain; just a text input and your face to show the world what you were up to in that very moment. If you haven't been to Meatspaces Chat yet, I recommend you go there now to familiarise yourself with how it looks and works before I get into technical details. You can also watch Edna's keynote at jQuery Con San Diego, where she talks about all this.

但是对于那些不了解历史的人:我们将聊天保密了一段时间,因为Edna打算在RealtimeConf上进行演示 。 然后...它爆炸了! 人们开始蜂拥而至,无所适从和普遍感到困惑。 回到True Web原始和诚实的精神:没有注册表格,没有名字要填写,没有身份可以建立和维护; 只需输入文字和您的脸,即可向世界展示您当时正在做什么。 如果您还没来过Meatspaces Chat,我建议您现在就去那里 ,在开始了解技术细节之前先熟悉一下它的外观和工作方式。 您也可以在jQuery Con San Diego上观看Edna的主题演讲,她在那儿谈论了所有这一切。

要多汁的技术细节! (To the juicy technical details!)

Are you all intrigued now? Cool!

你们现在都对它感兴趣吗? 凉!

But before we start deep diving into the code, let me add a little warning: Meatspaces chat is constantly being improved by the amazing community, so I will be referring to lines using a specific commit hash too. If you go directly to the project page and access the master branch, both the code and line numbers might differ from what this article says.

但是在开始深入研究代码之前,让我添加一些警告:令人惊叹的社区不断改善Meatspaces聊天,因此我也将引用使用特定提交哈希的行。 如果直接进入项目页面并访问master分支,则代码和行号可能与本文所说的有所不同。

And we are really ready to go!

我们真的准备好了!

存取相机 (Accessing the camera)

Everything starts with requesting access to the user's camera. We are using the gumHelper library for this. No, it has nothing to do with dental hygiene; it actually means "getUserMediaHelper", where getUserMedia is the part of the WebRTC API that allows us to obtain a live media stream containing live audio or video which we can then use in our websites. In this case we're only interested in video, as GIFs are (sadly) silent.

一切始于请求访问用户的相机。 我们正在为此使用gumHelper库。 不,它与牙齿卫生无关。 它实际上表示“ getUserMediaHelper”,其中getUserMedia是WebRTC API的一部分,它使我们能够获取包含实时音频或视频的实时媒体流,然后可以在我们的网站中使用它。 在这种情况下,我们只对视频感兴趣,因为GIF(非常)安静。

If you're running this on a laptop or a desktop--i.e. a full blown computer-- we'll access the webcam. If you're running this on a phone, it will not only ask you for permission to use the camera, but also show you a drop down so you can select which camera to use, if applicable (some devices only have a back camera).

如果您是在笔记本电脑或台式机(即功能齐全的计算机)上运行此程序,我们将访问网络摄像头。 如果你在手机上运行此,它不仅会询问您是否允许使用摄像头,而且还告诉你一个下拉,所以你可以选择相机使用,如果适用(某些设备只能有一个后置摄像头) 。

We'll attempt to start streaming by calling gumHelper.startVideoStreaming:

我们将尝试通过调用gumHelper.startVideoStreaming来开始流式传输:

gumHelper.startVideoStreaming(function (err, stream, videoElement, videoWidth, videoHeight) {
    // ...
}, { /* options */ });


startVideoStreaming takes a callback and an optional options object as parameters. In fairly standard node.js style, the callback function first parameter is err, which we check first. If it is truthy, we just give up on accessing the video. In earlier versions of the site, your messages would be accompanied by a giant meat cube avatar if video wasn't enabled for whatever the reason, but it was changed to disallow sending messages to prevent trolls from posting.

startVideoStreaming将回调和可选的 options对象作为参数。 在相当标准的node.js样式中,回调函数的第一个参数是err ,我们首先对其进行检查。 如果是事实 ,我们只是放弃观看视频。 在该网站的早期版本中,如果由于某种原因未启用视频,则您的消息将带有巨大的肉块头像,但已将其更改为禁止发送消息以阻止巨魔发布。

Supposing the stream was successfully started, the next step is to use the videoElement returned by gumHelper. This is just a plain HTML5 <video> element that we will place in the page to serve as preview, so the user can ensure they are in the frame when they press ENTER.

假设流已成功启动,则下一步是使用videoElement返回的videoElement。 这只是一个普通HTML5 <video>元素, 我们将其放置在页面中以用作预览,因此用户可以确保在按ENTER时它们在框架中。

捕捉帧 (Capturing frames)

The other thing we're doing is creating a VideoShooter instance. This is a little class that attaches to an existing video element and will start generating a GIF whenever we press ENTER, using frames from that video element:

我们正在做的另一件事是创建一个VideoShooter实例。 这是一个附加到现有视频元素的小类, 每当我们按ENTER时 ,就会使用该视频元素中的帧开始生成GIF:

videoShooter = new VideoShooter(videoElement, gifWidth, gifHeight, videoWidth, videoHeight, cropDimens);


The function to get a video capture is VideoShooter.getShot, which accepts a few parameters: callback (called to return the encoded GIF), numFrames (to specify how many frames to capture), interval (for setting the interval between capturing frames) and progressCallback (which is used to show a sort of progress indicator overlay over the video preview).

获取视频捕获的函数是VideoShooter.getShot ,它接受一些参数: callback (调用以返回编码的GIF), numFrames (指定要捕获的帧数), interval (用于设置捕获帧之间的间隔)和progressCallback (用于在视频预览上显示某种进度指示器)。

Internally, what getShot does is creating an instance of Animated_GIF and then periodically tells it to capture a frame as many times as requested, using Animated_GIF's addFrame method.

在内部, getShot工作是创建一个Animated_GIF实例,然后使用Animated_GIF的addFrame方法定期告诉它捕获一个帧,次数达到要求。

How often the frames are captured (and therefore how smooth the animation will be) depends on the interval parameter. The more frames and the more frequently they are captured, the better and less jerky the GIF will look, but it will also be bigger. We played a bit with the parameters and decided to settle on two second GIFs (10 frames shot every 0.2 seconds make 2 seconds). Hence the "lemma" of the site: "your two seconds of fame".

捕获帧的频率(以及动画的平滑程度)取决于interval参数。 帧越多,捕获的频率越多,GIF看起来会越好,但抖动也会越来越大,但也会更大。 我们使用了一些参数,并决定使用两个第二GIF(每0.2秒拍摄10帧即2秒)。 因此,网站的“引理”是:“您的成名两秒钟”。

动画GIF (Animating the GIF)

Each time we add a frame to the Animated_GIF instance, we pass videoElement as source parameter. It is then copied into an internal canvas to extract the image data and store it on a list of frames, taking advantage of the drawImage function that allows you to render HTML elements into CanvasRenderingContext2D objects.

每次将帧添加到Animated_GIF实例时,我们都将videoElement作为源参数传递。 然后将其复制到内部画布中,以提取图像数据并将其存储在帧列表中,并利用drawImage函数,该函数允许您将HTML元素呈现为CanvasRenderingContext2D对象。

Once the ten frames have been captured, the VideoShooter instance will call the getBase64GIF method from Animated_GIF.

一旦捕获了十帧, VideoShooter实例将从Animated_GIF 调用 getBase64GIF方法。

This part is probably the most involved of all in the whole process, since we are ultimately generating binary data in JavaScript. Fortunately, it is all abstracted enough that we only need to call the method and wait for it to be generated on the background using Web Workers.

这部分可能是整个过程中涉及最多的部分,因为我们最终将使用JavaScript生成二进制数据。 幸运的是,所有这些都足够抽象,我们只需要调用该方法,然后等待使用Web Workers在后台生成该方法即可。

We use Web Workers because rendering is quite an intensive process and can easily block the main thread, making the whole app unresponsive--that's something we don't want to happen!

我们之所以使用Web Worker,是因为渲染是一个非常繁琐的过程,并且可以轻松地阻塞主线程,从而使整个应用程序无响应-这是我们不希望发生的事情!

The callback function is invoked and sent the rendered GIF when it's ready. Since it is a Base64 string we can just include it without further processing on the submission object that is then posted to the server.

准备好时,将调用回调函数并发送呈现的GIF。 由于它是Base64字符串,我们可以直接包含它,而无需进一步处理submission对象然后将其发布到服务器

And that's how your funny faces get captured and travel down the wire to people all over the world. Or almost!

这就是您的搞笑面Kong被捕获并通过电线传送给世界各地人们的方式。 差不多!

GIF墙 (GIFWall)

I thought that maybe perusing the entire codebase of Meatspaces Chat would be a bit too much if you're only interested in the GIF side of things, so I build this little demo app that periodically captures GIFs using your webcam and adds them to the page.

我认为,如果您仅对GIF方面感兴趣,那么可能会仔细阅读Meatspaces Chat的整个代码库,所以我构建了这个小演示应用程序 ,该应用程序使用网络摄像头定期捕获GIF并将其添加到页面中。

It also uses gumHelper, Animated_GIF and a simplified version of the VideoShooter module.

它还使用gumHelper,Animated_GIF和VideoShooter模块的简化版本。

To demonstrate how easy it is to capture data from the webcam and turn it into a GIF with the right libraries to abstract the tedium, here is the main code from GIFwall:

为了演示从网络摄像头捕获数据并将其转换为带有正确库以抽象乏味的GIF的过程,这是GIFwall的主要代码:

var main = document.querySelector('main');
var mosaicContainer = document.getElementById('mosaic');
var videoWidth= 0, videoHeight = 0;
var videoElement;
var shooter;
var imagesPerRow = 5;
var maxImages = 20;

window.addEventListener('resize', onResize);

GumHelper.startVideoStreaming(function(error, stream, videoEl, width, height) {
    if(error) {
        alert('Cannot open the camera. Sad times: ' + error.message);
        return;
    }

    videoElement = videoEl;
    videoElement.width = width / 4;
    videoElement.height = height / 4;
    videoWidth = width;
    videoHeight = height;

    main.appendChild(videoElement);

    shooter = new VideoShooter(videoElement);

    onResize();

    startCapturing();

});

function startCapturing() {

    shooter.getShot(onFrameCaptured, 10, 0.2, function onProgress(progress) {
        // Not doing anything in the callback,
        // but you could animate a progress bar or similar using the `progress` value
    });

}

function onFrameCaptured(pictureData) {
    var img = document.createElement('img');
    img.src = pictureData;

    var imageSize = getImageSize();

    img.style.width = imageSize[0] + 'px';
    img.style.height = imageSize[1] + 'px';

    mosaicContainer.insertBefore(img, mosaicContainer.firstChild);

    if(mosaicContainer.childElementCount > maxImages) {
        mosaicContainer.removeChild(mosaicContainer.lastChild); 
    }

    setTimeout(startCapturing, 10);
}

function getImageSize() {
    var windowWidth = window.innerWidth;
    var imageWidth = Math.round(windowWidth / imagesPerRow);
    var imageHeight = (imageWidth / videoWidth) * videoHeight;

    return [ imageWidth, imageHeight ];
}

function onResize(e) {

    // Don't do anything until we have a video element from which to derive sizes
    if(!videoElement) {
        return;
    }

    var imageSize = getImageSize();
    var imageWidth = imageSize[0] + 'px';
    var imageHeight = imageSize[1] + 'px';

    for(var i = 0; i < mosaicContainer.childElementCount; i++) {
        var img = mosaicContainer.children[i];
        img.style.width = imageWidth;
        img.style.height = imageHeight;
    }

    videoElement.style.width = imageWidth;
    videoElement.style.height = imageHeight;

}


This is essentially Meatspace Chat, but without chatting and without sending the data to other connected people. Some homework for the reader could be to show a progress bar or other fancy similar effect while GIFs are being encoded, or even improve this so that the captured GIFs are actually sent to other users via real peer to peer connections over WebRTC.

这本质上是Meatspace聊天,但不聊天,也不将数据发送给其他已连接的人。 读者的一些作业可能是在对GIF进行编码时显示进度条或其他类似的效果,或者甚至对此进行改进,以使捕获的GIF实际上通过WebRTC上的真实对等连接发送给其他用户。

There are so many things you can do on the web nowadays! Isn't that exciting? Now go get the sources, play with the code and have fun, and don't forget to share your work so we can all learn and have fun too! :-)

如今,您可以在网上做很多事情! 那不令人兴奋吗? 现在去获取源代码 ,玩代码并玩得开心,不要忘了分享您的工作,这样我们大家都可以学习并玩得开心! :-)

翻译自: https://davidwalsh.name/webcam-animated-gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值