如何将网络摄像头直接链接到Rails的ActiveStorage(附代码)

165 阅读6分钟

ActiveStorage非常棒。在我所见过的所有使用中,它都是用于直接上传文件,即用户点击一个文件字段,弹出一个窗口,然后选择一个文件--没有什么新意。

不久前,我的团队负责开发一个允许用户上传图片的功能,不是我们已经知道的传统意义上的上传,而是从网络摄像头上传--这很新颖。

注意:我在这里建立了一个工作版本,并在GitHub上发布了代码供大家阅读。

将前端整理出来

首先,我们需要访问用户的网络摄像头。我们可以通过navigator.mediaDevices API用JavaScript实现这一点。navigator.mediaDevices 只读属性返回一个MediaDevices 对象,它提供了对连接的媒体输入设备如摄像头和麦克风的访问:

if (video) {
    navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
        video.srcObject = stream
    })
}

这样就可以从网络摄像头中串联出一个实时画面。但我们想要的是图片,为此我们设置一个画布,并在上面画出流媒体的内容,这可能是在任何时候,只捕捉一帧。HTMLCanvasElement 提供了一个toDataURL() 方法,它返回一个数据URL,其中包含一个base64 编码的图像表示,我们稍后会提到。

我们从流中得到的数据URL是一个很长的字符串。最初,当我在考虑如何实现这个方法时,我认为通过参数提交字符串是可以的,但很快就收回了这个想法!原因是不同的网络服务器可能会有不同的结果。原因是,不同的网络服务器可能默认设置了客户端请求体的最大允许大小。在这种情况下,如果我们发送的数据URL刚好太大,我们可能会得到一个413 Payload Too Large 的响应状态代码,浏览器可能无法正确显示。

另一方面,一个隐藏字段允许发送的数据在提交表单时不能被用户看到或修改,另外还有一个额外的好处,就是在浏览器中没有技术限制(对于一个很长的数据URL来说是一个完美的选择)。由于这些原因,通过表单上的隐藏字段向控制器发送数据URL是很安全的,就像这样。

<%= form.hidden_field :player_picture, value: @player.player_picture %>.

如果出于某种原因,你对安全和隐藏字段的内容有疑虑,在后端你可以随时过滤被推送的内容,毕竟这是互联网,各种奇怪的人与我们同居。

player_picture 这将是模型上的一个虚拟属性,因为我们对使其持久化不感兴趣,但需要一些东西来保存从我们的网络摄像头拍摄的图像的数据URL,以便我们可以将其传递给ActiveStorage。 必须在控制器内部的参数中接受。player_picture

对于前端,我已经在webcam.js中设置了一切。还有一件重要的事情值得注意,在获得对网络摄像头的访问权后,我们需要从流中画出一个帧到画布上,这发生在以下代码中:

  if (snapButton) {
    snapButton.onclick = function () {

    // snip!

      canvas.getContext('2d').drawImage(video, 0, 0)

      var dataUrl = canvas.toDataURL('image/jpeg')

      document.getElementById("shot").src = dataUrl

      hiddenPlayerPicture.value = dataUrl

    // snip!

    }
  }

上述代码中的相关部分是hiddenPlayerPicture.value = dataUrl ,我们将隐藏字段的值设置为dataUrl ,这是我们从网络摄像头流中抓取的帧的数据URL。

将网络摄像头图像附加到ActiveStorage

我没有提到设置Rails ActiveStorage的问题,因为我认为这已经完成了

在我们的模型中,我们可以像这样设置我前面提到的虚拟属性:

# /app/models/player.rb

attribute :player_picture, :string, default: ''

然后让控制器接受它:

# /app/controllers/players_controller.rb

  def player_params
    params.require(:player).permit(
     # other params
     :player_picture)
  end

现在,我们有了通过表单进入控制器的捕获框架,我们可以将数据附加到ActiveStorage。对于我们这里的用例,我们只对通过update 动作将图像附加到播放器上感兴趣。现在是将这个功能抽象为一个服务的好时机,看起来像这样:

# /app/services/picture_attachment_service.rb

class PictureAttachmentService
  class << self
    def attach(model, picture)
      base_64_image = picture.gsub!(/^data:.*,/, '')
      decoded_image = Base64.decode64(base_64_image)

      model.picture.attach(
        io: StringIO.new(decoded_image),
        filename: "player_picture_#{unique_string}.jpeg"
      )
    end

    private def unique_string
      SecureRandom.urlsafe_base64(10)
    end
  end
end

上面的代码是将图片附加到ActiveStorage的关键所在。这里正在发生一些事情。还记得我们上面提到的图片的base64 编码表示吗,它来自toDataURL() ,采取这种形式。

data:[<mediatype>][;base64],<data>

这有四个部分;一个前缀(data:),一个表示数据类型的MIME类型,一个可选的base64标记(如果数据是非文本的),以及数据本身。

这种表述的一个例子是。

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICA...42vYWS34f/9k=

我们只想要其中的<data> 部分,这就是base_64_image = picture.gsub!(/^data:.*,/, '') 为我们所做的。然后,由于它被编码了,我们需要用decoded_image = Base64.decode64(base_64_image) 对它进行解码。这就给了我们一个解码后的字符串,我们可以使用。

我喜欢ActiveStorage的一点是,它允许我们附加IO对象。Rails Guides提供的一个例子是这样的。

@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf')

幸运的是,我们可以有一个StringIO ,在我们的例子中,这是解码后的base64 数据。就这样了。我们已经成功地构建了我们可以连接的部分,以从网络摄像头的流(图片)中附加一个帧。

等等。没有那么快。

我们忘了在我们控制器的update 动作中调用这个服务。

这就是我们的控制器现在应该有的样子:

# /app/controllers/players_controller.rb

def update
  PictureAttachmentService.attach(@player, params['player']['player_picture'])

  respond_to do |format|
    # some stuff
  end
end

好了,现在我们完成了。

将网络摄像头视频附加到ActiveStorage

附加摄像头视频的过程与图像的过程类似。除了,在这种情况下,会有一些更多的工作。但这里最重要的是,在设置了带有播放、暂停控制的用户界面后,我们可以通过我们先前使用的同样的navigator.mediaDevices ,获得MIME类型的视频video/webm 。但请记住,如果视频的MIME类型在服务器上设置不正确,视频可能不会显示或显示一个包含X的灰色框(如果JavaScript被启用)。但我不认为video/webm 会有问题。

MDN Web Docs有一个很好的教程,介绍了如何通过视频录制来获得正确的用户界面。

一旦用户界面到位,所需要的就是在你的视图中显示视频,像这样的东西:

<video width="500" height="300" autoplay loop="true">
  <source src="<%= url_for(@player.video) %>"
     type="video/webm">
</video>

其中@player.video 是你从navigator.mediaDevices 得到的video/webm

我们可以相应地编辑我们的服务,使其看起来像这样:

class VideoAttachmentService
  class << self
    def attach(model, video_path)
      model.picture.attach(
        io: File.open(video_path),
        filename: "player_video_#{unique_string}.webm"
      )
    end

    private def unique_string
      SecureRandom.urlsafe_base64(10)
    end
  end
end

有几件事情我们可以谈一谈;错误处理,以防我们的附件服务出错。用Sidekiq或Resque为服务设置后台作业、安全、重构、Web服务器限制、测试等等,但这些都超出了本帖的范围。我们已经实现了主要功能。一切都在运行,大家都很高兴。

我相信ActiveStorage能够做得更多;其中一个例子是分析视频--这是很值得赞赏的。