{"id":22007,"date":"2020-02-10T16:16:25","date_gmt":"2020-02-10T16:16:25","guid":{"rendered":"http:\/\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary"},"modified":"2022-07-27T11:37:38","modified_gmt":"2022-07-27T18:37:38","slug":"building_a_camera_app_with_webrtc_vue_js_and_cloudinary","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary","title":{"rendered":"Building a Camera App With WebRTC, Vue.js, and Cloudinary"},"content":{"rendered":"\n<style>\n.btn.exp-btn {\n    text-transform: none;\n    padding: 10px 30px;\n    font-size: 1.1em;\n    border-radius: 40px;\n    margin-top: 20px;\n    margin-bottom: 40px;\n    display: inline-block;\n    position: relative;\n    max-width: 400px;\n}\n#exp-btn-carrot {margin-bottom: 1px; margin-left: 1px;}\n.btn.exp-btn::after {\n    content: \"\";\n    display: inline-block;\n    background-image: url(https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1657723837\/exp-btn.png);\n    position: absolute;\n    background-size: contain;\n    background-repeat: no-repeat;\n    width: 33px;\n    height: 70px;\n    top: 0px;\n    right: -22px;\n}\n@media (max-width: 992px) {\n    .btn.exp-btn {margin-left: 0;}\n}\n\n@media (max-width: 500px) {\n     .btn.exp-btn {\n         max-width: initial;\n         margin-left: auto!important;\n         margin-right: auto;\n         font-size: 15px;\n         width: 300px;\n         display: block;\n         padding: 10px;\n     }\n.btn.exp-btn:after {display: none;}\n}\n<\/style>\n<div style=\"display: flex; justify-content: center;\"><a href=\"#setup_on_cloudinary\" class=\"exp-btn btn btn-sm btn-primary c-subscription-cta__link\">Shortcut to Cloudinary\u2019s solution\n<svg id=\"exp-btn-carrot\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10.759\" height=\"11.117\" viewBox=\"0 0 10.759 11.117\">\n  <path id=\"Polygon_1\" data-name=\"Polygon 1\" d=\"M4.248,0,8.5,7.518H0Z\" transform=\"translate(6.511) rotate(60)\" fill=\"#fff\"><\/path>\n<\/svg>\n<\/a><\/div>\n\n\n<div class=\"wp-block-cloudinary-markdown \"><ul>\n<li>Check out the full <a href=\"https:\/\/github.com\/rebeccapeltz\/vue-camera\">code report<\/a>\n<\/li>\n<li>Try out the <a href=\"https:\/\/www.beckypeltz.me\/vue-camera\/#\/\">demo<\/a>\n<\/li>\n<\/ul>\n<p>Since 1999, WebRTC has served as an open-source and free solution for real-time audio and video communications within webpages, eliminating the need to use plugins, native apps, or other third-party software. I first became aware of WebRTC when Google and other browser vendors started supporting it. Many products, including Amazon Chime and Slack, have since jumped on the bandwagon.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_700,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/webrtc.png\" alt=\"webrtc\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"700\" height=\"280\"\/><\/p>\n<p>One notable WebRTC capability is that its API enables you to take pictures with the camera on the device that\u2019s rendering your webpage. In my new role as a developer instructional designer for Cloudinary, I recently built an instructional app, called Vue Camera, that integrates with our upload and delivery services and that can take pictures for upload to my Cloudinary account. This article describes the related components and process.<\/p>\n<h2>Vue Camera\u2019s Components<\/h2>\n<p>Vue Camera contains two routes:<\/p>\n<ul>\n<li>The home route, called Camera, has a component that takes pictures.<\/li>\n<li>The Gallery route displays pictures. This route and the Upload feature are disabled until after you\u2019ve specified a cloud name and an unsigned upload preset.<\/li>\n<\/ul>\n<p>Vue Camera is responsive and works well on desktop or mobile. Even though it can take pictures that face away from the device, its ability to do so depends on the device on which it\u2019s running. For example, I can only take selfies on my MacBook Pro but can take both selfies and outward pictures on my Google Pixel. On its home page, WebRTC states that it does not support all browsers.<\/p>\n<h2>The App Framework<\/h2>\n<p>The name Vue Camera clues you in on the JavaScript framework for building the app: Vue.js. Additionally, I used many APIs:<\/p>\n<ul>\n<li>WebRTC for streaming image data from a device camera<\/li>\n<li>Canvas toDataURL for representing formatted images as data<\/li>\n<li>Vue.js plugins:\n<ul>\n<li>Vue-bootstrap for responsive CSS<\/li>\n<li>Vuex, an implementation of the Flux container architecture<\/li>\n<li>Vue-ls for HTML5 local storage<\/li>\n<li>Vue-router for creating routable views<\/li>\n<\/ul>\n<\/li>\n<li>Cloudinary\u2019s Upload API for uploading images to a Cloudinary account<\/li>\n<li>Cloudinary\u2019s Product Gallery JavaScript function for rendering a responsive display of images in a Cloudinary account<\/li>\n<\/ul>\n<p>For details, see the <a href=\"https:\/\/github.com\/cloudinary-devs\/vue-camera-training\">complete code on Github<\/a>.<\/p>\n<h2>The Part Played by the APIs<\/h2>\n<p>Let\u2019s take a high-level look at how the APIs work together in Vue.js to create Vue Camera. Like many similar apps, Vue Camera performs these tasks:<\/p>\n<ul>\n<li>Collects data.<\/li>\n<li>Transmits the data to a database or static storage.<\/li>\n<li>Displays the data to users in another format.<\/li>\n<\/ul>\n<h3>WebRTC: Capture Data<\/h3>\n<p>The WebRTC APIs are asynchronous, promise-based browser calls that discover the source of the data and then capture it from the device they run on. Those APIs are wrapped in a component in Vue Camera.<\/p>\n<h3>Cloudinary Upload API: Save Data<\/h3>\n<p>Cloudinary offers an upload endpoint that can be requested by many HTTP clients, including Axios, XMLHttpRequest (XHR), jQuery, and Angular\u2019s HttpClient. Depending on the HTTP client, the code handles a callback or a promise. Axios posts data to Cloudinary in the Camera component.<\/p>\n<h3>Cloudinary Product Gallery: Display Data<\/h3>\n<p>A JavaScript function in Cloudinary renders a responsive set of images. Even though most frameworks work with plugins or npm libraries, this code wraps a JavaScript function call that binds to a Document Object Model (DOM) element from within a framework lifecycle. That way, you can take advantage of code that might not otherwise be readily available to the framework.<\/p>\n<p>You can build apps in the React and Angular frameworks with the same technique. As you move between app frameworks like Vue.js, Angular, and React, you organize, configure, and deliver captured data and render HTML, as appropriate. The frameworks have in common similar integrations with the outside world, following a component-based structure for modular development and ensuring that components are bound to the DOM. The idea is that you as the developer are creating HTML tags with your components, which is why you can integrate API functionalities in other frameworks just as you do with this Vue.js app.<\/p>\n<h2>Setup on Cloudinary<\/h2>\n<p>Now let\u2019s look at how Vue Camera works. To run the app from the <a href=\"https:\/\/cloudinary-devs.github.io\/vue-camera-training\/index.html#\/\">demo site<\/a>, <a href=\"https:\/\/cloudinary.com\/users\/register\/free\/?utm_content=cta-free-sign-up-sb-cam-webrtc\">sign up for a free Cloudinary account<\/a> and then create an unsigned upload preset to upload your pictures to Cloudinary and view them in the Gallery. Click the info icon (see the screenshot below) for details.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_700,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/vc-nav-info.png\" alt=\"navigation\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"48\"\/><\/p>\n<p>Here are the steps:<\/p>\n<ol>\n<li>Click the gear icon at the top of the Cloudinary console for the settings.<\/li>\n<li>Click <strong>Upload<\/strong> below the top navigation.<\/li>\n<li>Under <strong>Upload presets<\/strong>, click <strong>Enable unsigned uploads<\/strong> and then click <strong>Add upload preset<\/strong>.<\/li>\n<li>Under <strong>Upload preset name<\/strong>, type a name for the preset, e.g., <code>my-unsigned-preset<\/code>.<\/li>\n<li>Under <strong>Signing Mode<\/strong>, choose <strong>Unsigned<\/strong> from the drop-down menu.<\/li>\n<li>Click <strong>Save<\/strong> at the top.<\/li>\n<\/ol>\n<p>Cloudinary then displays the preset you just created under <strong>Upload presets<\/strong> in the next screen.\n<img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_700,c_fill,f_auto,fl_lossy,q_auto\/Web_Assets\/blog\/unsigned-upload-preset.gif\" alt=\"unsigned upload preset\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"700\" height=\"437\"\/><\/p>\n<p>Now open Vue Camera and register your cloud name and the preset. Click the gear icon for the <strong>Cloudinary Upload Info<\/strong> form and fill in the two text fields with your cloud name and preset name. Click <strong>OK<\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_700,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/vd-enter-cloudname-preset.png\" alt=\"preset\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"645\"\/><\/p>\n<p>Afterwards, Cloudinary displays the <strong>Upload<\/strong> button with the <strong>Gallery<\/strong> link enabled.<\/p>\n<p><strong>Important:<\/strong> The first time you run Vue Camera on a device, you must grant permission for the app to use the camera.<\/p>\n<p>You can now click <strong>Snapshot<\/strong> to take pictures and <strong>Upload<\/strong> to post them to your Cloudinary account.<\/p>\n<p>What do your cloud name and preset do behind the scenes? Cloudinary loads them into the Vuex store, making them available for use by all components. Cloudinary also stores them in the HTML5 local storage for one hour, saving you having to reregister them if you leave your browser and come back within that time.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_700,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/vc-local-storage.png\" alt=\"local storage\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"409\"\/><\/p>\n<p>In essence, the cloud name and preset act as credentials in Vue Camera, providing the information Cloudinary needs for its Upload API to post data. Note that you don\u2019t need credentials to download images to your local drive with Vue Camera. However, to save them to the cloud, you must register your cloud name and preset.<\/p>\n<h2>Implementation on WebRTC<\/h2>\n<p>The WebRTC code resides in the <a href=\"https:\/\/github.com\/rebeccapeltz\/vue-camera\/blob\/master\/src\/components\/Camera.vue\">camera component<\/a> in a model that includes media, devices, and constraints. The media is in the form of a data stream. The devices are detected and then opened with constraints, thus enabling the flow of media. See the snippet below.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">getDevices: <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n     <span class=\"hljs-keyword\">if<\/span> (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {\n       <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">false<\/span>;\n     }\n     <span class=\"hljs-keyword\">try<\/span> {\n       <span class=\"hljs-keyword\">let<\/span> allDevices = <span class=\"hljs-keyword\">await<\/span> navigator.mediaDevices.enumerateDevices();\n       <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> mediaDevice <span class=\"hljs-keyword\">of<\/span> allDevices) {\n         <span class=\"hljs-keyword\">if<\/span> (mediaDevice.kind === <span class=\"hljs-string\">\"videoinput\"<\/span>) {\n           <span class=\"hljs-keyword\">let<\/span> option = {};\n           option.text = mediaDevice.label;\n           option.value = mediaDevice.deviceId;\n           <span class=\"hljs-keyword\">this<\/span>.options.push(option);\n           <span class=\"hljs-keyword\">this<\/span>.devices.push(mediaDevice);\n         }\n       }\n       <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">true<\/span>;\n     } <span class=\"hljs-keyword\">catch<\/span> (err) {\n       <span class=\"hljs-keyword\">throw<\/span> err;\n     }\n   }\n },\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The HTML code loads with an HTML5 <code>video<\/code> tag and a hidden <code>canvas<\/code> tag.  WebRTC directs the video stream to the <code>video<\/code> tag so that when the user clicks <strong>Snapshot<\/strong>, WebRTC reads the video frame as a JPEG data URI, ready for handoff to the Cloudinary Upload API. See the snippet below.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"> snapshot: <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n     <span class=\"hljs-keyword\">this<\/span>.canvas.width = <span class=\"hljs-keyword\">this<\/span>.video.videoWidth;\n     <span class=\"hljs-keyword\">this<\/span>.canvas.height = <span class=\"hljs-keyword\">this<\/span>.video.videoHeight;\n     <span class=\"hljs-keyword\">this<\/span>.canvas\n       .getContext(<span class=\"hljs-string\">\"2d\"<\/span>)\n       .drawImage(<span class=\"hljs-keyword\">this<\/span>.video, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-keyword\">this<\/span>.canvas.width, <span class=\"hljs-keyword\">this<\/span>.canvas.height);\n     <span class=\"hljs-keyword\">this<\/span>.fileData = <span class=\"hljs-keyword\">this<\/span>.canvas.toDataURL(<span class=\"hljs-string\">\"image\/jpeg\"<\/span>);\n     <span class=\"hljs-keyword\">this<\/span>.isPhoto = <span class=\"hljs-literal\">true<\/span>;\n     <span class=\"hljs-keyword\">this<\/span>.cameraState = <span class=\"hljs-literal\">false<\/span>;\n     <span class=\"hljs-comment\">\/\/remove any hidden links used for download<\/span>\n     <span class=\"hljs-keyword\">let<\/span> hiddenLinks = <span class=\"hljs-built_in\">document<\/span>.querySelectorAll(<span class=\"hljs-string\">\".hidden_links\"<\/span>);\n     <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> hiddenLink <span class=\"hljs-keyword\">of<\/span> hiddenLinks) {\n       <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">\"body\"<\/span>).remove(hiddenLink);\n     }\n   }\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Cloudinary Upload API<\/h2>\n<p>The <code>uploadToCloudinary<\/code> function packages the registered cloud name and preset with the file data obtained in the snapshot described above as a <code>FormData<\/code> object. I posted the data to Cloudinary with the Axios library so the cloud name becomes part of the upload endpoint.<\/p>\n<p>You can organize media on Cloudinary in many ways. One of them is through tags, which, like metadata, can help you identify your assets. To take advantage of that capability, I added a tag named <code>browser_upload<\/code>, with which the Gallery component pulls only the pictures taken by Vue Camera. See the snippet below.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadToCloudinary<\/span>(<span class=\"hljs-params\">cloudName, preset, fileData<\/span>) <\/span>{\n <span class=\"hljs-keyword\">try<\/span> {\n   <span class=\"hljs-keyword\">let<\/span> fd = <span class=\"hljs-keyword\">new<\/span> FormData();\n   <span class=\"hljs-keyword\">let<\/span> url = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${cloudName}<\/span>\/image\/upload`<\/span>;\n   fd.append(<span class=\"hljs-string\">\"upload_preset\"<\/span>, preset);\n   fd.append(<span class=\"hljs-string\">\"tags\"<\/span>, <span class=\"hljs-string\">\"browser_upload\"<\/span>);\n   fd.append(<span class=\"hljs-string\">\"file\"<\/span>, fileData);\n   <span class=\"hljs-keyword\">let<\/span> res = <span class=\"hljs-keyword\">await<\/span> axios({\n     <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">\"post\"<\/span>,\n     <span class=\"hljs-attr\">url<\/span>: url,\n     <span class=\"hljs-attr\">data<\/span>: fd\n   });\n   <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">await<\/span> res.data;\n } <span class=\"hljs-keyword\">catch<\/span> (err) {\n   <span class=\"hljs-keyword\">throw<\/span> err;\n }\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>If the upload succeeds, the function that calls <code>uploadToCloudinary<\/code> notifies the user with a \u201ctoast\u201d message. The code below shows such a message that\u2019s five seconds long.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">this<\/span>.$bvToast.toast(\n         <span class=\"hljs-string\">`Upload to Cloudinary unsuccessful: use settings to provide cloudname and preset`<\/span>,\n         {\n           <span class=\"hljs-attr\">title<\/span>: <span class=\"hljs-string\">\"Cloudinary Upload\"<\/span>,\n           <span class=\"hljs-attr\">autoHideDelay<\/span>: <span class=\"hljs-number\">5000<\/span>,\n           <span class=\"hljs-attr\">appendToast<\/span>: <span class=\"hljs-literal\">false<\/span>\n         }\n       )\n\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>If you\u2019ve entered a nonexistent cloud name or preset, a fail error-message is displayed.<\/p>\n<h2>Gallery<\/h2>\n<p>Now comes the step to show off the images. Enter Cloudinary\u2019s Product Gallery widget, available as a JavaScript function that binds output to a DOM element, much like what you would do in jQuery or Vanilla JavaScript. So, how to include that function in a framework?<\/p>\n<p>The <a href=\"https:\/\/github.com\/rebeccapeltz\/vue-camera\/blob\/master\/src\/views\/Gallery.vue\">Gallery component code<\/a> shows how I \u201cwrapped\u201d the Gallery function in a component\u2019s script with a Vue.js lifecycle hook. First, when the component elements are added to the virtual DOM, Vue.js calls the mounted function, gets an instance of the Product Gallery widget, and renders the Product Gallery output to the DOM. Finally, to prevent memory leaks, the function called by the <code>beforeDestroy<\/code> lifecycle hook destroys the gallery element when exiting the component view. See the snippet below.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">mounted() {\n   <span class=\"hljs-comment\">\/\/get cloudname and preset from local storage<\/span>\n   <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">this<\/span>.$ls.get(<span class=\"hljs-string\">\"cloudname\"<\/span>)) {\n     <span class=\"hljs-keyword\">this<\/span>.cloudname = <span class=\"hljs-keyword\">this<\/span>.$ls.get(<span class=\"hljs-string\">\"cloudname\"<\/span>);\n   }\n   <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">this<\/span>.$ls.get(<span class=\"hljs-string\">\"preset\"<\/span>)) {\n     <span class=\"hljs-keyword\">this<\/span>.preset = <span class=\"hljs-keyword\">this<\/span>.$ls.get(<span class=\"hljs-string\">\"preset\"<\/span>);\n   }\n   <span class=\"hljs-comment\">\/\/if these aren't set don't allow browse<\/span>\n   <span class=\"hljs-comment\">\/*global cloudinary*\/<\/span>\n   <span class=\"hljs-comment\">\/*eslint no-undef: \"error\"*\/<\/span>\n   <span class=\"hljs-keyword\">this<\/span>.myGallery = cloudinary.galleryWidget({\n     <span class=\"hljs-attr\">container<\/span>: <span class=\"hljs-string\">\"#images\"<\/span>,\n     <span class=\"hljs-attr\">cloudName<\/span>: <span class=\"hljs-keyword\">this<\/span>.cloudname,\n     <span class=\"hljs-attr\">mediaAssets<\/span>: &#91;{ <span class=\"hljs-attr\">tag<\/span>: <span class=\"hljs-string\">\"browser_upload\"<\/span> }]\n   });\n   <span class=\"hljs-keyword\">this<\/span>.myGallery.render();\n },\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Be sure to add links for the widget CSS and JavaScript to your <code>public\/index.html<\/code> file, which is not part of the webpacked code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">head<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">charset<\/span>=<span class=\"hljs-string\">\"utf-8\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">http-equiv<\/span>=<span class=\"hljs-string\">\"X-UA-Compatible\"<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">\"IE=edge\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"viewport\"<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">\"width=device-width,initial-scale=1.0\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">link<\/span> <span class=\"hljs-attr\">rel<\/span>=<span class=\"hljs-string\">\"icon\"<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"&lt;%= BASE_URL %&gt;favicon.ico\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">title<\/span>&gt;<\/span>vue-camera<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">title<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">link<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"\/\/fonts.googleapis.com\/css?family=Roboto:300,400,500,700\"<\/span> <span class=\"hljs-attr\">rel<\/span>=<span class=\"hljs-string\">\"stylesheet\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"text\/css\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">link<\/span> <span class=\"hljs-attr\">rel<\/span>=<span class=\"hljs-string\">\"stylesheet\"<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\"https:\/\/use.fontawesome.com\/releases\/v5.11.2\/css\/all.css\"<\/span> <span class=\"hljs-attr\">integrity<\/span>=<span class=\"hljs-string\">\"sha384-KA6wR\/X5RY4zFAHpv\/CnoG2UW1uogYfdnP67Uv7eULvTveboZJg0qUpmJZb5VqzN\"<\/span> <span class=\"hljs-attr\">crossorigin<\/span>=<span class=\"hljs-string\">\"anonymous\"<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">link<\/span> <span class=\"hljs-attr\">rel<\/span>=<span class=\"hljs-string\">\"stylesheet\"<\/span> <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">\".\/css\/main.css\"<\/span>&gt;<\/span>\n \n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">head<\/span>&gt;<\/span>\n \n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">body<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">noscript<\/span>&gt;<\/span>\n   <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">strong<\/span>&gt;<\/span>We're sorry but vue-camera doesn't work properly without JavaScript enabled. Please enable it to\n     continue.<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">strong<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">noscript<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"app\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/product-gallery.cloudinary.com\/all.js\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/span>&gt;<\/span>\n <span class=\"hljs-comment\">&lt;!-- built files will be auto injected --&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">body<\/span>&gt;<\/span>\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The Product Gallery widget then becomes responsive, as shown in the screenshot below the desktop view.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_400,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/vc-gallery-desktop.png\" alt=\"gallery\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"400\" height=\"204.5\"\/><\/p>\n<p>The image below shows the mobile view along with a carousel for maintaining a responsive layout.\n<img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_400,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/vc-mobile-gallery.png\" alt=\"mobile gallery\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"400\" height=\"711\"\/><\/p>\n<h2>Conclusion<\/h2>\n<p>The Vue Camera app serves as instructional code only and is not intended to replace Google Photos or Mac\u2019s Photo Booth. You can add more capabilities, e.g., device constraints that change the size of the video-stream frame and a feature that shares the Cloudinary link. You can also use the <a href=\"https:\/\/cloudinary.com\/documentation\/vue_integration\">Cloudinary Vue.js SDK plugin<\/a> to transform images.<\/p>\n<p>If you are running Vue Camera on both desktop and mobile, feel free to use it as a tool for self-reflection. The image below shows me taking a selfie in the desktop version with the mobile version.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/image\/upload\/w_200,c_fill,f_auto,q_auto,dpr_2.0\/Web_Assets\/blog\/laptop.png\" alt=\"laptop\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"200\" height=\"355.5\"\/>\nAs a side note, I presented the material from this article at the <a href=\"https:\/\/www.meetup.com\/seattle-api\/events\/264745882\/\">API Meetup in Seattle, Washington<\/a> in November 2019. Gratifyingly, it elicited a lot of interest from the audience.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Shortcut to Cloudinary\u2019s solution<\/p>\n","protected":false},"author":41,"featured_media":22008,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[315],"class_list":["post-22007","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-vue"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Create a Camera App With WebRTC, Vue.js, and Cloudinary<\/title>\n<meta name=\"description\" content=\"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Camera App With WebRTC, Vue.js, and Cloudinary\" \/>\n<meta property=\"og:description\" content=\"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2020-02-10T16:16:25+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2022-07-27T18:37:38+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"1540\" \/>\n\t<meta property=\"og:image:height\" content=\"847\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Building a Camera App With WebRTC, Vue.js, and Cloudinary\",\"datePublished\":\"2020-02-10T16:16:25+00:00\",\"dateModified\":\"2022-07-27T18:37:38+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\"},\"wordCount\":297,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA\",\"keywords\":[\"Vue\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2020\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\",\"url\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\",\"name\":\"Create a Camera App With WebRTC, Vue.js, and Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA\",\"datePublished\":\"2020-02-10T16:16:25+00:00\",\"dateModified\":\"2022-07-27T18:37:38+00:00\",\"description\":\"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA\",\"width\":1540,\"height\":847},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Camera App With WebRTC, Vue.js, and Cloudinary\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Create a Camera App With WebRTC, Vue.js, and Cloudinary","description":"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.","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:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary","og_locale":"en_US","og_type":"article","og_title":"Building a Camera App With WebRTC, Vue.js, and Cloudinary","og_description":"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.","og_url":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary","og_site_name":"Cloudinary Blog","article_published_time":"2020-02-10T16:16:25+00:00","article_modified_time":"2022-07-27T18:37:38+00:00","og_image":[{"width":1540,"height":847,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356-jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary"},"author":{"name":"","@id":""},"headline":"Building a Camera App With WebRTC, Vue.js, and Cloudinary","datePublished":"2020-02-10T16:16:25+00:00","dateModified":"2022-07-27T18:37:38+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary"},"wordCount":297,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA","keywords":["Vue"],"inLanguage":"en-US","copyrightYear":"2020","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary","url":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary","name":"Create a Camera App With WebRTC, Vue.js, and Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA","datePublished":"2020-02-10T16:16:25+00:00","dateModified":"2022-07-27T18:37:38+00:00","description":"Learn how to build an instructional app with WebRTC, Vue.js, and APIs to integrate with Cloudinary and take pictures for upload to Cloudinary.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA","width":1540,"height":847},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/building_a_camera_app_with_webrtc_vue_js_and_cloudinary#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Building a Camera App With WebRTC, Vue.js, and Cloudinary"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649720537\/Web_Assets\/blog\/Vue-Camera-App-Tutorial-v1_22008ba356\/Vue-Camera-App-Tutorial-v1_22008ba356.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/22007","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=22007"}],"version-history":[{"count":4,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/22007\/revisions"}],"predecessor-version":[{"id":24711,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/22007\/revisions\/24711"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/22008"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=22007"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=22007"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=22007"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}