如何构建一个简单的摄像头组件

栏目: Html5 · 发布时间: 6年前

内容简介:特别声明,本文根据要构建一个让我们构建一具自定义的

特别声明,本文根据 @David East 的《 HOW TO BUILD A SIMPLE CAMERA COMPONENT 》一文所整理。

要构建一个 camera 组件,我们首先要了解所需的浏览器API。

让我们构建一具自定义的 camera 元素,这样你就不必再担心把这些代码连接起来。

使用自定义元素构建可跨框架重用的组件

这篇文章并没有指定在哪个框架构建摄像头组件。叶节点(Leaf Node)组件应该是可重用的。自定义元素是一种新的浏览器标准,允许你构建可在大多数JavaScript框架中移植的可重用元素。如果你不熟悉自定义元素(Custom Elements)并不重要。因为接下来的示例都是一些简单的示例,所以使用自定义元素并不复杂。在高级情况下,它会变得复杂,但我们将会避开这些。这是一个简单的例子:

class HelloElement extends HTMLElement {
    constructor() {
        // 调用构造函数不是必须的。如果你这样做,一定要确认调用了`super()`
        super();
    }

    // 当元素连接到DOM时调用这个函数
    connectedCallback() {
        // 附上一个shadow,这样任何人都不会弄乱你的样式
        const shadow = this.attachShadow({ mode: 'open' });
        shadow.textContent = 'Hello world!';
    }
}

// 定义标签名,它必须有一个破折号
customElements.define('hello-element', HelloElement);

在HTML中你可以像下面这样调用自定义的元素 hello-element

<hello-element></hello-element>

你在浏览器运行上面的代码之后,将看到的效果如下图所示:

如何构建一个简单的摄像头组件

这是自定义元素的简单用法。就像我说的,它也可以变得更复杂,但我们这里将要构建的是一个摄像头组件,而且是一个简单的摄像头组件,所以会尽量让它保持简单。

摄像头组件需要一个video元素和一个隐藏的canvas元素

让我们从简单的 camera 组件开始。

class SimpleCamera extends HTMLElement {

    constructor() {
        super();
    }

    connectedCallback() {
        const shadow = this.attachShadow({
            mode: 'open'
        })

        this.videoElement = document.createElement('video')
        this.canvasElement = document.createElement('canvas')
        this.videoElement.setAttribute('playsinline', true)
        this.canvasElement.style.display = 'none'

        shadow.appendChild(this.videoElement)
        shadow.appendChild(this.canvasElement)
    }
}

customElements.define('simple-camera', SimpleCamera)

该组件只添加了两个元素:一个是 video 元素和一个隐藏的 canvas 元素。

在 iOS 10 Safari 中,通过 playsinline 可以让视频内联播放。设置了 playsinline 属性的视频在播放时不会自动全屏,但用户可以点击全屏按钮来手动全屏;没有设置 playsinline 的视频会在播放时自动全屏。无论是否设置 playsinline 属性,退出全屏后视频都会继续播放。

playsinline 属性在 iOS 10 之前需要写成 webkit-playsinline ,它的浏览器厂商前缀在 iOS 10 中被移除。但是目前 iOS 微信还不支持去掉前缀的写法,两个属性最好都加上。

显然, <video>autoplay 必须和 playsinline 属性一起使用。也就是说,只有默认内联播放的视频才有可能自动播放,这一点很容易理解。

然后在HTML中像下面这样调用自定义好的元素:

<simple-camera></simple-camera>

这样就可以为摄像机创建一个元素。也可以开始播放一些视频。

如何构建一个简单的摄像头组件

好像啥也没有一样,是不。不急,咱们继续往下。

通过 MediaDevices API授权访问摄像头

使用 navigator.mediaDevices.getUserMedia() 方法,授权用户访问摄像头。

navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => {

})

请注意, getUserMedia() 会返回一个 Promise 。如果返回成功, Promise 会解析 MediaStream 。此流(Stream)将会用于 video 元素。如果 Promise 拒绝( rejects ),表示用户未授权访问摄像头。然而! Promise 有可能永远不会解决( resolve )或拒绝( reject )。用户可以决定永远不对权限弹出框执行操作。那不是很好玩吗?

浏览器对 MediaDevices 的支持很强大,但很奇怪

MediaDevices API得到浏览器强大的支持。它可以在所有现代浏览器中使用。然而,在IE中没有得到支持,所以你需要对该特性做一个检查。

