{"id":27918,"date":"2022-03-23T22:59:46","date_gmt":"2022-03-23T22:59:46","guid":{"rendered":"http:\/\/How-To-Make-A-Custom-Video-Player-in-Redwood"},"modified":"2022-03-23T22:59:46","modified_gmt":"2022-03-23T22:59:46","slug":"how-to-make-a-custom-video-player-in-redwood","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/","title":{"rendered":"How To Make A Custom Video Player"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Seeing videos on the web is expected at this point. It\u2019s one of the methods we use to teach ourselves new things or entertain ourselves. With videos being used so commonly, it\u2019s important for us to make good user interfaces for them.<\/p>\n<p>When your users are interacting with videos, you want to make sure they can easily control how the video plays and what features they have access to. Most video controls are right there on the video, but it doesn\u2019t have to be that way.<\/p>\n<p>In this tutorial, we\u2019re going to create a custom video player. It\u2019ll have multiple options for a user to enter and they\u2019ll be saved as user settings. When the page is reloaded, the selected video options will be reloaded as well.<\/p>\n<h2>Setting up Redwood<\/h2>\n<p>Let\u2019s start by talking about what Redwood is. It\u2019s a full-stack JavaScript framework that uses React, GraphQL, and Prisma to handle everything from the front-end to the database. It has built-in testing and Storybook support, plus a bunch of other great features. You can learn more about it in <a href=\"https:\/\/redwoodjs.com\/docs\/introduction\">the Redwood docs<\/a>.<\/p>\n<p>Now let\u2019s create a new Redwood app. You can do that by running:<\/p>\n<p><code>yarn create redwood-app custom-video-player<\/code><\/p>\n<p>Once that command finishes (it might take a few minutes), you\u2019ll have a fully functional full-stack app. The directory it creates has a number of files and subdirectories. The two most important directories are <code>api<\/code> and <code>web<\/code>.<\/p>\n<p>The <code>api<\/code> folder holds all of the code for the GraphQL back-end and the Prisma model for the Postgres database. The <code>web<\/code> folder holds all of the React front-end code. We\u2019ll be updating the code in these folders throughout this tutorial.<\/p>\n<p>You can run the app now to see what it looks like and how it loads with:<\/p>\n<p><code>yarn rw dev<\/code><\/p>\n<p>This command starts the GraphQL server and the front-end. The running app should look 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\/v1624027054\/e-603fc55d218a650069f5228b\/c1ihipnd9tdect5zkapy.png\" alt=\"base Redwood app\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1132\"\/><\/p>\n<p>Since we know the app is working, let\u2019s update some back-end code.<\/p>\n<h2>Handling the video data<\/h2>\n<p>We want to save a user\u2019s preferences for how a video is displayed and how it behaves, like will it run on a loop. To do that, we need to create a Prisma schema for Postgres database we\u2019ll connect to. I\u2019ll be connecting to a local Postgres instance and you can learn more about how to set that up on the <a href=\"https:\/\/www.postgresql.org\/download\/\">Postgres downloads page<\/a>.<\/p>\n<p>In the <code>.env<\/code> file, uncomment the <code>DATABASE_URL<\/code> line and update it to match your Postgres instance. Here\u2019s an example of what mine looks like. Make sure you remember what your username and password are for your local Postgres server!<\/p>\n<p><code>DATABASE_URL=postgres:\/\/postgres:admin@localhost:5432\/video_player<\/code><\/p>\n<p>Next, we need to update our Prisma schema so open the <code>schema.prisma<\/code> file. We\u2019re going to create the model for the <code>Setting<\/code> table we need to hold the user values. Redwood already generated an example model, so we can just swap out the names of everything. Update your file to look 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\">datasource db {\n  provider = <span class=\"hljs-string\">\"postgresql\"<\/span>\n  url      = env(<span class=\"hljs-string\">\"DATABASE_URL\"<\/span>)\n}\n\ngenerator client {\n  provider      = <span class=\"hljs-string\">\"prisma-client-js\"<\/span>\n  binaryTargets = <span class=\"hljs-string\">\"native\"<\/span>\n}\n\nmodel Setting {\n  id           Int     @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  videoName    <span class=\"hljs-built_in\">String<\/span>  @unique\n  loop         <span class=\"hljs-built_in\">Boolean<\/span>\n  volume       Float   @<span class=\"hljs-keyword\">default<\/span>(<span class=\"hljs-number\">0.5<\/span>)\n  controls     <span class=\"hljs-built_in\">Boolean<\/span> @<span class=\"hljs-keyword\">default<\/span>(<span class=\"hljs-literal\">false<\/span>)\n  playbackRate Float   @<span class=\"hljs-keyword\">default<\/span>(<span class=\"hljs-number\">1.5<\/span>)\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>This is how we tell our app to use Postgres as the database and how the tables should be structured. The <code>Setting<\/code> model defines the settings we\u2019re saving for the user and the data types we expect for them. The last three options have default values so that a video will play whether or not the user picks any particular options.<\/p>\n<p>Now we\u2019ll add one piece of seed data. When we run our migration, it\u2019ll be nice to already have a user setting generated to start with. We aren\u2019t creating a new setting each time, we\u2019re just updating it. That means we need to have a setting already in place to update.<\/p>\n<p>In the <code>seed.js<\/code> file, we\u2019re going to add one row of data to the <code>Setting<\/code> table. Update your file to look like this.<\/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\"><span class=\"hljs-comment\">\/* eslint-disable no-console *\/<\/span>\n<span class=\"hljs-keyword\">const<\/span> { PrismaClient } = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'@prisma\/client'<\/span>)\n<span class=\"hljs-keyword\">const<\/span> dotenv = <span class=\"hljs-built_in\">require<\/span>(<span class=\"hljs-string\">'dotenv'<\/span>)\n\ndotenv.config()\n<span class=\"hljs-keyword\">const<\/span> db = <span class=\"hljs-keyword\">new<\/span> PrismaClient()\n\n<span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">main<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-built_in\">console<\/span>.warn(<span class=\"hljs-string\">'Please define your seed data.'<\/span>)\n\n  <span class=\"hljs-keyword\">const<\/span> record = <span class=\"hljs-keyword\">await<\/span> db.setting.create({\n    <span class=\"hljs-attr\">data<\/span>: {\n      <span class=\"hljs-attr\">videoName<\/span>: <span class=\"hljs-string\">'elephant_herd'<\/span>,\n      <span class=\"hljs-attr\">loop<\/span>: <span class=\"hljs-literal\">false<\/span>,\n      <span class=\"hljs-attr\">controls<\/span>: <span class=\"hljs-literal\">true<\/span>,\n      <span class=\"hljs-attr\">volume<\/span>: <span class=\"hljs-number\">0.2<\/span>,\n      <span class=\"hljs-attr\">playbackRate<\/span>: <span class=\"hljs-number\">1.5<\/span>,\n    },\n  })\n  <span class=\"hljs-built_in\">console<\/span>.log(record)\n}\n\nmain()\n  .catch(<span class=\"hljs-function\">(<span class=\"hljs-params\">e<\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">console<\/span>.error(e))\n  .finally(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n    <span class=\"hljs-keyword\">await<\/span> db.$disconnect()\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>Now all that\u2019s left to do for our database is create and run a migration. To do this, run the following command:<\/p>\n<p><code>yarn rw prisma migrate dev<\/code><\/p>\n<p>This creates a new migration and seeds the data. Now when we get ready to connect the front-end, there will already be a setting to update. With the database ready to go, it\u2019s time to create our GraphQL server.<\/p>\n<h2>Building the GraphQL server<\/h2>\n<p>Redwood does a lot of things for you pretty quickly.<\/p>\n<h3>GraphQL schema and resolvers<\/h3>\n<p>We\u2019re about to run a command that will generate the GraphQL schema and resolvers.<\/p>\n<p><code>yarn rw g sdl setting<\/code><\/p>\n<p>If you take a look in the <code>api &gt; src &gt; graphql<\/code> directory, you\u2019ll find all of the GraphQL types based on the Prisma schema you need to do some basic operations. Now look in the <code>api &gt; src &gt; services<\/code> directory. There\u2019s a <code>settings<\/code> folder that has the file for one resolver.<\/p>\n<h3>Updating the base GraphQL files<\/h3>\n<p>Since we\u2019re in the <code>settings.js<\/code> with the resolver, let\u2019s add a couple more resolvers to handle our front-end requests. The first resolver will get an individual setting based on the setting ID. The second resolver will be used to handle updates to the setting.<\/p>\n<p>Add the following code after the <code>settings<\/code> resolver in the file.<\/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\">export<\/span> <span class=\"hljs-keyword\">const<\/span> setting = <span class=\"hljs-function\">(<span class=\"hljs-params\">input<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> db.setting.findFirst({\n    <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: input.id },\n  })\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> updateSetting = <span class=\"hljs-function\">(<span class=\"hljs-params\">{ input }<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">`This is the input: + <span class=\"hljs-subst\">${input.volume}<\/span>`<\/span>)\n  <span class=\"hljs-keyword\">return<\/span> db.setting.update({\n    <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: input.id },\n    <span class=\"hljs-attr\">data<\/span>: {\n      <span class=\"hljs-attr\">loop<\/span>: input.loop,\n      <span class=\"hljs-attr\">videoName<\/span>: input.videoName,\n      <span class=\"hljs-attr\">controls<\/span>: input.controls,\n      <span class=\"hljs-attr\">volume<\/span>: input.volume,\n      <span class=\"hljs-attr\">playbackRate<\/span>: input.playbackRate,\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>Then you\u2019ll need to update the <code>settings.sdl.js<\/code> file to have the matching schema for these new resolvers.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">type Mutation {\n  updateSetting(input: UpdateSettingInput): Setting\n}\n\ntype Query {\n  setting(id: Int): Setting!\n}\n<\/code><\/span><\/pre>\n<p>You\u2019ll also need to add the <code>id<\/code> field to the <code>UpdateSettingInput<\/code> type so that we\u2019re able to update based on the setting ID.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">input<\/span> <span class=\"hljs-selector-tag\">UpdateSettingInput<\/span> {\n  <span class=\"hljs-attribute\">id<\/span>: Int\n  videoName: String\n  loop: Boolean\n  volume: Float\n  controls: Boolean\n  playbackRate: Float\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\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>With the new resolvers and updated GraphQL schema in place, we can finally move on to the front-end.<\/p>\n<h2>Making the UI for the video player<\/h2>\n<p>This is something else that Redwood handles very well for us. The command we\u2019re about to run will create a new page and add the routing for us. We\u2019re going to make a new home page that displays at the root URL of the app.<\/p>\n<p><code>yarn rw g page home \/<\/code><\/p>\n<p>If you take a look in the <code>web &gt; src &gt; pages<\/code> directory, you\u2019ll see a new <code>HomePage<\/code> directory. This is where the home page we created with the previous command is located. We\u2019re going to make our video player in this file, but if you want to see what the app looks like in the browser now, run:<\/p>\n<p><code>yarn rw dev<\/code><\/p>\n<p>Your page should 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\/v1624027199\/e-603fc55d218a650069f5228b\/avl9ncu5riglyum60spm.png\" alt=\"Home page of Redwood app\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1073\"\/><\/p>\n<h3>Creating the video player<\/h3>\n<p>Now we\u2019ll work on the way our custom video player will look to users. It won\u2019t be the fanciest CSS work, but it\u2019ll be usable! You\u2019ll need to install a couple of packages inside the <code>web<\/code> directory. Once you\u2019re in the <code>web<\/code> directory in your terminal, run the following command:<\/p>\n<p><code>yarn add styled-components react-player<\/code><\/p>\n<p>We\u2019ll be using <code>styled-components<\/code> to add some simple styling to the video player and we\u2019re using <code>react-player<\/code> as the video player component. Let\u2019s start by completely updating the <code>Home<\/code> component.<\/p>\n<p>We\u2019re going to import some Redwood form components to give users custom control over their video player. This is how we\u2019ll be able to save those settings. The form values will be connected to the video player a little later, so we just need the UI in place.<\/p>\n<p>There will be a couple of styled components to space things a little better on the screen. We\u2019re also importing the video from Cloudinary and we\u2019ll talk about how to set that up in a bit.<\/p>\n<p>Update your <code>HomePage.js<\/code> file to have the following code.<\/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\">import<\/span> {\n  Form,\n  Label,\n  TextField,\n  CheckboxField,\n  RangeField,\n  RadioField,\n  Submit,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/forms'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useMutation, useQuery } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n<span class=\"hljs-keyword\">import<\/span> styled <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'styled-components'<\/span>\n<span class=\"hljs-keyword\">import<\/span> ReactPlayer <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-player'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> HomePage = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Container<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">VideoPlayer<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ReactPlayer<\/span>\n          <span class=\"hljs-attr\">controls<\/span>=<span class=\"hljs-string\">{true}<\/span>\n          <span class=\"hljs-attr\">loop<\/span>=<span class=\"hljs-string\">{false}<\/span>\n          <span class=\"hljs-attr\">volume<\/span>=<span class=\"hljs-string\">{0.5}<\/span>\n          <span class=\"hljs-attr\">playbackRate<\/span>=<span class=\"hljs-string\">{1}<\/span>\n          <span class=\"hljs-attr\">url<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">https:<\/span>\/\/<span class=\"hljs-attr\">res.cloudinary.com<\/span>\/<span class=\"hljs-attr\">milecia<\/span>\/<span class=\"hljs-attr\">video<\/span>\/<span class=\"hljs-attr\">upload<\/span>\/<span class=\"hljs-attr\">c_pad<\/span>,<span class=\"hljs-attr\">h_360<\/span>,<span class=\"hljs-attr\">w_480<\/span>,<span class=\"hljs-attr\">q_70<\/span>,<span class=\"hljs-attr\">du_10<\/span>\/<span class=\"hljs-attr\">elephant_herd.mp4<\/span>`}\n        &gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ReactPlayer<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">VideoPlayer<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Form<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormContainer<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"videoName\"<\/span>&gt;<\/span>Video Name<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TextField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"videoName\"<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"loop\"<\/span>&gt;<\/span>Loop<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CheckboxField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"loop\"<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"controls\"<\/span>&gt;<\/span>Controls<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CheckboxField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"controls\"<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"volume\"<\/span>&gt;<\/span>Volume<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RangeField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"volume\"<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>1x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{1}<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>1.5x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{1.5}<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>2x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{2}<\/span> \/&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Submit<\/span>&gt;<\/span>Save<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Submit<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">FormContainer<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Form<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Container<\/span>&gt;<\/span><\/span>\n  )\n}\n\n<span class=\"hljs-keyword\">const<\/span> Container = styled.div<span class=\"hljs-string\">`\n  width: 100%;\n`<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> FormContainer = styled.div<span class=\"hljs-string\">`\n  display: flex;\n  flex-direction: column;\n  margin: 0 auto;\n  padding-top: 25px;\n  width: 500px;\n`<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> VideoPlayer = styled.div<span class=\"hljs-string\">`\n  display: block;\n  margin: 0 auto;\n  width: 50%;\n`<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> HomePage\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>With this code on your home page, you should see something like this in the browser when you run your app.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1624027153\/e-603fc55d218a650069f5228b\/zvlj9ybbsgynk5swrewf.png\" alt=\"video player\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1144\"\/><\/p>\n<h3>Adding the GraphQL calls<\/h3>\n<p>Since there\u2019s a form, we probably need to connect it to the back-end to store and retrieve data for the custom video player. It\u2019s time to add our GraphQL requests on the front-end.<\/p>\n<p>Inside the <code>HomePage.js<\/code> file, we\u2019re going to add a new import to the others in order to create a query and mutation.<\/p>\n<p><code>import { useMutation, useQuery } from '@redwoodjs\/web'<\/code><\/p>\n<p>Now add the following code inside of the <code>HomePage<\/code> component. This will create the methods for updating and retriving the user settings and create the <code>onSubmit<\/code> method for the form. Since we seeded the database and we\u2019re only working with one user, I\u2019ve hard-coded the setting ID as <code>1<\/code>. We even do some state handling for when the data is being fetched in the GraphQL query.<\/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\">const<\/span> { loading, error, data } = useQuery(SETTING, { <span class=\"hljs-attr\">variables<\/span>: { <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-number\">1<\/span> } })\n\n<span class=\"hljs-keyword\">const<\/span> &#91;updateSetting] = useMutation(UPDATE_SETTING)\n\n<span class=\"hljs-keyword\">const<\/span> onSubmit = <span class=\"hljs-function\">(<span class=\"hljs-params\">data<\/span>) =&gt;<\/span> {\n  updateSetting({\n    <span class=\"hljs-attr\">variables<\/span>: {\n      <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-number\">1<\/span>,\n      <span class=\"hljs-attr\">videoName<\/span>: data.videoName,\n      <span class=\"hljs-attr\">loop<\/span>: data.loop,\n      <span class=\"hljs-attr\">controls<\/span>: data.controls,\n      <span class=\"hljs-attr\">volume<\/span>: <span class=\"hljs-built_in\">Number<\/span>(data.volume),\n      <span class=\"hljs-attr\">playbackRate<\/span>: <span class=\"hljs-built_in\">Number<\/span>(data.playbackRate),\n    },\n  })\n}\n\n<span class=\"hljs-keyword\">if<\/span> (loading) {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>Loading..<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n}\n\n<span class=\"hljs-keyword\">if<\/span> (error) {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>{error.message}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/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\">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 need to add the variables that define the shape of the GraphQL requests we want to execute. We\u2019ll use the GraphQL query language syntax to create these requests and define the data we want to send and return. Right after the <code>HomePage<\/code> component ends, add the following code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> SETTING = gql`\n  query Setting($id: Int) {\n    setting(id: $id) {\n      id\n      videoName\n      loop\n      controls\n      volume\n      playbackRate\n    }\n  }\n`\n\n<span class=\"hljs-keyword\">const<\/span> UPDATE_SETTING = gql`\n  mutation UpdateSetting(\n    $id: Int\n    $videoName: String\n    $loop: Boolean\n    $controls: Boolean\n    $volume: Float\n    $playbackRate: Float\n  ) {\n    updateSetting(\n      input: {\n        id: $id\n        videoName: $videoName\n        loop: $loop\n        controls: $controls\n        volume: $volume\n        playbackRate: $playbackRate\n      }\n    ) {\n      id\n      videoName\n      loop\n      controls\n      volume\n      playbackRate\n    }\n  }\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The last thing we have to do is update our form to submit the update when we click save and load the values returned from the query. We\u2019re going to update the video URL to use the <code>videoName<\/code> we saved and add <code>defaultValue<\/code> attributes to all of the form fields to show the stored values.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" 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\">VideoPlayer<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ReactPlayer<\/span>&gt;<\/span>\n  ...\n  url={`https:\/\/res.cloudinary.com\/milecia\/video\/upload\/c_pad,h_360,w_480,q_70,du_10\/${\n      data.setting.videoName || 'elephant_herd'\n    }.mp4`}\n  &gt;<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ReactPlayer<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">VideoPlayer<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Form<\/span> <span class=\"hljs-attr\">onSubmit<\/span>=<span class=\"hljs-string\">{onSubmit}<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">FormContainer<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"videoName\"<\/span>&gt;<\/span>Video Name<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TextField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"videoName\"<\/span> <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.videoName}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"loop\"<\/span>&gt;<\/span>Loop<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CheckboxField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"loop\"<\/span> <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.loop}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"controls\"<\/span>&gt;<\/span>Controls<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CheckboxField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"controls\"<\/span> <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.controls}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"volume\"<\/span>&gt;<\/span>Volume<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RangeField<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"volume\"<\/span> <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.volume}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>1x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span>\n      <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>\n      <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.playbackRate}<\/span>\n      <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{1}<\/span>\n    \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>1.5x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span>\n      <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>\n      <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.playbackRate}<\/span>\n      <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{1.5}<\/span>\n    \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>&gt;<\/span>2x<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">RadioField<\/span>\n      <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"playbackRate\"<\/span>\n      <span class=\"hljs-attr\">defaultValue<\/span>=<span class=\"hljs-string\">{data.setting.playbackRate}<\/span>\n      <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{2}<\/span>\n    \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Submit<\/span>&gt;<\/span>Save<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Submit<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">FormContainer<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Form<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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>Now you\u2019re able to give your users a custom video experience every time they come to your app! There\u2019s just one more thing we need to do before we can call this finished.<\/p>\n<p>You need to know how to fetch these videos from Cloudinary.<\/p>\n<h3>Working with Cloudinary<\/h3>\n<p>The video that\u2019s currently displaying is being loaded from Cloudinary. The string that we\u2019ve been using for the <code>url<\/code> value of the video player is how where this comes in. That string currently looks like this:<\/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\">url={<span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/milecia\/video\/upload\/c_pad,h_360,w_480,q_70,du_10\/<span class=\"hljs-subst\">${\n  data.setting.videoName || <span class=\"hljs-string\">'elephant_herd'<\/span>\n}<\/span>.mp4`<\/span>}\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>This is the URL to a video hosted on Cloudinary, but you\u2019ll want it to point to your account. If you don\u2019t have an account, you can <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">create a free one here<\/a>. After you\u2019ve registered, log in and you\u2019ll be taken to your Dashboard. You can find your cloud name here.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1624027285\/e-603fc55d218a650069f5228b\/jxivmfyxqxjiko8hoj29.png\" alt=\"cloud name in the Cloudinary dashboard\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"634\" height=\"310\"\/><\/p>\n<p>The next thing you\u2019ll need to do is go to your Media Library and upload a few videos. The video names are what a user will be able to enter in the form we created.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1624027320\/e-603fc55d218a650069f5228b\/lsqt68qn7paet0vyh3w6.png\" alt=\"videos in Cloudinary\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1054\"\/><\/p>\n<p>In the <code>url<\/code> string, replace <code>milecia<\/code> with your cloud name and replace <code>elephant_herd<\/code> with the name of one of your videos. Now when you run your Redwood app, you\u2019ll see your own video!<\/p>\n<h2>Finished code<\/h2>\n<p>If you want to take a look at the front-end with this <a href=\"https:\/\/codesandbox.io\/s\/gallant-bell-em4ll?file=\/src\/App.js\">CodesandBox<\/a>.<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/gallant-bell-em4ll?theme=dark&amp;codemirror=1&amp;highlights=&amp;editorsize=50&amp;fontsize=14&amp;expanddevtools=0&amp;hidedevtools=0&amp;eslint=0&amp;forcerefresh=0&amp;hidenavigation=0&amp;initialpath=%2F&amp;module=&amp;moduleview=0&amp;previewwindow=&amp;view=&amp;runonclick=1\"\n      height=\"500\"\n      style=\"width: 100%;\"\n      title=\"gallant-bell-em4ll\"\n      loading=\"lazy\"\n      allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n      sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n    ><\/iframe>\n  <\/div>\n\n  <div class=\"wp-block-cloudinary-markdown \"><p>If you want the entire project, including the front-end and back-end, check out the <a href=\"https:\/\/github.com\/flippedcoder\/blog-examples\/tree\/main\/custom-video-player\"><code>custom-video-player<\/code> folder in this repo<\/a>!<\/p>\n<h2>Conclusion<\/h2>\n<p>Giving your users a good video experience will make them spend more time on your site and lead to fewer support issues. Plus it only takes a little extra time. Once the functionality is there, you just have to decide on how you want it to look.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":27919,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[379,134,246,371,303],"class_list":["post-27918","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-graphql","tag-guest-post","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>How To Make A Custom Video Player<\/title>\n<meta name=\"description\" content=\"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we&#039;ll create a quick full-stack custom video player using Redwood. We&#039;ll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.\" \/>\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\/how-to-make-a-custom-video-player-in-redwood\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How To Make A Custom Video Player\" \/>\n<meta property=\"og:description\" content=\"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we&#039;ll create a quick full-stack custom video player using Redwood. We&#039;ll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-23T22:59:46+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"4000\" \/>\n\t<meta property=\"og:image:height\" content=\"2800\" \/>\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\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"How To Make A Custom Video Player\",\"datePublished\":\"2022-03-23T22:59:46+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\"},\"wordCount\":7,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA\",\"keywords\":[\"GraphQL\",\"Guest Post\",\"React\",\"Under Review\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\",\"name\":\"How To Make A Custom Video Player\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA\",\"datePublished\":\"2022-03-23T22:59:46+00:00\",\"description\":\"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we'll create a quick full-stack custom video player using Redwood. We'll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA\",\"width\":4000,\"height\":2800},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How To Make A Custom Video Player\"}]},{\"@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":"How To Make A Custom Video Player","description":"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we'll create a quick full-stack custom video player using Redwood. We'll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.","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\/how-to-make-a-custom-video-player-in-redwood\/","og_locale":"en_US","og_type":"article","og_title":"How To Make A Custom Video Player","og_description":"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we'll create a quick full-stack custom video player using Redwood. We'll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-23T22:59:46+00:00","og_image":[{"width":4000,"height":2800,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/"},"author":{"name":"","@id":""},"headline":"How To Make A Custom Video Player","datePublished":"2022-03-23T22:59:46+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/"},"wordCount":7,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","keywords":["GraphQL","Guest Post","React","Under Review","Video"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/","name":"How To Make A Custom Video Player","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","datePublished":"2022-03-23T22:59:46+00:00","description":"Making sure your users have a good video experience is important for any media-based product. In this tutorial, we'll create a quick full-stack custom video player using Redwood. We'll set up a Postgres database using Prisma, create a back-end with GraphQL, and finish the front-end with React.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","width":4000,"height":2800},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/how-to-make-a-custom-video-player-in-redwood\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"How To Make A Custom Video Player"}]},{"@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\/v1681925891\/Web_Assets\/blog\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b\/8bca3cdba8c5aae7e72e02461a1b5da93c78ca4d-4000x2800-1_2791934a9b.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27918","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=27918"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27918\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/27919"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=27918"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=27918"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=27918"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}