{"id":28417,"date":"2022-03-23T22:05:42","date_gmt":"2022-03-23T22:05:42","guid":{"rendered":"http:\/\/Build-A-Virtual-Photo-Booth-with-Redwood"},"modified":"2022-03-23T22:05:42","modified_gmt":"2022-03-23T22:05:42","slug":"build-a-virtual-photo-booth-with-redwood","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/","title":{"rendered":"Build A Virtual Photo Booth"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>There are a lot of useful tools built into the browser that we don\u2019t take advantage of as much as we could. Working with <a href=\"https:\/\/webrtc.org\/getting-started\/overview\">WebRTC<\/a> is one of those tools that doesn\u2019t come up as often as it could.<\/p>\n<p>Do you have an app where a user can upload photos or videos? Why not let them capture that media right there on your site instead of getting them to dig up a photo from somewhere? Or maybe you want to make some kind of custom video call app. WebRTC is one tool you can use to do that.<\/p>\n<p>In this tutorial, you\u2019ll learn how to build a full-stack photo booth app that applies filters to images and videos and uploads them to Cloudinary, while saving a link to them in your own database. Hopefully at the end of this, you\u2019ll have a better understanding of how WebRTC works and one of the use cases for it.<\/p>\n<h2>Setting up the tools we need<\/h2>\n<p>There are a few things we need to have in place before we get started on code. First, we\u2019ll be using a PostgreSQL database locally. If you don\u2019t have that installed, you can download it for <a href=\"https:\/\/www.postgresql.org\/download\/\">free here<\/a>.<\/p>\n<p>Next, you\u2019ll need to have a Cloudinary account set up so you can upload the images and get the URL for your database. If you don\u2019t have a Cloudinary account, you can make a <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">free one here<\/a>.<\/p>\n<p>The last thing we need to do is initialize the Redwood app we\u2019re going to build. Open a terminal and run the following command.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn create redwood-app --typescript photobooth\n<\/code><\/span><\/pre>\n<p>This will create a number of files and directories with different pre-built functionality. We\u2019ll do all of our work in the <code>api<\/code> and <code>web<\/code> directories. The <code>api<\/code> directory holds all of the work for the back-end and the <code>web<\/code> directory contains all of the front-end code.<\/p>\n<p>Let\u2019s start by adding the business logic for the app on the back-end.<\/p>\n<h2>Writing the database model<\/h2>\n<p>For this app, we want to upload the images a user takes to Cloudinary and then save the URL to the database. This is one of the ways you can have this image available in different parts of your web app.<\/p>\n<p>Go to the <code>api &gt; db<\/code> folder and open the <code>schema.prisma<\/code> file. This is where we\u2019ll define the tables and relations for our database. Let\u2019s start by updating the <code>provider<\/code> to <code>postgresql<\/code> instead of <code>sqlite<\/code>.<\/p>\n<p>Then you\u2019ll see the reference to <code>DATABASE_URL<\/code>. This is an environment variable that defines the database connection string. So open the <code>.env<\/code> file in the root of the project and uncomment the <code>DATABASE_URL<\/code> line and update it with your connection string. It might look something like this.<\/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\">DATABASE_URL=postgres:<span class=\"hljs-comment\">\/\/postgres:admin@localhost:5432\/photobooth<\/span>\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>This will let the app establish a connection to the database so you can work with the data you want to store. Now back in the <code>schema.prisma<\/code> file, let\u2019s write our photo model. You can delete the example model and then add the following code.<\/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\">model Photo {\n  id     Int    @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  url    <span class=\"hljs-built_in\">String<\/span> @unique\n  userId <span class=\"hljs-built_in\">String<\/span> @unique\n  user   User   @relation(fields: &#91;userId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n}\n\nmodel User {\n  id    <span class=\"hljs-built_in\">String<\/span> @id @<span class=\"hljs-keyword\">default<\/span>(uuid())\n  name  <span class=\"hljs-built_in\">String<\/span>\n  photo Photo?\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<p>We\u2019ve defined a couple of models to show how these photos might be related to a specific user. The photos will have their own attributes and will be associated with a user based on the <code>userId<\/code>. Then we have a user model defined that has a few attributes.<\/p>\n<h3>Seeding the database<\/h3>\n<p>Since we aren\u2019t going to build out the functionality to manage users, we\u2019re going to add a default user to the database so that we have an id to reference when we\u2019re ready to upload pictures.<\/p>\n<p>In the <code>api &gt; db<\/code> directory, you\u2019ll see a <code>seed.js<\/code> file. This is where we\u2019ll add the default user\u2019s information. There is a lot of commented out code in the <code>main<\/code> function. Feel free to delete everything in the <code>main<\/code> function and add this code.<\/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\">const<\/span> data = &#91;\n  { <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'alice'<\/span> },\n]\n\n<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">Promise<\/span>.all(\n  data.map(<span class=\"hljs-keyword\">async<\/span> (user) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> record = <span class=\"hljs-keyword\">await<\/span> db.user.create({\n      <span class=\"hljs-attr\">data<\/span>: { <span class=\"hljs-attr\">name<\/span>: user.name },\n    })\n    <span class=\"hljs-built_in\">console<\/span>.log(record)\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>This adds one user record to the database. With the models and seed data ready, we can run a migration to get these changes to the database.<\/p>\n<h3>Running the migration<\/h3>\n<p>In your terminal, run the following commands.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw primsa migrate dev\nyarn rw prisma db seed\n<\/code><\/span><\/pre>\n<p>This will create the database and add two tables defined by our photo and user models. Then we add the default user to the database. That covers everything we need for our database. Now we can move on to the GraphQL back-end.<\/p>\n<h2>Working with types and resolvers in GraphQL<\/h2>\n<p>Since we\u2019re working in the Redwood framework, there are a lot of commands we can use to generate a lot of the code we need. Normally to make a GraphQL back-end, you have to manually check that your types match the database schema exactly and that your resolvers call the right methods to trigger database changes.<\/p>\n<p>We\u2019re going to run a couple of commands that will create the types and resolvers we need for both models.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g sdl user\nyarn rw g sdl --crud photo\n<\/code><\/span><\/pre>\n<p>Take a look in the <code>api &gt; src &gt; graphql<\/code> directory and you\u2019ll see two new files. These sdl files have the types for the queries and mutations we need to use for our GraphQL resolvers. Open the <code>photos.sdl.ts<\/code> file and you\u2019ll see all of the types for the functionality we need to work with photos.<\/p>\n<p>You\u2019ll see similar types in the <code>users.sdl.ts<\/code> file, but since we added the <code>--crud<\/code> flag to the photo command we get a little extra functionality done for us. Now let\u2019s look at the resolvers.<\/p>\n<p>Go to <code>api &gt; src &gt; services<\/code> and you\u2019ll see a couple of new folders. These folders have two test related files and one file with the resolvers for that respective table. Open <code>photos.ts<\/code> and you\u2019ll see all of the resolvers for the CRUD functionality.<\/p>\n<p>This is one of my favorite things about Redwood. If you want to get a functional app quickly, it generates all of the code you need. With those two commands, we\u2019re done building the back-end.<\/p>\n<p>Now we can turn our attention to the front-end where some of the fun stuff happens.<\/p>\n<h2>Generating the page for our photo booth<\/h2>\n<p>First thing we need to do on the front-end is generate the page that will hold the photo booth. There\u2019s a handy Redwood command to do this. In your terminal, run this command.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g page photobooth \/\n<\/code><\/span><\/pre>\n<p>This will create a new folder in <code>web &gt; src &gt; pages<\/code> called <code>PhotoboothPage<\/code>. In that folder, you\u2019ll find a test file, a <a href=\"https:\/\/storybook.js.org\/\">Storybook<\/a> file, and the page component. It also updates the <code>Routes.tsx<\/code> file to make this the home page route.<\/p>\n<p>Open the <code>Photobooth.tsx<\/code> file in <code>web &gt; src &gt; pages &gt; PhotoboothPage<\/code> because this is where we\u2019ll be doing all of the coding. Let\u2019s start by deleting all of the imports and the code inside the <code>PhotoboothPage<\/code> component.<\/p>\n<h3>Writing the create mutation<\/h3>\n<p>Then we\u2019ll add the mutation to create new photo entries in our database. That means we\u2019ll import a mutation hook at the top of the file and right beneath it, we\u2019ll define the mutation.<\/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\">import<\/span> { useMutation } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> CREATE_PHOTO_MUTATION = gql<span class=\"hljs-string\">`\n  mutation CreatePhotoMutation($input: CreatePhotoInput!) {\n    createPhoto(input: $input) {\n      id\n    }\n  }\n`<\/span>\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>This uses a Redwood wrapper on <a href=\"https:\/\/www.apollographql.com\/\">Apollo<\/a> to work with the mutation we\u2019ve defined. Inside of the <code>PhotoboothPage<\/code> component, we\u2019ll use this hook and definition to make a function we can use to execute the upload when a user takes a photo.<\/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\"><span class=\"hljs-keyword\">const<\/span> &#91;createPhoto] = useMutation(CREATE_PHOTO_MUTATION)\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>That\u2019s all for the mutation! Now we\u2019ll add another import so we can use a few different hooks. So at the top of the file, right below the <code>useMutation<\/code> import, add the following.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { useEffect, useRef, useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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>Now we\u2019ll add a few states and refs we\u2019ll be using. Inside the component, below the <code>createPhoto<\/code> method, add this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> videoRef = useRef()\n<span class=\"hljs-keyword\">const<\/span> canvasRef = useRef()\n<span class=\"hljs-keyword\">const<\/span> &#91;mediaStream, setMediaStream] = useState(<span class=\"hljs-literal\">null<\/span>)\n<span class=\"hljs-keyword\">const<\/span> &#91;src, setSrc] = useState(<span class=\"hljs-literal\">null<\/span>)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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><code>videoRef<\/code> is how we\u2019ll interact with the video element that will show the user\u2019s camera in the browser. This is where we get to play with the WebRTC stuff. <code>canvasRef<\/code> is how we\u2019ll take a snapshot of the current frame of the video when the user wants to capture the picture.<\/p>\n<p><code>mediaStream<\/code> is how we\u2019ll get the feed from a user\u2019s camera. <code>src<\/code> is the image data for the snapshot a user takes. It lets us show the user the image as soon as they take the picture.<\/p>\n<p>Let\u2019s write out the functions we need before we start adding elements to the page.<\/p>\n<h3>Getting everything wired up<\/h3>\n<p>We want to request access to the user\u2019s camera as soon as they land on our page. To do that, we\u2019ll use the <code>useEffect<\/code> hook. Beneath the last state declaration in the component, add this code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">enableStream<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n    <span class=\"hljs-keyword\">const<\/span> stream = <span class=\"hljs-keyword\">await<\/span> navigator.mediaDevices.getUserMedia({\n      <span class=\"hljs-attr\">video<\/span>: <span class=\"hljs-literal\">true<\/span>,\n      <span class=\"hljs-attr\">audio<\/span>: <span class=\"hljs-literal\">false<\/span>,\n    })\n    setMediaStream(stream)\n  }\n\n  <span class=\"hljs-keyword\">if<\/span> (!mediaStream) {\n    enableStream()\n  }\n}, &#91;mediaStream])\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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>This is where we get to use the WebRTC stuff! Calling <code>getUserMedia<\/code> with the options we passed opens a user\u2019s camera but doesn\u2019t open their mic. We don\u2019t need access to their mic to take a picture. This goes into some data ethics with us taking the least amount of information from a user we need.<\/p>\n<p>Now when the page loads or there are any changes to the user\u2019s camera settings, the media stream will be updated. The next thing we need to do is set the media stream in the video element we\u2019ll make shortly. For now, add this code below the hook we just finished.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">if (mediaStream &amp;&amp; videoRef.current &amp;&amp; !videoRef.current.srcObject) {\n  videoRef.current.srcObject = mediaStream\n}\n<\/code><\/span><\/pre>\n<p>This checks that we have a media stream and a video element available. Then it sets the source of the video element to the media stream. This is how we show the camera in the browser.<\/p>\n<p>Next we have a small function to make the video play once the user has given us permission. This goes below the video check we just added.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> handleCanPlay = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  videoRef.current.play()\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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>Now we have the largest function in our component. It will handle the upload to Cloudinary and the mutation to add the photo record to the database.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> uploadImage = <span class=\"hljs-keyword\">async<\/span> (imgSrc) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> uploadApi = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${cloudName}<\/span>\/image\/upload`<\/span>\n\n  <span class=\"hljs-keyword\">const<\/span> formData = <span class=\"hljs-keyword\">new<\/span> FormData()\n  formData.append(<span class=\"hljs-string\">'file'<\/span>, imgSrc)\n  formData.append(<span class=\"hljs-string\">'upload_preset'<\/span>, uploadPreset)\n\n  <span class=\"hljs-keyword\">const<\/span> cloudinaryRes = <span class=\"hljs-keyword\">await<\/span> fetch(uploadApi, {\n    <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n    <span class=\"hljs-attr\">body<\/span>: formData,\n  })\n\n  <span class=\"hljs-keyword\">const<\/span> input = {\n    <span class=\"hljs-attr\">url<\/span>: cloudinaryRes.url,\n    <span class=\"hljs-attr\">userId<\/span>: <span class=\"hljs-string\">'1efeb34e-287f-11ec-9621-0242ac130002'<\/span>,\n  }\n\n  createPhoto({\n    <span class=\"hljs-attr\">variables<\/span>: { input },\n  })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><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>First, there\u2019s the upload API. You can get your cloud name from your Cloudinary dashboard. You might want to grab an upload preset while you\u2019re in the dashboard as well. That\u2019s where the <code>uploadPreset<\/code> value comes from in the form data. The <code>file<\/code> value will be the image data we get from the canvas.<\/p>\n<p>Then we make a <code>fetch<\/code> request to the Cloudinary endpoint and take the <code>url<\/code> to store in the database. You can find the <code>userId<\/code> for the seeded user we made earlier directly in your Postgres instance and just paste it in there. At the very end, we add the photo record to the database.<\/p>\n<p>Only one more function left! We\u2019re going to get the image data from the canvas.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> takePicture = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> context = canvasRef.current.getContext(<span class=\"hljs-string\">'2d'<\/span>)\n\n  context.drawImage(videoRef.current, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">580<\/span>, <span class=\"hljs-number\">320<\/span>)\n\n  <span class=\"hljs-keyword\">const<\/span> src = canvasRef.current.toDataURL()\n  setSrc(src)\n\n  uploadImage(src)\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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>This gets the context of the canvas element so that we can capture the video frame and get the image data. Then we call the <code>uploadImage<\/code> method we just wrote.<\/p>\n<p>We\u2019re finished with all of the functions now! All that\u2019s is rendering elements on the page.<\/p>\n<h2>Rendering elements for the photo booth<\/h2>\n<p>We finally get to add that beautiful return statement. This is the last bit of code we need to write to get everything working. This will be the last thing inside the <code>PhotoboothPage<\/code> component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">return<\/span> (\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>Photobooth<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">video<\/span>\n      <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"video\"<\/span>\n      <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{videoRef}<\/span>\n      <span class=\"hljs-attr\">onCanPlay<\/span>=<span class=\"hljs-string\">{handleCanPlay}<\/span>\n      <span class=\"hljs-attr\">autoPlay<\/span>\n      <span class=\"hljs-attr\">playsInline<\/span>\n      <span class=\"hljs-attr\">muted<\/span>\n    &gt;<\/span>\n      Video stream not available.\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">video<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{takePicture}<\/span>&gt;<\/span>Take photo<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">canvas<\/span>\n      <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">none<\/span>' }}\n      <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{canvasRef}<\/span>\n      <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">{580}<\/span>\n      <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">{320}<\/span>\n    &gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">canvas<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n      <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"photo\"<\/span>\n      <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"The screen capture will appear in this box.\"<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{src}<\/span>\n    \/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><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 <code>&lt;video&gt;<\/code> element has the <code>videoRef<\/code> we setup earlier and it calls the <code>handleCanPlay<\/code> function we wrote to start up the video stream. Then we have a button that lets users take pictures when they\u2019re ready.<\/p>\n<p>Next is the <code>&lt;canvas&gt;<\/code> element with our <code>canvasRef<\/code> as a prop. Lastly, there\u2019s the <code>&lt;img&gt;<\/code> element that lets users see the image they just took.<\/p>\n<p>Now we can run the app and finally see all of our hard work in action! In your terminal, run this command.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw dev\n<\/code><\/span><\/pre>\n<p>Your browser should open and ask you for permission to access your camera. Once you give it permission, you should see something like this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1633735770\/e-603fc55d218a650069f5228b\/c6ywx4bqe3x6hxfikwmx.png\" alt=\"showing the camera, button, and empty image on the page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1092\"\/><\/p>\n<p>If you take a picture, it\u2019ll look similar to this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1633735809\/e-603fc55d218a650069f5228b\/zebbui5zialujq6m8vtv.png\" alt=\"the camera, button, and a captured image\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"924\"\/><\/p>\n<p>We\u2019re done and now you know how to get started with WebRTC! I\u2019ll leave any style work to you, but hopefully you see how this could be useful.<\/p>\n<h2>Finished code<\/h2>\n<p>If you want to check out the complete front-end and back-end code, you can see everything in the <a href=\"https:\/\/github.com\/flippedcoder\/blog-examples\/tree\/main\/photobooth\"><code>photobooth<\/code> folder of this repo<\/a>.<\/p>\n<p>You can also check out the front-end in <a href=\"https:\/\/codesandbox.io\/s\/angry-rosalind-8zwln\">this Code Sandbox<\/a>.<\/p>\n<h2>Conclusion<\/h2>\n<p>There are times when you\u2019ll run into these kinds of seemingly obscure use cases for different web functionality, but they can be super handy. You might end up working on a video chat app for doctors or handle some facial recognition software for a security company.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":28418,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,370,177,246,371,303],"class_list":["post-28417","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-image","tag-javascript","tag-react","tag-under-review","tag-video"],"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>Build A Virtual Photo Booth<\/title>\n<meta name=\"description\" content=\"Working with WebRTC has gained more use as we&#039;ve all turned to online video calls. It&#039;s one of the things that&#039;s good to have in your toolbox as a JavaScript developer. In this tutorial, we&#039;ll make a quick photo booth to allow user&#039;s to take and store images from their camera.\" \/>\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\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build A Virtual Photo Booth\" \/>\n<meta property=\"og:description\" content=\"Working with WebRTC has gained more use as we&#039;ve all turned to online video calls. It&#039;s one of the things that&#039;s good to have in your toolbox as a JavaScript developer. In this tutorial, we&#039;ll make a quick photo booth to allow user&#039;s to take and store images from their camera.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-23T22:05:42+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Build A Virtual Photo Booth\",\"datePublished\":\"2022-03-23T22:05:42+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\"},\"wordCount\":5,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"Image\",\"Javascript\",\"React\",\"Under Review\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\",\"name\":\"Build A Virtual Photo Booth\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA\",\"datePublished\":\"2022-03-23T22:05:42+00:00\",\"description\":\"Working with WebRTC has gained more use as we've all turned to online video calls. It's one of the things that's good to have in your toolbox as a JavaScript developer. In this tutorial, we'll make a quick photo booth to allow user's to take and store images from their camera.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA\",\"width\":5806,\"height\":3868},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build A Virtual Photo Booth\"}]},{\"@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":"Build A Virtual Photo Booth","description":"Working with WebRTC has gained more use as we've all turned to online video calls. It's one of the things that's good to have in your toolbox as a JavaScript developer. In this tutorial, we'll make a quick photo booth to allow user's to take and store images from their camera.","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\/guest_post\/build-a-virtual-photo-booth-with-redwood\/","og_locale":"en_US","og_type":"article","og_title":"Build A Virtual Photo Booth","og_description":"Working with WebRTC has gained more use as we've all turned to online video calls. It's one of the things that's good to have in your toolbox as a JavaScript developer. In this tutorial, we'll make a quick photo booth to allow user's to take and store images from their camera.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-23T22:05:42+00:00","twitter_card":"summary_large_image","twitter_image":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/"},"author":{"name":"","@id":""},"headline":"Build A Virtual Photo Booth","datePublished":"2022-03-23T22:05:42+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/"},"wordCount":5,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","keywords":["Guest Post","Image","Javascript","React","Under Review","Video"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/","name":"Build A Virtual Photo Booth","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","datePublished":"2022-03-23T22:05:42+00:00","description":"Working with WebRTC has gained more use as we've all turned to online video calls. It's one of the things that's good to have in your toolbox as a JavaScript developer. In this tutorial, we'll make a quick photo booth to allow user's to take and store images from their camera.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","width":5806,"height":3868},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/build-a-virtual-photo-booth-with-redwood\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Build A Virtual Photo Booth"}]},{"@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\/v1681924556\/Web_Assets\/blog\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3\/c5e3e4adfc23de224ecd03a9aec2d50640ae806c-5806x3868-1_28418dbbb3.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28417","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=28417"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28417\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/28418"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=28417"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=28417"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=28417"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}