if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => {

    })
}

然而,一些浏览器版本对 MediaDevices 的API只有部分支持,有些则需要添加浏览器供应商的前端才能实现。 MDN文章 中有一个关于设置Polyfills的部分有介绍到这方面的知识。幸运的是,这些Polyfill应该应用在元素之外,所以我们不需要在元素中考虑这个。

为mediaStream的audio和video设置相应的约束

getUserMedia() 方法接受一组约束。这些限制有助于在用户接受权限后配置流。它们具有 MediaStreamConstraints 的类型。你可以指定两个主要属性: audiovideo

if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
            facingMode: 'user'
        }
    }).then((mediaStream) => {

    })
}

audio 属性是一个简单的布尔值。你要么请求用户的音频,要么不请求。 video 属性要复杂得多。视频约束,也称为 MediaTrackConstraints ,指定了视频流可能需要的所有内容: echoCancellationlatencysampleRatesampleSizevolumenoiseSuppressionframeRateaspectRatiofacingMode ,当然还有 widthheight

有很多约束。然而,除非你正开发一个摄像头应用程序,否则你只需要几个。即: heightwidthfacingMode

将MediaStream分配给video元素

现在已经配置了 MediaStream ,就可以将其分配给 video 元素。

open(constraints) {
    return navigator.mediaDevices.getUserMedia(constraints)
        .then((mediaStream) => {
            // 分配MediaStream
            this.videoElement.srcObject = mediaStream

            // 加载时播放流
            this.videoElement.onloadedmetadata = (e) => {
                this.videoElement.play()
            }
        })
}

video 元素有一个 srcObject 。它在分配 MediaStream 时从设置的摄像头流式输出。上面的代码片段在元素上添加了一个 open 方法。自定义元素具有可调用方法。如果用户调用这个 open 方法,它将启动视频流。

<script>
    (async function() {
        const camera = document.querySelector('simple-camera')
        await camera.open({
            video: {
                facingMode: 'user'
            }
        })
    }())
</script>

现在我们可以播放视频,让我们拍照。

如何构建一个简单的摄像头组件

使用canvas将照片作为blob拍摄

canvas 元素能够从 video 元素中绘制帧。使用此功能,你可以在不可见的 canvas 上绘制,然后将图像导出为 blob

_drawImage() {
    const imageWidth = this.videoElement.videoWidth
    const imageHeight = this.videoElement.videoHeight

    const context = this.canvasElement.getContext('2d')
    this.canvasElement.width = imageWidth
    this.canvasElement.height = imageHeight

    context.drawImage(this.videoElement, 0, 0, imageWidth, imageHeight)

    return {
        imageHeight,
        imageWidth
    }
}

这个私有的 _drawImage() 方法将不可见的 canvasheightwidth 设置为 video 的大小。然后在上下文( context )中使用 drawImage() 方法。提供 video 元素的 xy 位置, widthheight 。这将在不可见的 canvas 上绘图,并将相关设置创建为一个 blob

takeBlobPhoto() {
    const {imageHeight, imageWidth} = this._drawImage()

    return new Promise((resolve, reject) => {
        this.canvasElement.toBlob((blob) => {
            resolve({blob, imageHeight, imageWidth})
        })
    })
}

canvas 元素有一个 toBlob() 方法。由于它是异步的,所以你可以将它转换为一个 Promise ,这样它就更容易使用。

现在你可以开始控制这个相机了:

<simple-camera></simple-camera>
<button id="btnPhoto">Take Blob</button>

<script>
    (async function(){
        const camera = document.querySelector('simple-camera')
        const btnPhoto = document.querySelector('#btnPhoto')

        await camera.open({
            video: {
                facingMode: 'user'
            }
        })

        btnPhoto.addEventListener('click', async event => {
            const photo = await camera.takeBlobPhoto()
        })
    }())
</script>

当你需要上传一个文件时, blob 是最好的。但是有时候,在 image 标签中插入一个 base64 编码的字符串会更好。 canvas 有相应的解决方案。

如何构建一个简单的摄像头组件

使用canvas把拍摄的图片转换为base64

canvas 元素有现代战争 toDataURL() 方法。该方法获取 canvas 的当前内容,并将其输出成 base64 编码的图像。

takeBase64Photo({type, quality} = {type: 'png', quality: 1}) {
    const {imageHeight, imageWidth} = this._drawImage()

    const base64 = this.canvasElement.toDataURL('image/' + type, quality)

    return {base64, imageHeight, imageWidth}
}

