{"id":1181,"date":"2025-03-25T15:03:03","date_gmt":"2025-03-25T18:03:03","guid":{"rendered":"https:\/\/acsiv.com.br\/blog\/?p=1181"},"modified":"2025-07-01T10:10:20","modified_gmt":"2025-07-01T13:10:20","slug":"uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus","status":"publish","type":"post","link":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/","title":{"rendered":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus"},"content":{"rendered":"\n<p>A API <strong>MediaDevices<\/strong> fornece interfaces para interagir com dispositivos de m\u00eddia conectados, permitindo acesso \u00e0 m\u00eddia gerada pelo hardware. Neste artigo, veremos como a biblioteca JavaScript <strong>Stimulus<\/strong> pode nos ajudar a integrar essa API em um ambiente <strong>Rails<\/strong>.<\/p>\n\n\n\n<p>Iremos criar um <code>controller<\/code> para solicitar a permiss\u00e3o do usu\u00e1rio para acessar dispositivos de m\u00eddia conectados, capturar um <em>stream<\/em> de v\u00eddeo na melhor qualidade poss\u00edvel e extrair uma foto de um frame do v\u00eddeo. Al\u00e9m disso, desenvolveremos um <code>helper<\/code> que facilitar\u00e1 a integra\u00e7\u00e3o desse componente, permitindo salvar a foto capturada utilizando <strong>ActiveStorage<\/strong>. Este artigo assume que seu ambiente <strong>Rails<\/strong> e <strong>Stimulus<\/strong> j\u00e1 est\u00e1 configurado.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">O b\u00e1sico sobre Stimulus<\/h3>\n\n\n\n<p>Se voc\u00ea j\u00e1 sabe como criar um <code>controller<\/code> <strong>Stimulus<\/strong> e conect\u00e1-lo na p\u00e1gina, sinta-se \u00e0 vontade para pular para o pr\u00f3ximo t\u00f3pico.<br>O primeiro passo \u00e9 criar o <code>controller<\/code> <strong>Stimulus<\/strong> e conect\u00e1-lo com <strong>HTML<\/strong>. O <strong>Rails<\/strong> possui um comando para agilizar a cria\u00e7\u00e3o:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rails g stimulus camera<\/code><\/pre>\n\n\n\n<p>O que esse comando faz \u00e9 criar o arquivo <code>camera_controller.js<\/code> e registrar o <code>controller<\/code> em <code>index.js<\/code>. Bem simples. Segue abaixo o <code>controller<\/code> gerado:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n\n  connect() { \n  }\n}<\/code><\/pre>\n\n\n\n<p>O <strong>Stimulus<\/strong> interage com a p\u00e1gina atrav\u00e9s do <strong>HTML<\/strong> de forma bastante pr\u00e1tica. Veja o exemplo abaixo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;div data-controller=\"camera\"&gt;&lt;\/div&gt;<\/code><\/pre>\n\n\n\n<p>Quando essa <code>div<\/code> for renderizada na <strong>DOM<\/strong>, o <code>controller<\/code> de nome <code>camera<\/code> ser\u00e1 conectado. Por isso, \u00e9 essencial garantir que ele esteja devidamente registrado no <code>index.js<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Nosso ponto de partida<\/h3>\n\n\n\n<p>Acessar a c\u00e2mera \u00e9 um processo relativamente simples. Basta utilizar o seguinte comando:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const constraints = { video: true }\nawait navigator.mediaDevices.getUserMedia(constraints)<\/code><\/pre>\n\n\n\n<p>Ao executar esse comando, o navegador solicitar\u00e1 permiss\u00e3o ao usu\u00e1rio para acessar os dispositivos de m\u00eddia conectados. Caso a permiss\u00e3o seja concedida, a API selecionar\u00e1 automaticamente o dispositivo que melhor atenda \u00e0s restri\u00e7\u00f5es definidas.<\/p>\n\n\n\n<p>Esse comando retorna um objeto <code>MediaStream<\/code>, que representa o fluxo de m\u00eddia capturado pelo dispositivo. Esse fluxo \u00e9 composto por <em>tracks<\/em>, e podemos utiliz\u00e1-lo diretamente como fonte de um elemento <code>&lt;video&gt;<\/code>, permitindo a reprodu\u00e7\u00e3o em tempo real.<\/p>\n\n\n\n<p>Vejamos como podemos fazer isso com <strong>Stimulus<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  static targets = &#91;\"display\"]\n\n  async connect() {\n    if (\"mediaDevices\" in navigator &amp;&amp; \"getUserMedia\" in navigator.mediaDevices) {\n      const constraints = { video: true }\n      const stream = await navigator.mediaDevices.getUserMedia(constraints)\n\n      this.displayTarget.srcObject = stream\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>Para tornar essa funcionalidade reutiliz\u00e1vel em um projeto <strong>Rails<\/strong>, podemos criar um <code>helper<\/code> que encapsula a estrutura <strong>HTML<\/strong> e conecta o <code>controller<\/code> <strong>Stimulus<\/strong> automaticamente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># camera_helper.rb\n\nmodule CameraHelper\n  def camera\n    content_tag(:div, data: { controller: \"camera\" }) do\n      tag.video(autoplay: true, data: { \"camera-target\": \"display\" })\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<p>Agora, sempre que esse <code>helper<\/code> for chamado em uma <em>view<\/em>, ele adicionar\u00e1 automaticamente o suporte \u00e0 c\u00e2mera na p\u00e1gina. Assim que o usu\u00e1rio conceder a permiss\u00e3o para acessar os dispositivos, o <em>stream<\/em> de v\u00eddeo ser\u00e1 exibido em tempo real.<\/p>\n\n\n\n<p>Os navegadores costumam limitar o acesso \u00e0 alguns recursos em websites que n\u00e3o possuem o protocolo <strong>HTTPS<\/strong>, e via de regra, o <em>localhost<\/em> utiliza apenas o <strong>HTTP<\/strong>. Para contornar isso no seu ambiente de desenvolvimento, \u00e9 poss\u00edvel executar os navegadores sem as restri\u00e7\u00f5es de seguran\u00e7a.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Google Chrome<\/h5>\n\n\n\n<p>Para usu\u00e1rios de Windows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --unsafely-treat-insecure-origin-as-secure=\"http:\/\/localhost:3000\/\"<\/code><\/pre>\n\n\n\n<p>Para usu\u00e1rios de Linux:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>google-chrome --unsafely-treat-insecure-origin-as-secure=\"http:\/\/localhost:3000\/\"<\/code><\/pre>\n\n\n\n<h5 class=\"wp-block-heading\">Firefox<\/h5>\n\n\n\n<ol>\n<li>Abra o Firefox.<\/li>\n\n\n\n<li>Na barra de endere\u00e7os, digite:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>about:config<\/code><\/pre>\n\n\n\n<p>e pressione Enter.<\/p>\n\n\n\n<ol start=\"3\">\n<li>Clique no bot\u00e3o <strong>&#8220;Aceitar o risco e continuar&#8221;<\/strong> (se aparecer).<\/li>\n\n\n\n<li>Na barra de pesquisa, procure por:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>media.devices.insecure.enabled\nmedia.getusermedia.insecure.enabled<\/code><\/pre>\n\n\n\n<ol start=\"5\">\n<li>Se o valor estiver como <code>false<\/code>, <strong>clique duas vezes<\/strong> para alter\u00e1-lo para <code>true<\/code>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Controles b\u00e1sicos<\/h3>\n\n\n\n<p>Agora que j\u00e1 aprendemos a utilizar a API <strong>MediaDevices<\/strong> para capturar imagens, podemos avan\u00e7ar um pouco mais e adicionar controles b\u00e1sicos para iniciar e encerrar a c\u00e2mera. No <strong>Stimulus<\/strong>, uma <strong>a\u00e7\u00e3o<\/strong> \u00e9 uma fun\u00e7\u00e3o JavaScript vinculada a um evento de um elemento <strong>HTML<\/strong>. Qualquer fun\u00e7\u00e3o p\u00fablica dentro de um <code>controller<\/code> pode ser chamada, desde que esteja corretamente associada a um elemento da p\u00e1gina.<\/p>\n\n\n\n<p>Vamos criar dois bot\u00f5es: um para iniciar e outro para interromper a captura de v\u00eddeo. As a\u00e7\u00f5es associadas a esses bot\u00f5es ser\u00e3o <code>play()<\/code> e <code>stop()<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  static targets = &#91;\"display\"]\n\n  videoStream = null\n\n  async play() {\n    await this.#setVideoStream()\n  }\n \n  stop() {\n    this.videoStream.getTracks().forEach((track) =&gt; track.stop())\n  }\n \n  async #setVideoStream() {\n    if (\"mediaDevices\" in navigator &amp;&amp; \"getUserMedia\" in navigator.mediaDevices) {\n      const constraints = { video: true }\n      this.videoStream = await navigator.mediaDevices.getUserMedia(constraints)\n\n      this.displayTarget.srcObject = this.videoStream\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>As fun\u00e7\u00f5es <code>play()<\/code> e <code>stop()<\/code> ser\u00e3o nossas a\u00e7\u00f5es executadas por elementos <strong>HTML<\/strong>. J\u00e1 a fun\u00e7\u00e3o <code>#setVideoStream()<\/code> \u00e9 privada e n\u00e3o pode ser chamada diretamente pelos elementos. Essa fun\u00e7\u00e3o \u00e9 respons\u00e1vel por criar o fluxo de m\u00eddia e armazen\u00e1-lo na vari\u00e1vel <code>videoStream<\/code>. Essa vari\u00e1vel pode ser acessada por outros m\u00e9todos, como <code>stop()<\/code>, que a utiliza para encerrar o fluxo de v\u00eddeo corretamente. Dessa forma, garantimos que o objeto <code>MediaStream<\/code> fique acess\u00edvel enquanto o componente estiver ativo.<\/p>\n\n\n\n<p>Agora, precisamos criar os bot\u00f5es que ir\u00e3o ativar essas fun\u00e7\u00f5es no <strong>Stimulus<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># camera_helper.rb\n\nmodule CameraHelper\n  def camera\n    content_tag(:div, data: { controller: \"camera\" }) do\n      concat(camera_display)\n      concat(camera_actions)\n    end\n  end\n\n  def camera_display\n    tag.video(autoplay: true, data: { \"camera-target\": \"display\" })\n  end\n\n  def camera_actions\n    content_tag(:div) do\n      concat(tag.button(\"play\", data: { action: \"click-&gt;camera#play\" }, type: \"button\"))\n      concat(tag.button(\"stop\", data: { action: \"click-&gt;camera#stop\" }, type: \"button\"))\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<p>As a\u00e7\u00f5es s\u00e3o acionadas pelo atributo <code>data-action<\/code> nos bot\u00f5es, que define qual fun\u00e7\u00e3o deve ser chamada ao ocorrer um evento, sendo que o evento n\u00e3o \u00e9 obrigat\u00f3rio. O evento pode ser especificado no atributo, mas, em muitos casos, o pr\u00f3prio elemento <strong>HTML<\/strong> j\u00e1 define um evento padr\u00e3o (como <code>click<\/code> para bot\u00f5es e <code>input<\/code> para campos de formul\u00e1rio). Com essa implementa\u00e7\u00e3o, temos um componente funcional que permite ao usu\u00e1rio iniciar e parar a transmiss\u00e3o de v\u00eddeo de forma intuitiva.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Qualidade de captura<\/h3>\n\n\n\n<p>A API permite manipular a qualidade de captura atrav\u00e9s de par\u00e2metros, conhecidos como <code>constraints<\/code>, que s\u00e3o fornecidos na inicializa\u00e7\u00e3o de um <code>MediaStream<\/code>. A API tentar\u00e1 configurar o <em>streaming<\/em> de forma a atender o m\u00e1ximo poss\u00edvel das restri\u00e7\u00f5es impostas. Algumas das <code>constraints<\/code> mais utilizadas s\u00e3o <code>width<\/code> e <code>height<\/code>.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Navegadores diferentes podem disponibilizar <code>constraints<\/code> diferentes. \u00c9 poss\u00edvel checar o que est\u00e1 dispon\u00edvel em um navegador espec\u00edfico atrav\u00e9s do comando <code>navigator.mediaDevices.getSupportedConstraints()<\/code>. \u00datil para garantir compatibilidade entre navegadores.<\/p>\n<\/blockquote>\n\n\n\n<p>Meu caso de uso requeria capturas no formato <strong>3:4<\/strong>, ent\u00e3o pensei em estabelecer um valor fixo de pixels de <strong>540&#215;720<\/strong>, por\u00e9m, isso significa que c\u00e2meras melhores n\u00e3o seriam usadas em sua capacidade m\u00e1xima e c\u00e2meras inferiores seriam incapazes de gerar imagens no formato desejado. Para garantir a melhor captura de v\u00eddeo poss\u00edvel, decidi calibrar a qualidade usando uma lista de resolu\u00e7\u00f5es <strong>3:4<\/strong> conhecidas. O processo de calibra\u00e7\u00e3o foi projetado para encontrar a melhor qualidade de captura para cada dispositivo, dentro das restri\u00e7\u00f5es de formato. Segue abaixo o processo de calibra\u00e7\u00e3o:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n\n \/\/ ...\n \n  async #setVideoStream() {\n    if (\"mediaDevices\" in navigator &amp;&amp; \"getUserMedia\" in navigator.mediaDevices) {\n      const calibration = await this.#calibrate()\n      const resolutions = this.#cameraResolutions()\n      const streamQuality = resolutions&#91;calibration]\n      const constraints = { video: streamQuality }\n\n      this.videoStream = await navigator.mediaDevices.getUserMedia(constraints)\n\n      this.displayTarget.srcObject = this.videoStream\n    }\n  }\n\n  async #calibrate() {\n    const resolutions = this.#cameraResolutions()\n    const keys = Object.keys(resolutions)\n\n    for (let i = keys.length - 1; i &gt;= 0; i--) {\n      let key = keys&#91;i]\n      let resolution = resolutions&#91;key]\n\n      try {\n        await navigator.mediaDevices.getUserMedia({ video: resolution })\n\n        return key\n      } catch (error) {\n        continue\n      }\n    }\n\n    throw new DOMException(\"Sem resolu\u00e7\u00f5es v\u00e1lidas\", \"NoResolutionError\")\n  }\n\n  #cameraResolutions() {\n    return {\n      custom: null, \/\/ no restrictions\n      sd: { width: { exact: 360 }, height: { exact: 480 } }, \/\/ 640x480\n      hd: { width: { exact: 540 }, height: { exact: 720 } }, \/\/ 1280x720\n      fhd: { width: { exact: 810 }, height: { exact: 1080 } }, \/\/ 1920x1080\n      qhd: { width: { exact: 1080 }, height: { exact: 1440 } }, \/\/ 2560x1440\n      uhd: { width: { exact: 1620 }, height: { exact: 2160 } } \/\/ 3840x2160\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>Antes de vincular o <code>MediaStream<\/code> ao <code>video<\/code>, \u00e9 realizada uma calibra\u00e7\u00e3o baseada em uma lista de resolu\u00e7\u00f5es <strong>3:4<\/strong>, entregando a melhor qualidade poss\u00edvel para o dispositivo usado. A calibra\u00e7\u00e3o percorre a lista, da maior qualidade conhecida at\u00e9 a menor, e se n\u00e3o for poss\u00edvel criar o fluxo de m\u00eddia com tais dimens\u00f5es, levanta o erro <code>OverconstrainedError<\/code>. Nesse caso, ser\u00e1 testada a qualidade logo abaixo, at\u00e9 que a primeira v\u00e1lida seja encontrada.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Otimiza\u00e7\u00e3o com Cookies<\/h5>\n\n\n\n<p>Realizar a calibra\u00e7\u00e3o sempre desperdi\u00e7a recursos e aumenta o tempo necess\u00e1rio para o carregamento. Para otimizar o componente, considere salvar a qualidade mais adequada em <code>cookies<\/code> e reutiliz\u00e1-la em futuras intera\u00e7\u00f5es.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>Salvar a qualidade no cookie ajuda a evitar calibra\u00e7\u00f5es desnecess\u00e1rias, reduzindo o tempo de carregamento nas visitas subsequentes. No entanto, \u00e9 importante garantir que o cookie seja atualizado caso o dispositivo ou prefer\u00eancias do usu\u00e1rio mudem. Voc\u00ea pode limpar ou atualizar o cookie quando necess\u00e1rio, garantindo que ele sempre contenha a melhor qualidade de captura dispon\u00edvel para o dispositivo.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Situa\u00e7\u00f5es em que existem mais de um dispositivo<\/h5>\n\n\n\n<p>Ao definir uma medida fixa para as dimens\u00f5es, <strong>a API tentar\u00e1 configurar o <em>streaming<\/em> de forma a atender o m\u00e1ximo poss\u00edvel das restri\u00e7\u00f5es impostas<\/strong>. Isso pode resultar no uso de diferentes c\u00e2meras, caso o dispositivo tenha mais de uma. Se houver m\u00faltiplas c\u00e2meras conectadas, considere utilizar o par\u00e2metro <code>deviceId<\/code> para escolher um dispositivo espec\u00edfico, evitando que a API selecione a c\u00e2mera incorreta.<\/p>\n\n\n\n<p>Ao definir um <code>deviceId<\/code>, voc\u00ea garante que a captura de v\u00eddeo ser\u00e1 feita a partir de um dispositivo espec\u00edfico. Isso pode ser \u00fatil quando h\u00e1 m\u00faltiplas c\u00e2meras conectadas ao dispositivo, como c\u00e2meras frontal e traseira.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Limita\u00e7\u00f5es no Firefox<\/h5>\n\n\n\n<p>O <strong>Firefox<\/strong> n\u00e3o permite manipular o <code>aspectRatio<\/code> da captura, o que impede a cria\u00e7\u00e3o de uma captura <strong>3:4<\/strong>. Para contornar isso, criamos a categoria <code>custom<\/code>, que n\u00e3o imp\u00f5e restri\u00e7\u00f5es \u00e0s dimens\u00f5es do v\u00eddeo. A manipula\u00e7\u00e3o de <code>aspectRatio<\/code> funciona corretamente em navegadores baseados em <strong>Chromium<\/strong>, como o Chrome.<\/p>\n\n\n\n<p>Para checar as configura\u00e7\u00f5es do <code>MediaStream<\/code> existente, utilize o seguinte comando: <code>MediaStream.getVideoTracks()[0].getSettings()<\/code>.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Capturar foto<\/h3>\n\n\n\n<p>O processo de captura consiste em extrair um <em>frame<\/em> do v\u00eddeo e colocar em um <code>canvas<\/code> para exibi-lo em tela. No caso de uso em quest\u00e3o, iremos exibir a imagem capturada sobre o <code>display<\/code>. Portanto, tamb\u00e9m ser\u00e1 necess\u00e1rio permitir que o usu\u00e1rio exclua a imagem.<\/p>\n\n\n\n<p>Abaixo, temos a implementa\u00e7\u00e3o em JavaScript:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  static targets = &#91;\"display\"]\n\n  videoStream = null\n  canvas = null\n  snapshot = null\n\n \/\/ ...\n\n  capture() {\n    const canvas = document.createElement(\"canvas\")\n\n    canvas.width = this.displayTarget.videoWidth\n    canvas.height = this.displayTarget.videoHeight\n    canvas.getContext(\"2d\").drawImage(this.displayTarget, 0, 0)\n\n    this.canvas = canvas\n    this.#setSnapshot()\n  }\n\n  eraseSnapshot() {\n    this.snapshot.remove()\n    this.snapshot = undefined\n    this.#toggleDisplay()\n  }\n\n \/\/ ...\n\n  #setSnapshot() {\n    const div = document.createElement(\"div\")\n    const img = document.createElement(\"img\")\n\n    img.src = this.canvas.toDataURL(\"image\/png\")\n    div.append(img, this.#eraseButton())\n    this.snapshot = div\n\n    this.#toggleDisplay()\n    this.stop()\n    this.displayTarget.after(div)\n  }\n\n  #eraseButton() {\n    const button = document.createElement(\"button\")\n\n    button.textContent = \"erase snapshot\"\n    button.type = \"button\"\n    button.dataset&#91;\"action\"] = \"click-&gt;camera#eraseSnapshot\"\n\n    return button\n  }\n\n  #toggleDisplay() {\n    const displayStyle = this.displayTarget.style.display\n    this.displayTarget.style.display = displayStyle === \"none\" ? \"block\" : \"none\"\n  }\n}<\/code><\/pre>\n\n\n\n<p>Adicionamos \u00e0 classe dois atributos: <code>canvas<\/code>, que armazena a imagem capturada, e <code>snapshot<\/code>, que representa o elemento <code>div<\/code> que cont\u00e9m a imagem e o bot\u00e3o de exclus\u00e3o. O <code>canvas<\/code> ser\u00e1 usado para criar o <em>snapshot<\/em>, que servir\u00e1 apenas para exibi\u00e7\u00e3o em tela.<\/p>\n\n\n\n<p>A fun\u00e7\u00e3o <code>capture()<\/code> cria o <code>canvas<\/code> a partir do v\u00eddeo e o armazena na classe. Em seguida, ela gera o <code>snapshot<\/code>, que substitui o <code>displayTarget<\/code> (o elemento <code>video<\/code> na tela). Al\u00e9m disso, um bot\u00e3o de exclus\u00e3o \u00e9 adicionado ao lado da imagem.<\/p>\n\n\n\n<p>Em rela\u00e7\u00e3o ao <strong>HTML<\/strong>, basta adicionar um bot\u00e3o vinculado \u00e0 a\u00e7\u00e3o de capturar a imagem:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># camera_helper.rb\n\nmodule CameraHelper\n  # ...\n\n  def camera_actions\n    content_tag(:div) do\n      concat(tag.button(\"play\", data: { action: \"click-&gt;camera#play\" }, type: \"button\"))\n      concat(tag.button(\"stop\", data: { action: \"click-&gt;camera#stop\" }, type: \"button\"))\n      concat(tag.button(\"capture\", data: { action: \"click-&gt;camera#capture\" }, type: \"button\"))\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Upload com ActiveStorage<\/h3>\n\n\n\n<p>Iremos utilizar o <strong>ActiveStorage<\/strong> para realizar o <em>upload<\/em> da foto. \u00c9 requerido que o <strong>model<\/strong> se relacione com o arquivo da seguinte forma:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>has_one_attached :photo<\/code><\/pre>\n\n\n\n<p>No formul\u00e1rio, ser\u00e1 preciso um <code>input<\/code> do tipo <code>file<\/code> para enviar o arquivo. No entanto, quem ir\u00e1 preencher esse campo n\u00e3o ser\u00e1 o usu\u00e1rio, mas sim o nosso <code>controller<\/code> <strong>Stimulus<\/strong>. Por isso, o <code>input<\/code> deve estar presente na estrutura do <code>controller<\/code> <strong>Stimulus<\/strong>. Quando o usu\u00e1rio capturar uma imagem, ela ser\u00e1 inserida automaticamente no <code>input<\/code>, que enviar\u00e1 o arquivo ao servidor quando o formul\u00e1rio for submetido.<\/p>\n\n\n\n<p>Primeiro, o JavaScript:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  static targets = &#91;\"display\", \"input\"]\n\n \/\/ ...\n\n  async capture() {\n    const canvas = document.createElement(\"canvas\")\n\n    canvas.width = this.displayTarget.videoWidth\n    canvas.height = this.displayTarget.videoHeight\n    canvas.getContext(\"2d\").drawImage(this.displayTarget, 0, 0)\n\n    this.canvas = canvas\n    await this.#upload()\n    this.#setSnapshot()\n  }\n\n  eraseSnapshot() {\n    this.snapshot.remove()\n    this.snapshot = undefined\n    this.#clearInput()\n    this.#toggleDisplay()\n  }\n\n \/\/ ...\n\n  async #upload() {\n    const blob = await new Promise(resolve =&gt; this.canvas.toBlob(resolve))\n    const file = new File(&#91;blob], \"photo\")\n    const dataTransfer = new DataTransfer()\n\n    dataTransfer.items.add(file)\n    this.inputTarget.files = dataTransfer.files\n  }\n\n  #clearInput() {\n    this.inputTarget.files = new DataTransfer().files\n  }\n}<\/code><\/pre>\n\n\n\n<p>Come\u00e7amos adicionando um novo <em>target<\/em> chamado <em>input<\/em>. Ao inserir em nosso <strong>HTML<\/strong> um elemento <code>input<\/code> e atribui-lo \u00e0 esse <em>target<\/em>, somos capazes de manipul\u00e1-lo facilmente. Ent\u00e3o, executamos a fun\u00e7\u00e3o <code>#upload()<\/code> durante a fun\u00e7\u00e3o <code>#capture()<\/code>, que agora se tornou ass\u00edncrona. Criamos um <code>blob<\/code> a partir do <code>canvas<\/code> e um <code>file<\/code> a partir do <code>blob<\/code>, para ent\u00e3o inseri-lo no <code>input<\/code> em um <code>DataTransfer<\/code>. Quando o formul\u00e1rio for enviado ao servidor, o arquivo tamb\u00e9m ser\u00e1. Para uma melhor experi\u00eancia, excluir o <em>snapshot<\/em> tamb\u00e9m remove o arquivo do <code>input<\/code>.<\/p>\n\n\n\n<p>Abaixo veremos as modifica\u00e7\u00f5es em nosso <code>helper<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># camera_helper.rb\n\nmodule CameraHelper\n  def camera(form, attribute)\n    content_tag(:div, data: { controller: \"camera\" }) do\n      concat(camera_display)\n      concat(camera_input(form, attribute))\n      concat(camera_actions)\n    end\n  end\n\n  # ...\n\n  def camera_input(form, attribute)\n    form.input(attribute, input_html: { data: { \"camera-target\": \"input\" } })\n  end\n\n  # ...\nend<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>O formul\u00e1rio utiliza a <strong>gem<\/strong> <code>simple_form<\/code>, que automaticamente cria um <code>input<\/code> do tipo <code>file<\/code> para atributos de arquivo.<\/p>\n<\/blockquote>\n\n\n\n<p>Com essa abordagem, conseguimos integrar o <strong>ActiveStorage<\/strong> ao <strong>Stimulus<\/strong> de forma transparente. Para uma experi\u00eancia ainda melhor, podemos ocultar o <code>input<\/code>, tornando-o invis\u00edvel ao usu\u00e1rio.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Conserva\u00e7\u00e3o de recursos<\/h3>\n\n\n\n<p>Para evitar o desperd\u00edcio de recursos do sistema e o aquecimento do dispositivo de m\u00eddia, lembre-se de fechar o <em>streaming<\/em> sempre que n\u00e3o estiver sendo usado. Se a c\u00e2mera permanecer ligada por longos per\u00edodos de tempo, ela pode reduzir a qualidade da captura, al\u00e9m de comprometer a vida \u00fatil do dispositivo. Utilizando o <strong>Stimulus<\/strong>, isso pode ser facilmente alcan\u00e7ado da seguinte maneira:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  \/\/ ...\n\n  disconnect() {\n    this.stop()\n  }\n\n  stop() {\n    this.videoStream.getTracks().forEach((track) =&gt; track.stop())\n  }\n\n  \/\/ ...\n}<\/code><\/pre>\n\n\n\n<p><code>disconnect()<\/code> \u00e9 uma fun\u00e7\u00e3o executada sempre que o elemento <code>controller<\/code> \u00e9 removido da <code>DOM<\/code>, garantindo que nossa c\u00e2mera seja desligada quando n\u00e3o for mais necess\u00e1ria.<\/p>\n\n\n\n<p>Vers\u00e3o final do c\u00f3digo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ camera_controller.js\n\nimport { Controller } from \"@hotwired\/stimulus\"\n\n\/\/ Connects to data-controller=\"camera\"\n\nexport default class extends Controller {\n  static targets = &#91;\"display\", \"input\"]\n\n  videoStream = null\n  canvas = null\n  snapshot = null\n\n  disconnect() {\n    this.stop()\n  }\n\n  async play() {\n    await this.#setVideoStream()\n  }\n \n  stop() {\n    this.videoStream.getTracks().forEach((track) =&gt; track.stop())\n  }\n\n  async capture() {\n    const canvas = document.createElement(\"canvas\")\n\n    canvas.width = this.displayTarget.videoWidth\n    canvas.height = this.displayTarget.videoHeight\n    canvas.getContext(\"2d\").drawImage(this.displayTarget, 0, 0)\n\n    this.canvas = canvas\n    await this.#upload()\n    this.#setSnapshot()\n  }\n\n  eraseSnapshot() {\n    this.snapshot.remove()\n    this.snapshot = undefined\n    this.#clearInput()\n    this.#toggleDisplay()\n  }\n\n  async #setVideoStream() {\n    if (\"mediaDevices\" in navigator &amp;&amp; \"getUserMedia\" in navigator.mediaDevices) {\n      const calibration = await this.#calibrate()\n      const resolutions = this.#cameraResolutions()\n      const streamQuality = resolutions&#91;calibration]\n      const constraints = { video: streamQuality }\n\n      this.videoStream = await navigator.mediaDevices.getUserMedia(constraints)\n\n      this.displayTarget.srcObject = this.videoStream\n    }\n  }\n\n  async #calibrate() {\n    const resolutions = this.#cameraResolutions()\n    const keys = Object.keys(resolutions)\n\n    for (let i = keys.length - 1; i &gt;= 0; i--) {\n      let key = keys&#91;i]\n      let resolution = resolutions&#91;key]\n\n      try {\n        await navigator.mediaDevices.getUserMedia({ video: resolution })\n\n        return key\n      } catch (error) {\n        continue\n      }\n    }\n\n    throw new DOMException(\"Sem resolu\u00e7\u00f5es v\u00e1lidas\", \"NoResolutionError\")\n  }\n\n  #cameraResolutions() {\n    return {\n      custom: null, \/\/ no restrictions\n      sd: { width: { exact: 360 }, height: { exact: 480 } }, \/\/ 640x480\n      hd: { width: { exact: 540 }, height: { exact: 720 } }, \/\/ 1280x720\n      fhd: { width: { exact: 810 }, height: { exact: 1080 } }, \/\/ 1920x1080\n      qhd: { width: { exact: 1080 }, height: { exact: 1440 } }, \/\/ 2560x1440\n      uhd: { width: { exact: 1620 }, height: { exact: 2160 } } \/\/ 3840x2160\n    }\n  }\n\n  #setSnapshot() {\n    const div = document.createElement(\"div\")\n    const img = document.createElement(\"img\")\n\n    img.src = this.canvas.toDataURL(\"image\/png\")\n    div.append(img, this.#eraseButton())\n    this.snapshot = div\n\n    this.#toggleDisplay()\n    this.stop()\n    this.displayTarget.after(div)\n  }\n\n  #eraseButton() {\n    const button = document.createElement(\"button\")\n\n    button.textContent = \"erase snapshot\"\n    button.type = \"button\"\n    button.dataset&#91;\"action\"] = \"click-&gt;camera#eraseSnapshot\"\n\n    return button\n  }\n\n  #toggleDisplay() {\n    const displayStyle = this.displayTarget.style.display\n    this.displayTarget.style.display = displayStyle === \"none\" ? \"block\" : \"none\"\n  }\n\n  async #upload() {\n    const blob = await new Promise(resolve =&gt; this.canvas.toBlob(resolve))\n    const file = new File(&#91;blob], \"photo\")\n    const dataTransfer = new DataTransfer()\n\n    dataTransfer.items.add(file)\n    this.inputTarget.files = dataTransfer.files\n  }\n\n  #clearInput() {\n    this.inputTarget.files = new DataTransfer().files\n  }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># camera_helper.rb\n\nmodule CameraHelper\n  def camera(form, attribute)\n    content_tag(:div, data: { controller: \"camera\" }) do\n      concat(camera_display)\n      concat(camera_input(form, attribute))\n      concat(camera_actions)\n    end\n  end\n\n  def camera_display\n    tag.video(autoplay: true, data: { \"camera-target\": \"display\" })\n  end\n\n  def camera_input(form, attribute)\n    form.input(attribute, input_html: { data: { \"camera-target\": \"input\" } })\n  end\n\n  def camera_actions\n    content_tag(:div) do\n      concat(tag.button(\"play\", data: { action: \"click-&gt;camera#play\" }, type: \"button\"))\n      concat(tag.button(\"stop\", data: { action: \"click-&gt;camera#stop\" }, type: \"button\"))\n      concat(tag.button(\"capture\", data: { action: \"click-&gt;camera#capture\" }, type: \"button\"))\n    end\n  end\nend<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Considera\u00e7\u00f5es finais<\/h3>\n\n\n\n<p>Integrar a API <strong>MediaDevices<\/strong> com <strong>Stimulus<\/strong> e <strong>Rails<\/strong> nos permite criar um componente simples, reutiliz\u00e1vel e altamente funcional para captura de m\u00eddia diretamente no navegador. Atrav\u00e9s do uso de <strong>Stimulus<\/strong>, conseguimos estruturar nossa l\u00f3gica de forma organizada, garantindo uma experi\u00eancia fluida para o usu\u00e1rio sem a necessidade de bibliotecas mais complexas.<\/p>\n\n\n\n<p>Por fim, vale lembrar que cada navegador pode ter diferentes permiss\u00f5es e restri\u00e7\u00f5es de seguran\u00e7a ao lidar com dispositivos de m\u00eddia. Portanto, sempre teste sua implementa\u00e7\u00e3o em diferentes ambientes e certifique-se de que a experi\u00eancia do usu\u00e1rio seja consistente e segura.<\/p>\n\n\n\n<p>Se voc\u00ea deseja expandir essa abordagem, pode considerar aprimoramentos como detec\u00e7\u00e3o facial, filtros de imagem em tempo real ou at\u00e9 mesmo integra\u00e7\u00e3o com WebRTC para chamadas de v\u00eddeo. O potencial da API <strong>MediaDevices<\/strong> vai muito al\u00e9m da simples captura de fotos, abrindo portas para diversas aplica\u00e7\u00f5es interativas e inovadoras.<\/p>\n\n\n\n<p class=\"has-text-align-right\">Escrito por: Fillipe Palhares<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Neste artigo, veremos como a biblioteca JavaScript Stimulus pode nos ajudar a integrar essa API em um ambiente Rails.<\/p>\n","protected":false},"author":2,"featured_media":1182,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[8],"tags":[838,842,844,840,845,841,843,833,836,835,834,839,837],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v19.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\r\n<title>Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus&quot;&quot;<\/title>\r\n<meta name=\"description\" content=\"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!\" \/>\r\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\r\n<link rel=\"canonical\" href=\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\" \/>\r\n<meta property=\"og:locale\" content=\"pt_BR\" \/>\r\n<meta property=\"og:type\" content=\"article\" \/>\r\n<meta property=\"og:title\" content=\"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus&quot;&quot;\" \/>\r\n<meta property=\"og:description\" content=\"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!\" \/>\r\n<meta property=\"og:url\" content=\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\" \/>\r\n<meta property=\"og:site_name\" content=\"Blog da Acsiv\" \/>\r\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/acsiv\" \/>\r\n<meta property=\"article:published_time\" content=\"2025-03-25T18:03:03+00:00\" \/>\r\n<meta property=\"article:modified_time\" content=\"2025-07-01T13:10:20+00:00\" \/>\r\n<meta property=\"og:image\" content=\"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2025\/03\/web_devices_image.webp\" \/>\r\n\t<meta property=\"og:image:width\" content=\"1792\" \/>\r\n\t<meta property=\"og:image:height\" content=\"1024\" \/>\r\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\r\n<meta name=\"author\" content=\"Eduardo Rodrigues\" \/>\r\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\r\n<meta name=\"twitter:creator\" content=\"@acsiv\" \/>\r\n<meta name=\"twitter:site\" content=\"@acsiv\" \/>\r\n<meta name=\"twitter:label1\" content=\"Escrito por\" \/>\n\t<meta name=\"twitter:data1\" content=\"Eduardo Rodrigues\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. tempo de leitura\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 minutos\" \/>\r\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#organization\",\"name\":\"Acsiv Sistema\",\"url\":\"https:\/\/acsiv.com.br\/blog\/\",\"sameAs\":[\"https:\/\/instagram.com\/acsivsistemas\",\"https:\/\/pt.linkedin.com\/company\/acsiv\",\"https:\/\/www.facebook.com\/acsiv\",\"https:\/\/twitter.com\/acsiv\"],\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pt-BR\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2020\/10\/Acsiv_logotipo.png\",\"contentUrl\":\"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2020\/10\/Acsiv_logotipo.png\",\"width\":1564,\"height\":668,\"caption\":\"Acsiv Sistema\"},\"image\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#website\",\"url\":\"https:\/\/acsiv.com.br\/blog\/\",\"name\":\"Blog da Acsiv\",\"description\":\"Conte\u00fado digital para cart\u00f3rios\",\"publisher\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/acsiv.com.br\/blog\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"pt-BR\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\",\"url\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\",\"name\":\"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\\\"\\\"\",\"isPartOf\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/#website\"},\"datePublished\":\"2025-03-25T18:03:03+00:00\",\"dateModified\":\"2025-07-01T13:10:20+00:00\",\"description\":\"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!\",\"breadcrumb\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#breadcrumb\"},\"inLanguage\":\"pt-BR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"In\u00edcio\",\"item\":\"https:\/\/acsiv.com.br\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\"}]},{\"@type\":\"Article\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\"},\"author\":{\"name\":\"Eduardo Rodrigues\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/54fd566d0efeb6dc731df7e4e38ce156\"},\"headline\":\"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\",\"datePublished\":\"2025-03-25T18:03:03+00:00\",\"dateModified\":\"2025-07-01T13:10:20+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/\"},\"wordCount\":2016,\"publisher\":{\"@id\":\"https:\/\/acsiv.com.br\/blog\/#organization\"},\"keywords\":[\"#ActiveStorage\",\"#APIs\",\"#BackEnd\",\"#Coding\",\"#DevTips\",\"#FrontEnd\",\"#FullStack\",\"#JavaScript\",\"#MediaDevices\",\"#Rails\",\"#StimulusJS\",\"#WebDevelopment\",\"#WebRTC\"],\"articleSection\":[\"Tecnologia\"],\"inLanguage\":\"pt-BR\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/54fd566d0efeb6dc731df7e4e38ce156\",\"name\":\"Eduardo Rodrigues\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"pt-BR\",\"@id\":\"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/fee13940cf1da650036b52bc338d5881?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/fee13940cf1da650036b52bc338d5881?s=96&d=mm&r=g\",\"caption\":\"Eduardo Rodrigues\"}}]}<\/script>\r\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\"\"","description":"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/","og_locale":"pt_BR","og_type":"article","og_title":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\"\"","og_description":"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!","og_url":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/","og_site_name":"Blog da Acsiv","article_publisher":"https:\/\/www.facebook.com\/acsiv","article_published_time":"2025-03-25T18:03:03+00:00","article_modified_time":"2025-07-01T13:10:20+00:00","og_image":[{"width":1792,"height":1024,"url":"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2025\/03\/web_devices_image.webp","type":"image\/webp"}],"author":"Eduardo Rodrigues","twitter_card":"summary_large_image","twitter_creator":"@acsiv","twitter_site":"@acsiv","twitter_misc":{"Escrito por":"Eduardo Rodrigues","Est. tempo de leitura":"16 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Organization","@id":"https:\/\/acsiv.com.br\/blog\/#organization","name":"Acsiv Sistema","url":"https:\/\/acsiv.com.br\/blog\/","sameAs":["https:\/\/instagram.com\/acsivsistemas","https:\/\/pt.linkedin.com\/company\/acsiv","https:\/\/www.facebook.com\/acsiv","https:\/\/twitter.com\/acsiv"],"logo":{"@type":"ImageObject","inLanguage":"pt-BR","@id":"https:\/\/acsiv.com.br\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2020\/10\/Acsiv_logotipo.png","contentUrl":"https:\/\/acsiv.com.br\/blog\/wp-content\/uploads\/2020\/10\/Acsiv_logotipo.png","width":1564,"height":668,"caption":"Acsiv Sistema"},"image":{"@id":"https:\/\/acsiv.com.br\/blog\/#\/schema\/logo\/image\/"}},{"@type":"WebSite","@id":"https:\/\/acsiv.com.br\/blog\/#website","url":"https:\/\/acsiv.com.br\/blog\/","name":"Blog da Acsiv","description":"Conte\u00fado digital para cart\u00f3rios","publisher":{"@id":"https:\/\/acsiv.com.br\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/acsiv.com.br\/blog\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"pt-BR"},{"@type":"WebPage","@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/","url":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/","name":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus\"\"","isPartOf":{"@id":"https:\/\/acsiv.com.br\/blog\/#website"},"datePublished":"2025-03-25T18:03:03+00:00","dateModified":"2025-07-01T13:10:20+00:00","description":"Aprenda a integrar a API MediaDevices com Stimulus e Rails para capturar e armazenar fotos com ActiveStorage!","breadcrumb":{"@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#breadcrumb"},"inLanguage":"pt-BR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"In\u00edcio","item":"https:\/\/acsiv.com.br\/blog\/"},{"@type":"ListItem","position":2,"name":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus"}]},{"@type":"Article","@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/#article","isPartOf":{"@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/"},"author":{"name":"Eduardo Rodrigues","@id":"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/54fd566d0efeb6dc731df7e4e38ce156"},"headline":"Uma vis\u00e3o sobre a utiliza\u00e7\u00e3o da API MediaDevices com Stimulus","datePublished":"2025-03-25T18:03:03+00:00","dateModified":"2025-07-01T13:10:20+00:00","mainEntityOfPage":{"@id":"https:\/\/acsiv.com.br\/blog\/2025\/03\/25\/uma-visao-sobre-a-utilizacao-da-api-mediadevices-com-stimulus\/"},"wordCount":2016,"publisher":{"@id":"https:\/\/acsiv.com.br\/blog\/#organization"},"keywords":["#ActiveStorage","#APIs","#BackEnd","#Coding","#DevTips","#FrontEnd","#FullStack","#JavaScript","#MediaDevices","#Rails","#StimulusJS","#WebDevelopment","#WebRTC"],"articleSection":["Tecnologia"],"inLanguage":"pt-BR"},{"@type":"Person","@id":"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/54fd566d0efeb6dc731df7e4e38ce156","name":"Eduardo Rodrigues","image":{"@type":"ImageObject","inLanguage":"pt-BR","@id":"https:\/\/acsiv.com.br\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/fee13940cf1da650036b52bc338d5881?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/fee13940cf1da650036b52bc338d5881?s=96&d=mm&r=g","caption":"Eduardo Rodrigues"}}]}},"_links":{"self":[{"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/posts\/1181"}],"collection":[{"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/comments?post=1181"}],"version-history":[{"count":8,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/posts\/1181\/revisions"}],"predecessor-version":[{"id":1256,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/posts\/1181\/revisions\/1256"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/media\/1182"}],"wp:attachment":[{"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/media?parent=1181"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/categories?post=1181"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/acsiv.com.br\/blog\/wp-json\/wp\/v2\/tags?post=1181"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}