{"id":28028,"date":"2022-03-21T19:02:58","date_gmt":"2022-03-21T19:02:58","guid":{"rendered":"http:\/\/Adding-Subtitles-to-Video-Content"},"modified":"2022-03-21T19:02:58","modified_gmt":"2022-03-21T19:02:58","slug":"adding-subtitles-to-video-content","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/","title":{"rendered":"Adding Subtitles to Video Content with Redwood"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Sometimes you just want to watch videos with the subtitles on. It might be because you want to mute the video and just read what\u2019s happening. Or maybe you\u2019re using subtitles to help teach yourself a different language.<\/p>\n<p>These are a few reasons why it\u2019s important to know how to add subtitles to videos in your web apps. It\u2019s also important to include subtitles for accessibility purposes so that everyone can get the information from your videos. So we\u2019re going to build a Redwood app that uses Cloudinary to display video subtitles using <code>.srt<\/code> files.<\/p>\n<h2>Setting up the project<\/h2>\n<p>To get started we\u2019ll initialize a new Redwood project with the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn create redwood-app --typescript video-subtitles\n<\/code><\/span><\/pre>\n<p>This command generates a functioning full-stack app. There are a number of files and folders, but we\u2019ll mainly work in the <code>api<\/code> and <code>web<\/code> directories. These contain the code for the front-end and back-end respectively. This project uses the <code>--typescript<\/code> flag so we\u2019ll be able to add types for everything from the beginning.<\/p>\n<p>Let\u2019s start with the back-end and set up our database.<\/p>\n<h2>Writing the database schema<\/h2>\n<p>Open the <code>schema.prisma<\/code> file in <code>api &gt; db<\/code>. This is where we\u2019ll set up our database connection and the schema for all of the tables. The first thing we\u2019ll do is update the <code>provider<\/code> value from <code>sqlite<\/code> to <code>postgresql<\/code>. This is how we tell the app to connect to our Postgres instance.<\/p>\n<p>Next, we need to update the <code>DATABASE_URL<\/code> environment variable in the <code>.env<\/code> file to your local Postgres. Your connection string might look similar to this:<\/p>\n<pre class=\"js-syntax-highlighted\"><code>DATABASE_URL=postgres:\/\/postgres:admin@localhost:5432\/subtitles\n<\/code><\/pre>\n<p>Don\u2019t worry if you don\u2019t have a database called <code>subtitles<\/code>. When we run the migration, this will be created automatically.<\/p>\n<p>Now we can define the model for the videos we\u2019ll upload. You can delete the <code>UserExample<\/code> model and replace it with the following code:<\/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\">model Video {\n  id        Int     @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  url       <span class=\"hljs-built_in\">String<\/span>\n  srtFile   <span class=\"hljs-built_in\">String<\/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 defines the table and the columns we\u2019ll need for the database to store the videos and their subtitle files. We\u2019re going to do one more thing before doing the database migration.<\/p>\n<h3>Seeding the database with data<\/h3>\n<p>We\u2019ll seed the database with a subtitle file to have data to get started with. Check out the <code>scripts<\/code> folder and you\u2019ll see a file called <code>seed.ts<\/code>. This is where we\u2019ll add the data for the video. You can <a href=\"https:\/\/github.com\/flippedcoder\/media-projects\/blob\/main\/video-subtitles\/sample.srt\">download an example of a <code>.srt<\/code> file here<\/a> and save it to the root of your project.<\/p>\n<p>If you don\u2019t have <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">a Cloudinary account<\/a>, this is the perfect time to make one so that you can upload a video and the subtitle file. Once you have these two files uploaded to your Cloudinary account, then you can update the code in <code>seed.ts<\/code> to match the <code>Video<\/code> schema we have.<\/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-keyword\">import<\/span> type { Prisma } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@prisma\/client'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { db } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'api\/src\/lib\/db'<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> data: Prisma.VideoCreateInput&#91;<span class=\"hljs-string\">'data'<\/span>]&#91;] = &#91;\n      { <span class=\"hljs-attr\">url<\/span>: <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${cloudName}<\/span>\/video\/upload\/v1606580790\/elephant_herd.mp4`<\/span>, <span class=\"hljs-attr\">srtFile<\/span>: <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${cloudName}<\/span>\/raw\/upload\/v1643650731\/sample_nyiy7a.srt`<\/span> }\n    ]\n    <span class=\"hljs-built_in\">console<\/span>.log(\n      <span class=\"hljs-string\">\"\\nUsing the default '.\/scripts\/seed.{js,ts}' template\\nEdit the file to add seed data\\n\"<\/span>\n    )\n\n    <span class=\"hljs-built_in\">Promise<\/span>.all(\n      data.map(<span class=\"hljs-keyword\">async<\/span> (data: Prisma.VideoCreateInput&#91;<span class=\"hljs-string\">'data'<\/span>]) =&gt; {\n        <span class=\"hljs-keyword\">const<\/span> record = <span class=\"hljs-keyword\">await<\/span> db.video.create({ data })\n        <span class=\"hljs-built_in\">console<\/span>.log(record)\n      })\n    )\n  } <span class=\"hljs-keyword\">catch<\/span> (error) {\n    <span class=\"hljs-built_in\">console<\/span>.warn(<span class=\"hljs-string\">'Please define your seed data.'<\/span>)\n    <span class=\"hljs-built_in\">console<\/span>.error(error)\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>An important thing to note in this code is that you need to update the <code>url<\/code> and <code>srtFile<\/code> values to use the links from your Cloudinary account. Since this is the only data we\u2019re going to seed in the database, we can go ahead and run the migration with the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood prisma migrate dev\n<\/code><\/span><\/pre>\n<p>This command creates the <code>subtitles<\/code> database if it doesn\u2019t exist, creates the <code>Video<\/code> table schema, and adds the seed data to the table. If you check your local Postgres instance, you should see a record in the <code>Video<\/code> table already.<\/p>\n<p>Now that we\u2019ve done everything we need to with the database setup, let\u2019s move on to the GraphQL server.<\/p>\n<h2>Making the GraphQL types and queries<\/h2>\n<p>One of my favorite things about Redwood is that once you have all of your database schema defined, there are a few commands that make it super easy to generate all of the types, queries, and mutations to handle all of the CRUD operations in GraphQL. So let\u2019s run the following command to generate all the things we need on the back-end.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate sdl video --crud\n<\/code><\/span><\/pre>\n<p>This one command has just generated all of the files and code we need in order to work with our data on the front-end. Take a look in <code>api &gt; src &gt; graphql<\/code> and you\u2019ll see the <code>video.sdl.ts<\/code> file. This has all of the types we need in GraphQL for the video data based on how we defined the database schema. Now look over in <code>api &gt; src &gt; services &gt; videos<\/code>. This has the file with all of the queries and mutations we need to execute actions on the database.<\/p>\n<p>That\u2019s all of the GraphQL requirements met with a single command. Now we can turn our attention over to the front-end where most of the interesting work is happening because we\u2019ll be using Cloudinary\u2019s video uploader and their upload API.<\/p>\n<h2>Creating the video page<\/h2>\n<p>Moving over to the front-end, take a look in the <code>web &gt; src<\/code> directory. This is where the rest of our work will take place. We\u2019ll start by adding the package we need to use the Cloudinary widget. Go to the <code>web<\/code> directory in your terminal and run the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn add react-cloudinary-upload-widget\n<\/code><\/span><\/pre>\n<p>Then we\u2019ll make a new page that will handle the uploads for both our videos and subtitle files and it will also display the videos we\u2019ve uploaded before with the subtitles enabled. To make this new page, run the following command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate page video\n<\/code><\/span><\/pre>\n<p>This will create a new page component in <code>web &gt; src &gt; pages &gt; VideoPage<\/code> and it will update the <code>Routes.tsx<\/code> file to include this new page. We\u2019re going to start by adding the video upload functionality to this page.<\/p>\n<h3>Uploading the videos to Cloudinary<\/h3>\n<p>So open the <code>VideoPage.tsx<\/code> file and take a look at the boilerplate code. We\u2019ll use the Cloudinary upload widget to handle our videos and we\u2019ll keep a record of the URL for display later. You can delete the code below the <code>&lt;h1&gt;<\/code> element inside the component. That will leave your file looking like this.<\/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\">import<\/span> { Link, routes } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/router'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { MetaTags } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> VideoPage = <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;&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">MetaTags<\/span> <span class=\"hljs-attr\">title<\/span>=<span class=\"hljs-string\">\"Video\"<\/span> <span class=\"hljs-attr\">description<\/span>=<span class=\"hljs-string\">\"Video page\"<\/span> \/&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>VideoPage<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n  )\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> VideoPage\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>We\u2019ll replace the unused import with the following import. This will give us access to that widget and all of its functionality.<\/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> { WidgetLoader, Widget } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-cloudinary-upload-widget'<\/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>Before we jump into using this widget component, let\u2019s define a couple more environment variables. Open the <code>.env<\/code> file and add the following values below your <code>DATABASE_URL<\/code>. These will protect your sensitive information from being visible directly in the code. You can find the values for these variables in your Cloudinary dashboard.<\/p>\n<pre class=\"js-syntax-highlighted\"><code>CLOUD_NAME=your_cloud_name\nUPLOAD_PRESET=the_preset_name\n<\/code><\/pre>\n<p>Now we can turn back to the <code>VideoPage<\/code> component. Let\u2019s add an empty upload function that we\u2019ll update later and the upload widget itself.<\/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\">\n<span class=\"hljs-keyword\">import<\/span> { WidgetLoader, Widget } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-cloudinary-upload-widget'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { MetaTags } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> VideoPage = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadVideoFn<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n\n  }\n\n  <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\">MetaTags<\/span> <span class=\"hljs-attr\">title<\/span>=<span class=\"hljs-string\">\"Video\"<\/span> <span class=\"hljs-attr\">description<\/span>=<span class=\"hljs-string\">\"Video page\"<\/span> \/&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h1<\/span>&gt;<\/span>VideoPage<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h1<\/span>&gt;<\/span>\n\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">WidgetLoader<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Widget<\/span>\n        <span class=\"hljs-attr\">sources<\/span>=<span class=\"hljs-string\">{&#91;<\/span>'<span class=\"hljs-attr\">local<\/span>', '<span class=\"hljs-attr\">camera<\/span>']}\n        <span class=\"hljs-attr\">cloudName<\/span>=<span class=\"hljs-string\">{process.env.CLOUD_NAME}<\/span>\n        <span class=\"hljs-attr\">uploadPreset<\/span>=<span class=\"hljs-string\">{process.env.UPLOAD_PRESET}<\/span>\n        <span class=\"hljs-attr\">buttonText<\/span>=<span class=\"hljs-string\">{<\/span>'<span class=\"hljs-attr\">Add<\/span> <span class=\"hljs-attr\">Video<\/span>'}\n        <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n          <span class=\"hljs-attr\">color:<\/span> '<span class=\"hljs-attr\">white<\/span>',\n          <span class=\"hljs-attr\">border:<\/span> '<span class=\"hljs-attr\">none<\/span>',\n          <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">120px<\/span>',\n          <span class=\"hljs-attr\">backgroundColor:<\/span> '<span class=\"hljs-attr\">green<\/span>',\n          <span class=\"hljs-attr\">borderRadius:<\/span> '<span class=\"hljs-attr\">4px<\/span>',\n          <span class=\"hljs-attr\">height:<\/span> '<span class=\"hljs-attr\">25px<\/span>',\n        }}\n        <span class=\"hljs-attr\">folder<\/span>=<span class=\"hljs-string\">{<\/span>'<span class=\"hljs-attr\">subtitled_videos<\/span>'}\n        <span class=\"hljs-attr\">onSuccess<\/span>=<span class=\"hljs-string\">{uploadVideoFn}<\/span>\n      \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n  )\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> VideoPage\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>If you run the app now, you\u2019ll be able to see the uploader button and clicking it will open the widget. You can run the app with the following command. Make sure you\u2019re at the root of the project in your terminal.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood dev\n<\/code><\/span><\/pre>\n<p>Going to the <code>video<\/code> route will show you views similar to these.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1644351114\/e-603fc55d218a650069f5228b\/vygp7whlbss2enr9ogka.png\" alt=\"uploader button on the page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1928\" height=\"1142\"\/><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1644351128\/e-603fc55d218a650069f5228b\/qjnftj2otxaehoe8ctoj.png\" alt=\"uploader opened to the main card after button click\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1173\"\/><\/p>\n<p>Now we need to update that <code>uploadVideoFn<\/code>. This is where we\u2019ll save the uploaded video\u2019s URL until we have the subtitle file as well. Add this code to the placeholder function.<\/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-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadVideoFn<\/span>(<span class=\"hljs-params\">results: CloudinaryResult<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> imageInfo = results.info\n\n  setUrl(imageInfo.url)\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>There are a couple of things we need to do before you can use this function. First, let\u2019s add a new import to use the state hook in React. So add this to the top of the import list.<\/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\">import<\/span> { useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/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>Next, add the type definition for the data we\u2019ll be using from the Cloudinary response below the import statements.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">interface<\/span> <span class=\"hljs-selector-tag\">CloudinaryResult<\/span> {\n  <span class=\"hljs-attribute\">info<\/span>: {\n    url: string\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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>Then right inside of the component, above the <code>uploadVideoFn<\/code>, add the following states. We\u2019ll only use one for now, but we\u2019ll need the other really soon.<\/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> &#91;url, setUrl] = useState&lt;string&gt;(<span class=\"hljs-string\">\"\"<\/span>)\n<span class=\"hljs-keyword\">const<\/span> &#91;srtFile, setSrtFile] = useState&lt;string&gt;(<span class=\"hljs-string\">\"\"<\/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>The video uploader is finished now! You can test it out by uploading a video in the browser and checking the state of the app in the developer tools. Now we can move on to the subtitle uploader.<\/p>\n<h3>Uploading the subtitles file<\/h3>\n<p>We\u2019ll need a way for users to upload new subtitle files along with their videos so we\u2019ll add a button with this functionality and it\u2019ll make a call to the Cloudinary API. This time, let\u2019s start by defining the <code>uploadSubtitleFn<\/code> before we create the file upload input. So let\u2019s add this function below <code>uploadVideoFn<\/code>.<\/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\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadSubtitleFn<\/span>(<span class=\"hljs-params\">e<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> uploadApi = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${process.env.CLOUD_NAME}<\/span>\/raw\/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>, e.currentTarget.value)\n  formData.append(<span class=\"hljs-string\">'upload_preset'<\/span>, process.env.CLOUD_NAME)\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  setSrtFile(cloudinaryRes.url)\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>Then we can add the file input element that will trigger this function whenever a new <code>.srt<\/code> file gets uploaded. This element will go just below the <code>&lt;Widget&gt;<\/code>.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" 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\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"file\"<\/span> <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{uploadSubtitleFn}<\/span> \/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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>That\u2019s all for the upload functionality. The last thing we need to do is add a button that will handle the creation of a database record with the URLs for the video and subtitles.<\/p>\n<h3>Saving the record<\/h3>\n<p>We need to update one of the import statements we have. Add the two new objects to this import.<\/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\">import<\/span> { MetaTags, useMutation, useQuery } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\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>Next, let\u2019s define the mutation we need to create a new video record. This will go right below the import statements.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" 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> CREATE_VIDEO_MUTATION = gql`\n  mutation CreateVideoMutation($input: CreateVideoInput!) {\n    createVideo(input: $input) {\n      id\n      url\n      srtFile\n    }\n  }\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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>Then we\u2019ll define the method we\u2019ll use when we\u2019re ready to submit the video. This will go inside the component, right above the <code>useState<\/code> declarations.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-14\" 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;createVideo] = useMutation(CREATE_VIDEO_MUTATION)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><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 can add one more function that will let us handle video record submissions. This will go just below the <code>uploadSubtitleFn<\/code>.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createVideoRecord<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> input = {\n    <span class=\"hljs-attr\">url<\/span>: url,\n    <span class=\"hljs-attr\">srtFile<\/span>: srtFile,\n  }\n\n  createVideo({ <span class=\"hljs-attr\">variables<\/span>: { input } })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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 calls the GraphQL mutation we created on the back-end earlier and adds the new info to the table. All that\u2019s left is adding the button. We\u2019ll put that below the file input.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-16\" 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\">button<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">block<\/span>' }} <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{createVideoRecord}<\/span>&gt;<\/span>Make this video record<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><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>Whenever this button is clicked, it will attempt to create a new video record so make sure that your states have valid values. This is a great place to practice implementing input validation and error handling. If you run your app at this point with <code>yarn rw dev<\/code>, then 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\/v1644351165\/e-603fc55d218a650069f5228b\/a6lshgerj5xviazlkgcd.png\" alt=\"app with all of the upload and create buttons\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1736\" height=\"1058\"\/><\/p>\n<p>All that\u2019s left is the query and displaying of the videos we upload with their subtitles!<\/p>\n<h3>Displaying the video with subtitles<\/h3>\n<p>There\u2019s a GraphQL query that we need to define to get all of the video data. We\u2019ll do that right above the mutation definition.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-17\" 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> GET_VIDEOS = gql<span class=\"hljs-string\">`\n  query {\n    videos {\n      url\n      srtFile\n    }\n  }\n`<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><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 we\u2019ll add a few objects from the <code>useQuery<\/code> hook we use to get the data back. Add this line right above the mutation method inside the component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">...\n<span class=\"hljs-keyword\">const<\/span> { data, loading } = useQuery(GET_VIDEOS)\n<span class=\"hljs-keyword\">const<\/span> &#91;createVideo] = useMutation(CREATE_VIDEO_MUTATION)\n<span class=\"hljs-keyword\">const<\/span> &#91;url, setUrl] = useState&lt;string&gt;(<span class=\"hljs-string\">\"\"<\/span>)\n...\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><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>Any time we\u2019re working with loading data on the front-end, it\u2019s a great practice to have something displayed while the data is loading to prevent the app from crashing. That\u2019s where the <code>loading<\/code> value comes in. It tells us if the video data is still being fetched. We\u2019ll add a simple loading element above the return statement.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><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<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><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>Lastly, we need to display the videos with their subtitles. To do that, we\u2019ll need to add some <code>&lt;video&gt;<\/code> elements for every video returned from the query. This will go below the video record creation button.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-20\" 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\">div<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">block<\/span>' }}&gt;<\/span>\n  {data?.videos &amp;&amp;\n    data?.videos.map(image =&gt; (\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">video<\/span>\n        <span class=\"hljs-attr\">controls<\/span>\n        <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{image.name}<\/span>\n        <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">height:<\/span> '<span class=\"hljs-attr\">500px<\/span>', <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">500px<\/span>' }}\n      &gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">source<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">https:<\/span>\/\/<span class=\"hljs-attr\">res.cloudinary.com<\/span>\/<span class=\"hljs-attr\">demo<\/span>\/<span class=\"hljs-attr\">video<\/span>\/<span class=\"hljs-attr\">upload<\/span>\/<span class=\"hljs-attr\">l_subtitles:sample_heeir8.srt<\/span>\/<span class=\"hljs-attr\">elephant_herd.mp4<\/span>`}&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">source<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">video<\/span>&gt;<\/span>\n    ))\n  }\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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>We\u2019re using a custom Cloudinary URL with the names of the video and subtitle files we uploaded together. When you reload the page with your app running, 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\/v1644351193\/e-603fc55d218a650069f5228b\/xsikc2bwcrl9idu94ixw.png\" alt=\"all the upload buttons and the videos with subtitles\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1666\" height=\"1382\"\/><\/p>\n<p>Now we\u2019re done!<\/p>\n<h2>Finished code<\/h2>\n<p>You might want to take a look at the complete code over here in <a href=\"https:\/\/github.com\/flippedcoder\/media-projects\/tree\/main\/video-subtitles\">the <code>video-subtitles<\/code> folder of this repo<\/a>.<\/p>\n<p>Or you can check out some of the front-end functionality in <a href=\"https:\/\/codesandbox.io\/s\/kind-paper-mkefy\">this Code Sandbox<\/a>.<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/kind-paper-mkefy?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=\"kind-paper-mkefy\"\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 \"><h2>Conclusion<\/h2>\n<p>Having subtitles on videos is a crucial step for keeping content online accessible to everyone. The <code>.srt<\/code> are well worth the time to learn how to write. It\u2019s just a specific format you have to follow in a text file. So hopefully this made everything look easy to you and now you can add subtitles to all the videos!<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":28029,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[337,134,246,371,303],"class_list":["post-28028","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-accessibility","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>Adding Subtitles to Video Content with Redwood<\/title>\n<meta name=\"description\" content=\"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That&#039;s why you&#039;ll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.\" \/>\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\/adding-subtitles-to-video-content\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Adding Subtitles to Video Content with Redwood\" \/>\n<meta property=\"og:description\" content=\"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That&#039;s why you&#039;ll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-21T19:02:58+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"4928\" \/>\n\t<meta property=\"og:image:height\" content=\"3264\" \/>\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\/adding-subtitles-to-video-content\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Adding Subtitles to Video Content with Redwood\",\"datePublished\":\"2022-03-21T19:02:58+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/\"},\"wordCount\":7,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA\",\"keywords\":[\"Accessibility\",\"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\/adding-subtitles-to-video-content\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/\",\"name\":\"Adding Subtitles to Video Content with Redwood\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA\",\"datePublished\":\"2022-03-21T19:02:58+00:00\",\"description\":\"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That's why you'll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA\",\"width\":4928,\"height\":3264},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Adding Subtitles to Video Content with Redwood\"}]},{\"@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":"Adding Subtitles to Video Content with Redwood","description":"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That's why you'll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.","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\/adding-subtitles-to-video-content\/","og_locale":"en_US","og_type":"article","og_title":"Adding Subtitles to Video Content with Redwood","og_description":"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That's why you'll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-21T19:02:58+00:00","og_image":[{"width":4928,"height":3264,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.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\/adding-subtitles-to-video-content\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/"},"author":{"name":"","@id":""},"headline":"Adding Subtitles to Video Content with Redwood","datePublished":"2022-03-21T19:02:58+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/"},"wordCount":7,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA","keywords":["Accessibility","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\/adding-subtitles-to-video-content\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/","name":"Adding Subtitles to Video Content with Redwood","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA","datePublished":"2022-03-21T19:02:58+00:00","description":"You can find subtitles in almost every type of video content. From Netflix to TikTok, subtitles are expected to be there in some form. That's why you'll learn how to make a subtitle file and how to show them on your own videos with Cloudinary in this tutorial.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA","width":4928,"height":3264},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-subtitles-to-video-content\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Adding Subtitles to Video Content with Redwood"}]},{"@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\/v1681925637\/Web_Assets\/blog\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783\/e3075709852bae28eff5f9d030e715c097b854a4-4928x3264-1_28029dc783.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28028","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=28028"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28028\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/28029"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=28028"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=28028"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=28028"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}