takeBase64() 方法调用 toDataURL() 方法并返回它的 base64 值。注意,你可以指定图像类型和图像质量。

<simple-camera></simple-camera>

<button id="btnBlobPhoto">Take Blob</button>
<button id="btnBase64Photo">Take Base64</button>

<script>
    (async function() {
        const camera = document.querySelector('simple-camera')
        const btnBlobPhoto = document.querySelector('#btnBlobPhoto')
        const btnBase64Photo = document.querySelector('#btnBase64Photo')

        await camera.open({video: {facingMode: 'user'}})

        btnBlobPhoto.addEventListener('click', async event => {
            const photo = await camera.takeBlobPhoto()
        })

        btnBase64Photo.addEventListener('click', async event => {
            const photo = camera.takeBase64Photo({type: 'jpeg', quality: 0.8})
        })
    }())
</script>

如何构建一个简单的摄像头组件

把所有代码结合到一起:

<script>
    class SimpleCamera extends HTMLElement {

        constructor() {
            super();
        }

        connectedCallback() {
            const shadow = this.attachShadow({
                mode: 'open'
            })

            this.videoElement = document.createElement('video')
            this.canvasElement = document.createElement('canvas')
            this.videoElement.setAttribute('playsinline', true)
            this.canvasElement.style.display = 'none'

            shadow.appendChild(this.videoElement)
            shadow.appendChild(this.canvasElement)
        }

        open(constraints) {
            return navigator.mediaDevices.getUserMedia(constraints).then((mediaStream) => {
                this.videoElement.srcObject = mediaStream
                console.log(mediaStream)
                this.videoElement.onloadedmetadata = (e) => {
                this.videoElement.play()
                }
            })
        }

        _drawImage() {
            const imageWidth = this.videoElement.videoWidth
            const imageHeight = this.videoElement.videoHeight

            const context = this.canvasElement.getContext('2d')
            this.canvasElement.width = imageWidth
            this.canvasElement.height = imageHeight

            context.drawImage(this.videoElement, 0, 0, imageWidth, imageHeight)

            return {
                imageHeight,
                imageWidth
            }
        }

        takeBlobPhoto() {
            const {imageHeight, imageWidth} = this._drawImage()
            this.canvasElement.style.display="block"
            const card = document.createElement('div')
            card.classList.add('card')
            document.querySelector('.wrapper').appendChild(card)
            card.appendChild(this.canvasElement)

            return new Promise((resolve, reject) => {
                this.canvasElement.toBlob((blob) => {
                    resolve({blob, imageHeight, imageWidth})
                })
            })
        }

        takeBase64Photo({type, quality} = {type: 'png', quality: 1}) {
            const {imageHeight, imageWidth} = this._drawImage()

            const base64 = this.canvasElement.toDataURL('image/' + type, quality)

            this.canvasElement.style.display="block" 
            const card = document.createElement('div')
            card.classList.add('card')
            document.querySelector('.wrapper').appendChild(card)
            card.appendChild(this.canvasElement)

            return {base64, imageHeight, imageWidth}
        }

    }


    customElements.define('simple-camera', SimpleCamera)

</script>
<div class="wrapper">
    <div class="card">
        <simple-camera></simple-camera>

        <div class="active">
            <button id="btnBlobPhoto">Take Blob</button>
            <button id="btnBase64Photo">Take Base64</button>
        </div>
    </div>
</div>

<script>
    (async function() {
        const camera = document.querySelector('simple-camera')
        const btnBlobPhoto = document.querySelector('#btnBlobPhoto')
        const btnBase64Photo = document.querySelector('#btnBase64Photo')

        await camera.open({video: {facingMode: 'user'}})

        btnBlobPhoto.addEventListener('click', async event => {
            const photo = await camera.takeBlobPhoto()
        })

        btnBase64Photo.addEventListener('click', async event => {
            const photo = camera.takeBase64Photo({type: 'jpeg', quality: 0.8})
        })
    }())
</script>

Demo效果如下:


以上所述就是小编给大家介绍的《如何构建一个简单的摄像头组件》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Caching

Web Caching

Duane Wessels / O'Reilly Media, Inc. / 2001-6 / 39.95美元

On the World Wide Web, speed and efficiency are vital. Users have little patience for slow web pages, while network administrators want to make the most of their available bandwidth. A properly design......一起来看看 《Web Caching》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码